# frozen_string_literal: true # spec/models/grid_character_spec.rb # # This spec verifies the GridCharacter model’s associations, validations, # and callbacks. It uses FactoryBot for object creation, shoulda-matchers # for association/validation shortcuts, and a custom matcher (have_error_on) # for checking that error messages include specific phrases. # # In this version we use canonical data loaded from CSV (via our CSV loader) # rather than generating new Character and Awakening records. # require 'rails_helper' RSpec.describe GridCharacter, type: :model do # Association tests using shoulda-matchers. it { is_expected.to belong_to(:character) } it { is_expected.to belong_to(:party) } it { is_expected.to belong_to(:awakening).optional } it { is_expected.to belong_to(:collection_character).optional } # Use the canonical "Balanced" awakening already loaded from CSV. before(:all) do @balanced_awakening = Awakening.find_by!(slug: 'character-balanced') end # Use canonical records loaded from CSV for our character. let(:party) { create(:party) } let(:character) do # Assume canonical test data has been loaded. Character.find_by!(granblue_id: '3040087000') end let(:valid_attributes) do { party: party, character: character, position: 0, uncap_level: 3, transcendence_step: 0 } end describe 'Validations and Associations' do context 'with valid attributes' do subject { build(:grid_character, valid_attributes) } it 'is valid' do expect(subject).to be_valid end end context 'without a party' do subject { build(:grid_character, valid_attributes.merge(party: nil)) } it 'is invalid' do subject.valid? expect(subject.errors[:party]).to include("can't be blank") end end end describe 'Callbacks' do context 'before_validation :apply_new_rings' do it 'sets the ring attributes when new_rings is provided' do grid_char = build( :grid_character, valid_attributes.merge(new_rings: [ { 'modifier' => '1', 'strength' => 300 }, { 'modifier' => '2', 'strength' => 150 } ]) ) grid_char.valid? # triggers the before_validation callback expect(grid_char.ring1).to eq({ 'modifier' => '1', 'strength' => 300 }) expect(grid_char.ring2).to eq({ 'modifier' => '2', 'strength' => 150 }) # The rings array is padded to have exactly four entries. expect(grid_char.ring3).to eq({ 'modifier' => nil, 'strength' => nil }) expect(grid_char.ring4).to eq({ 'modifier' => nil, 'strength' => nil }) end end context 'before_validation :apply_new_awakening' do it 'sets awakening_id and awakening_level when new_awakening is provided using a canonical awakening' do # Use an existing awakening from the CSV data. canonical_awakening = Awakening.find_by!(slug: 'character-def') new_awakening = { id: canonical_awakening.id, level: '5' } grid_char = build(:grid_character, valid_attributes.merge(new_awakening: new_awakening)) grid_char.valid? expect(grid_char.awakening_id).to eq(canonical_awakening.id) expect(grid_char.awakening_level).to eq(5) end end context 'before_save :add_awakening' do it 'sets the awakening to the balanced canonical awakening if none is provided' do grid_char = build(:grid_character, valid_attributes.merge(awakening: nil)) grid_char.save! expect(grid_char.awakening).to eq(@balanced_awakening) end it 'does not override an existing awakening' do existing_awakening = Awakening.find_by!(slug: 'character-def') grid_char = build(:grid_character, valid_attributes.merge(awakening: existing_awakening)) grid_char.save! expect(grid_char.awakening).to eq(existing_awakening) end end end describe 'Update Validations (on :update)' do before do # Persist a valid GridCharacter record. @grid_char = create(:grid_character, valid_attributes) end context 'validate_awakening_level' do it 'adds an error if awakening_level is below 1' do @grid_char.awakening_level = 0 @grid_char.valid?(:update) expect(@grid_char.errors[:awakening]).to include('awakening level too low') end it 'adds an error if awakening_level is above 9' do @grid_char.awakening_level = 10 @grid_char.valid?(:update) expect(@grid_char.errors[:awakening]).to include('awakening level too high') end end context 'transcendence validation' do it 'adds an error if transcendence_step is positive but character.ulb is false' do @grid_char.character.update!(ulb: false) @grid_char.transcendence_step = 1 @grid_char.valid?(:update) expect(@grid_char.errors[:transcendence_step]).to include('character has no transcendence') end it 'adds an error if transcendence_step is greater than 5 when character.ulb is true' do @grid_char.character.update!(ulb: true) @grid_char.transcendence_step = 6 @grid_char.valid?(:update) expect(@grid_char.errors[:transcendence_step]).to include('transcendence step too high') end it 'adds an error if transcendence_step is negative when character.ulb is true' do @grid_char.character.update!(ulb: true) @grid_char.transcendence_step = -1 @grid_char.valid?(:update) expect(@grid_char.errors[:transcendence_step]).to include('transcendence step too low') end end context 'over_mastery_attack_matches_hp validation' do it 'adds an error if ring1 and ring2 values are inconsistent' do @grid_char.ring1 = { modifier: '1', strength: 300 } # Expected: ring2 strength should be half of 300 (i.e. 150) @grid_char.ring2 = { modifier: '2', strength: 100 } @grid_char.valid?(:update) expect(@grid_char.errors[:over_mastery]).to include('over mastery attack and hp values do not match') end it 'is valid if ring2 strength equals half of ring1 strength' do @grid_char.ring1 = { modifier: '1', strength: 300 } @grid_char.ring2 = { modifier: '2', strength: 150 } @grid_char.valid?(:update) expect(@grid_char.errors[:over_mastery]).to be_empty end end end describe 'Collection Sync' do let(:user) { create(:user) } let(:collection_character) do create(:collection_character, user: user, character: character, uncap_level: 5, transcendence_step: 3, perpetuity: true, ring1: { 'modifier' => '1', 'strength' => 1500 }, ring2: { 'modifier' => '2', 'strength' => 750 }, ring3: { 'modifier' => nil, 'strength' => nil }, ring4: { 'modifier' => nil, 'strength' => nil }, earring: { 'modifier' => '3', 'strength' => 20 }, awakening: @balanced_awakening, awakening_level: 7) end describe '#sync_from_collection!' do context 'when collection_character is linked' do before do character.update!(ulb: true) # Enable transcendence @grid_char = create(:grid_character, valid_attributes.merge( collection_character: collection_character, uncap_level: 3, transcendence_step: 0 )) end it 'copies all customizations from collection' do expect(@grid_char.sync_from_collection!).to be true @grid_char.reload expect(@grid_char.uncap_level).to eq(5) expect(@grid_char.transcendence_step).to eq(3) expect(@grid_char.perpetuity).to be true expect(@grid_char.ring1).to eq({ 'modifier' => '1', 'strength' => 1500 }) expect(@grid_char.ring2).to eq({ 'modifier' => '2', 'strength' => 750 }) expect(@grid_char.awakening_level).to eq(7) end end context 'when no collection_character is linked' do before do @grid_char = create(:grid_character, valid_attributes) end it 'returns false and does not change anything' do original_uncap = @grid_char.uncap_level expect(@grid_char.sync_from_collection!).to be false expect(@grid_char.uncap_level).to eq(original_uncap) end end end describe '#out_of_sync?' do context 'when collection_character is linked' do before do character.update!(ulb: true) @grid_char = create(:grid_character, valid_attributes.merge(collection_character: collection_character)) end it 'returns true when uncap_level differs' do @grid_char.update!(uncap_level: 4) expect(@grid_char.out_of_sync?).to be true end it 'returns true when transcendence_step differs' do @grid_char.update!(transcendence_step: 1) expect(@grid_char.out_of_sync?).to be true end it 'returns true when perpetuity differs' do @grid_char.update!(perpetuity: false) expect(@grid_char.out_of_sync?).to be true end it 'returns false when all values match' do @grid_char.sync_from_collection! expect(@grid_char.out_of_sync?).to be false end end context 'when no collection_character is linked' do before do @grid_char = create(:grid_character, valid_attributes) end it 'returns false' do expect(@grid_char.out_of_sync?).to be false end end end end end