[WV2] Add tests for item sorting & grouping
This commit is contained in:
parent
e8d768961b
commit
f4417f7fb0
1 changed files with 310 additions and 0 deletions
310
spec/helpers/outfits_helper_spec.rb
Normal file
310
spec/helpers/outfits_helper_spec.rb
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe OutfitsHelper, type: :helper do
|
||||
fixtures :zones, :colors, :species, :pet_types
|
||||
|
||||
# Use the Blue Acara's body_id throughout tests
|
||||
let(:body_id) { pet_types(:blue_acara).body_id }
|
||||
|
||||
# Helper to create a test outfit with a pet type
|
||||
# Biology assets are just setup noise - we only care about pet_type.body_id
|
||||
def create_test_outfit
|
||||
pet_type = pet_types(:blue_acara)
|
||||
|
||||
# PetState requires at least one biology asset (validation requirement)
|
||||
bio_asset = SwfAsset.create!(
|
||||
type: "biology",
|
||||
remote_id: (@bio_remote_id = (@bio_remote_id || 1000) + 1),
|
||||
url: "https://images.neopets.example/bio_#{@bio_remote_id}.swf",
|
||||
zone: zones(:body),
|
||||
zones_restrict: "",
|
||||
body_id: 0
|
||||
)
|
||||
|
||||
pet_state = PetState.create!(
|
||||
pet_type: pet_type,
|
||||
pose: "HAPPY_MASC",
|
||||
swf_assets: [bio_asset],
|
||||
swf_asset_ids: [bio_asset.id]
|
||||
)
|
||||
|
||||
Outfit.create!(pet_state: pet_state)
|
||||
end
|
||||
|
||||
# Helper to create SwfAssets for items (matches pattern from item_spec.rb)
|
||||
def build_item_asset(zone, body_id:)
|
||||
@item_remote_id = (@item_remote_id || 0) + 1
|
||||
SwfAsset.create!(
|
||||
type: "object",
|
||||
remote_id: @item_remote_id,
|
||||
url: "https://images.neopets.example/item_#{@item_remote_id}.swf",
|
||||
zone: zone,
|
||||
zones_restrict: "",
|
||||
body_id: body_id
|
||||
)
|
||||
end
|
||||
|
||||
# Helper to create an item with zones
|
||||
def create_item(name, zones_and_bodies)
|
||||
item = Item.create!(
|
||||
name: name,
|
||||
description: "",
|
||||
thumbnail_url: "https://images.neopets.example/#{name.parameterize}.gif",
|
||||
rarity: "Common",
|
||||
price: 100,
|
||||
zones_restrict: "0" * 52
|
||||
)
|
||||
|
||||
zones_and_bodies.each do |zone, body_id|
|
||||
item.swf_assets << build_item_asset(zone, body_id: body_id)
|
||||
end
|
||||
|
||||
item
|
||||
end
|
||||
|
||||
describe '#outfit_items_by_zone' do
|
||||
context 'with nil pet_type' do
|
||||
it 'returns empty array' do
|
||||
# Create an outfit without a pet_state (pet_type will be nil)
|
||||
outfit = Outfit.new
|
||||
# Allow the delegation to fail gracefully
|
||||
allow(outfit).to receive(:pet_type).and_return(nil)
|
||||
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
expect(result).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with empty outfit' do
|
||||
it 'returns empty array' do
|
||||
outfit = create_test_outfit
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
expect(result).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with single-zone items' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
let!(:hat_item) { create_item("Blue Hat", [[zones(:hat), body_id]]) }
|
||||
let!(:jacket_item) { create_item("Red Jacket", [[zones(:jacket), body_id]]) }
|
||||
|
||||
before do
|
||||
outfit.worn_items << hat_item
|
||||
outfit.worn_items << jacket_item
|
||||
end
|
||||
|
||||
it 'groups items by zone' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
expect(result.length).to eq(2)
|
||||
zone_labels = result.map { |g| g[:zone_label] }
|
||||
expect(zone_labels).to contain_exactly("Hat", "Jacket")
|
||||
end
|
||||
|
||||
it 'sorts zones alphabetically' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
zone_labels = result.map { |g| g[:zone_label] }
|
||||
expect(zone_labels).to eq(["Hat", "Jacket"])
|
||||
end
|
||||
|
||||
it 'includes items in their respective zones' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
hat_group = result.find { |g| g[:zone_label] == "Hat" }
|
||||
jacket_group = result.find { |g| g[:zone_label] == "Jacket" }
|
||||
|
||||
expect(hat_group[:items]).to eq([hat_item])
|
||||
expect(jacket_group[:items]).to eq([jacket_item])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple items in same zone' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
let!(:hat1) { create_item("Awesome Hat", [[zones(:hat), body_id]]) }
|
||||
let!(:hat2) { create_item("Cool Hat", [[zones(:hat), body_id]]) }
|
||||
let!(:hat3) { create_item("Blue Hat", [[zones(:hat), body_id]]) }
|
||||
|
||||
before do
|
||||
outfit.worn_items << hat1
|
||||
outfit.worn_items << hat2
|
||||
outfit.worn_items << hat3
|
||||
end
|
||||
|
||||
it 'sorts items alphabetically within zone' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
hat_group = result.find { |g| g[:zone_label] == "Hat" }
|
||||
item_names = hat_group[:items].map(&:name)
|
||||
|
||||
expect(item_names).to eq(["Awesome Hat", "Blue Hat", "Cool Hat"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multi-zone item (no conflicts)' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
let!(:bow_tie) do
|
||||
create_item("Bow Tie", [
|
||||
[zones(:collar), body_id],
|
||||
[zones(:necklace), body_id],
|
||||
[zones(:earrings), body_id]
|
||||
])
|
||||
end
|
||||
|
||||
before do
|
||||
outfit.worn_items << bow_tie
|
||||
end
|
||||
|
||||
it 'shows item in only one zone (simplification)' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
# Should show in Collar zone only (first alphabetically)
|
||||
expect(result.length).to eq(1)
|
||||
expect(result[0][:zone_label]).to eq("Collar")
|
||||
expect(result[0][:items]).to eq([bow_tie])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multi-zone simplification (item appears in conflict zone)' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
let!(:multi_zone_item) do
|
||||
create_item("Fancy Outfit", [
|
||||
[zones(:jacket), body_id],
|
||||
[zones(:collar), body_id]
|
||||
])
|
||||
end
|
||||
let!(:collar_item) { create_item("Simple Collar", [[zones(:collar), body_id]]) }
|
||||
|
||||
before do
|
||||
outfit.worn_items << multi_zone_item
|
||||
outfit.worn_items << collar_item
|
||||
end
|
||||
|
||||
it 'keeps conflict zone and hides redundant single-item zone' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
zone_labels = result.map { |g| g[:zone_label] }
|
||||
|
||||
# Should show Collar (has conflict with 2 items)
|
||||
# Should NOT show Jacket (redundant - item already in Collar)
|
||||
expect(zone_labels).to eq(["Collar"])
|
||||
|
||||
collar_group = result.find { |g| g[:zone_label] == "Collar" }
|
||||
item_names = collar_group[:items].map(&:name).sort
|
||||
expect(item_names).to eq(["Fancy Outfit", "Simple Collar"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with zone label disambiguation' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
|
||||
# Create additional zones with duplicate labels for this test
|
||||
let!(:markings_zone_a) do
|
||||
Zone.create!(label: "Markings", depth: 100, plain_label: "markings_a", type_id: 2)
|
||||
end
|
||||
let!(:markings_zone_b) do
|
||||
Zone.create!(label: "Markings", depth: 101, plain_label: "markings_b", type_id: 2)
|
||||
end
|
||||
|
||||
let!(:item_zone_a) { create_item("Tattoo A", [[markings_zone_a, body_id]]) }
|
||||
let!(:item_zone_b) { create_item("Tattoo B", [[markings_zone_b, body_id]]) }
|
||||
let!(:item_zone_a_b) { create_item("Tattoo C", [[markings_zone_a, body_id]]) }
|
||||
|
||||
before do
|
||||
outfit.worn_items << item_zone_a
|
||||
outfit.worn_items << item_zone_b
|
||||
outfit.worn_items << item_zone_a_b
|
||||
end
|
||||
|
||||
it 'adds zone IDs to duplicate labels' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
zone_labels = result.map { |g| g[:zone_label] }
|
||||
|
||||
# Both should have IDs appended since they share the label "Markings"
|
||||
expect(zone_labels).to contain_exactly(
|
||||
"Markings (##{markings_zone_a.id})",
|
||||
"Markings (##{markings_zone_b.id})"
|
||||
)
|
||||
end
|
||||
|
||||
it 'groups items correctly by zone despite same label' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
zone_a_group = result.find { |g| g[:zone_label] == "Markings (##{markings_zone_a.id})" }
|
||||
zone_b_group = result.find { |g| g[:zone_label] == "Markings (##{markings_zone_b.id})" }
|
||||
|
||||
expect(zone_a_group[:items].map(&:name).sort).to eq(["Tattoo A", "Tattoo C"])
|
||||
expect(zone_b_group[:items].map(&:name)).to eq(["Tattoo B"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with incompatible items' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
let!(:compatible_item) { create_item("Fits Pet", [[zones(:hat), body_id]]) }
|
||||
let!(:incompatible_item) { create_item("Wrong Body", [[zones(:jacket), 999]]) }
|
||||
|
||||
before do
|
||||
outfit.worn_items << compatible_item
|
||||
outfit.worn_items << incompatible_item
|
||||
end
|
||||
|
||||
it 'separates incompatible items into their own section' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
zone_labels = result.map { |g| g[:zone_label] }
|
||||
expect(zone_labels).to contain_exactly("Hat", "Incompatible")
|
||||
|
||||
incompatible_group = result.find { |g| g[:zone_label] == "Incompatible" }
|
||||
expect(incompatible_group[:items]).to eq([incompatible_item])
|
||||
end
|
||||
|
||||
it 'sorts incompatible items alphabetically' do
|
||||
outfit.worn_items << create_item("Alpha Item", [[zones(:jacket), 999]])
|
||||
outfit.worn_items << create_item("Zulu Item", [[zones(:jacket), 999]])
|
||||
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
incompatible_group = result.find { |g| g[:zone_label] == "Incompatible" }
|
||||
|
||||
item_names = incompatible_group[:items].map(&:name)
|
||||
expect(item_names).to eq(["Alpha Item", "Wrong Body", "Zulu Item"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with complex multi-zone scenario' do
|
||||
let(:outfit) { create_test_outfit }
|
||||
let!(:bg1) { create_item("Forest Background", [[zones(:background), 0]]) }
|
||||
let!(:bg2) { create_item("Beach Background", [[zones(:background), 0]]) }
|
||||
let!(:multi_item) do
|
||||
create_item("Wings and Hat", [
|
||||
[zones(:wings), 0],
|
||||
[zones(:hat), 0]
|
||||
])
|
||||
end
|
||||
let!(:hat_item) { create_item("Simple Hat", [[zones(:hat), 0]]) }
|
||||
|
||||
before do
|
||||
outfit.worn_items << bg1
|
||||
outfit.worn_items << bg2
|
||||
outfit.worn_items << multi_item
|
||||
outfit.worn_items << hat_item
|
||||
end
|
||||
|
||||
it 'correctly applies all sorting and grouping rules' do
|
||||
result = helper.outfit_items_by_zone(outfit)
|
||||
|
||||
# Background: has conflict (2 items)
|
||||
# Hat: has conflict (2 items, including multi-zone item)
|
||||
# Wings: should be hidden (multi-zone item already in Hat conflict)
|
||||
zone_labels = result.map { |g| g[:zone_label] }
|
||||
expect(zone_labels).to eq(["Background", "Hat"])
|
||||
|
||||
bg_group = result.find { |g| g[:zone_label] == "Background" }
|
||||
expect(bg_group[:items].map(&:name)).to eq(["Beach Background", "Forest Background"])
|
||||
|
||||
hat_group = result.find { |g| g[:zone_label] == "Hat" }
|
||||
expect(hat_group[:items].map(&:name)).to eq(["Simple Hat", "Wings and Hat"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue