[WV2] Add tests for item sorting & grouping

This commit is contained in:
Emi Matchu 2025-11-03 00:31:05 +00:00
parent e8d768961b
commit f4417f7fb0

View 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