Add tests and docs to GridCharacter
We added a factory, spec and documentation to the GridCharacter model
This commit is contained in:
parent
ea4ce4dcdc
commit
65e03f62d3
3 changed files with 352 additions and 19 deletions
|
|
@ -1,24 +1,43 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
##
|
||||||
|
# This file defines the GridCharacter model which represents a character's grid configuration within a party.
|
||||||
|
# The GridCharacter model handles validations related to awakenings, rings, mastery values, and transcendence.
|
||||||
|
# It includes virtual attributes for processing new rings and awakening data, and utilizes the amoeba gem
|
||||||
|
# for duplicating records with specific attribute resets.
|
||||||
|
#
|
||||||
|
# @note This model belongs to a Character, an optional Awakening, and a Party. It maintains associations for
|
||||||
|
# these relationships and includes counter caches for performance optimization.
|
||||||
|
#
|
||||||
|
# @!attribute [r] character
|
||||||
|
# @return [Character] the associated character record.
|
||||||
|
# @!attribute [r] awakening
|
||||||
|
# @return [Awakening, nil] the associated awakening record (optional).
|
||||||
|
# @!attribute [r] party
|
||||||
|
# @return [Party] the associated party record.
|
||||||
|
#
|
||||||
class GridCharacter < ApplicationRecord
|
class GridCharacter < ApplicationRecord
|
||||||
|
# Associations
|
||||||
belongs_to :character, foreign_key: :character_id, primary_key: :id
|
belongs_to :character, foreign_key: :character_id, primary_key: :id
|
||||||
|
|
||||||
belongs_to :awakening, optional: true
|
belongs_to :awakening, optional: true
|
||||||
belongs_to :party,
|
belongs_to :party,
|
||||||
counter_cache: :characters_count,
|
counter_cache: :characters_count,
|
||||||
inverse_of: :characters
|
inverse_of: :characters
|
||||||
|
|
||||||
|
# Validations
|
||||||
validates_presence_of :party
|
validates_presence_of :party
|
||||||
|
|
||||||
|
# Validate that uncap_level and transcendence_step are present and numeric.
|
||||||
|
validates :uncap_level, presence: true, numericality: { only_integer: true }
|
||||||
|
validates :transcendence_step, presence: true, numericality: { only_integer: true }
|
||||||
|
|
||||||
validate :validate_awakening_level, on: :update
|
validate :validate_awakening_level, on: :update
|
||||||
validate :transcendence, on: :update
|
validate :transcendence, on: :update
|
||||||
validate :validate_over_mastery_values, on: :update
|
validate :validate_over_mastery_values, on: :update
|
||||||
validate :validate_aetherial_mastery_value, on: :update
|
validate :validate_aetherial_mastery_value, on: :update
|
||||||
validate :over_mastery_attack_matches_hp, on: :update
|
|
||||||
|
|
||||||
# Virtual attribute for the new rings structure
|
# Virtual attributes
|
||||||
attr_accessor :new_rings
|
attr_accessor :new_rings
|
||||||
|
|
||||||
# Virtual attribute for the new awakening structure
|
|
||||||
attr_accessor :new_awakening
|
attr_accessor :new_awakening
|
||||||
|
|
||||||
##### Amoeba configuration
|
##### Amoeba configuration
|
||||||
|
|
@ -31,52 +50,121 @@ class GridCharacter < ApplicationRecord
|
||||||
set perpetuity: false
|
set perpetuity: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Hooks
|
||||||
before_validation :apply_new_rings, if: -> { new_rings.present? }
|
before_validation :apply_new_rings, if: -> { new_rings.present? }
|
||||||
before_validation :apply_new_awakening, if: -> { new_awakening.present? }
|
before_validation :apply_new_awakening, if: -> { new_awakening.present? }
|
||||||
|
|
||||||
# Add awakening before the model saves
|
|
||||||
before_save :add_awakening
|
before_save :add_awakening
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the awakening level to ensure it falls within the allowed range.
|
||||||
|
#
|
||||||
|
# @note Triggered on update.
|
||||||
|
# @return [void]
|
||||||
def validate_awakening_level
|
def validate_awakening_level
|
||||||
errors.add(:awakening, 'awakening level too low') if awakening_level < 1
|
errors.add(:awakening, 'awakening level too low') if awakening_level < 1
|
||||||
errors.add(:awakening, 'awakening level too high') if awakening_level > 9
|
errors.add(:awakening, 'awakening level too high') if awakening_level > 9
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the transcendence step of the character.
|
||||||
|
#
|
||||||
|
# Ensures that the transcendence step is appropriate based on the character's ULB status.
|
||||||
|
# Adds errors if:
|
||||||
|
# - The character has a positive transcendence_step but no transcendence (ulb is false).
|
||||||
|
# - The transcendence_step exceeds the allowed maximum.
|
||||||
|
# - The transcendence_step is negative when character.ulb is true.
|
||||||
|
#
|
||||||
|
# @note Triggered on update.
|
||||||
|
# @return [void]
|
||||||
def transcendence
|
def transcendence
|
||||||
errors.add(:transcendence_step, 'character has no transcendence') if transcendence_step.positive? && !character.ulb
|
errors.add(:transcendence_step, 'character has no transcendence') if transcendence_step.positive? && !character.ulb
|
||||||
errors.add(:transcendence_step, 'transcendence step too high') if transcendence_step > 5 && character.ulb
|
errors.add(:transcendence_step, 'transcendence step too high') if transcendence_step > 5 && character.ulb
|
||||||
errors.add(:transcendence_step, 'transcendence step too low') if transcendence_step.negative? && character.ulb
|
errors.add(:transcendence_step, 'transcendence step too low') if transcendence_step.negative? && character.ulb
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the over mastery attack value for ring1.
|
||||||
|
#
|
||||||
|
# Checks that if ring1's modifier is set, the strength must be one of the allowed attack values.
|
||||||
|
# Adds an error if the value is not valid.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def over_mastery_attack
|
def over_mastery_attack
|
||||||
errors.add(:ring1, 'invalid value') unless ring1['modifier'].nil? || atk_values.include?(ring1['strength'])
|
errors.add(:ring1, 'invalid value') unless ring1['modifier'].nil? || atk_values.include?(ring1['strength'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the over mastery HP value for ring2.
|
||||||
|
#
|
||||||
|
# If ring2's modifier is present, ensures that the strength is within the allowed HP values.
|
||||||
|
# Adds an error if the value is not valid.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def over_mastery_hp
|
def over_mastery_hp
|
||||||
return if ring2['modifier'].nil?
|
return if ring2['modifier'].nil?
|
||||||
|
|
||||||
errors.add(:ring2, 'invalid value') unless hp_values.include?(ring2['strength'])
|
errors.add(:ring2, 'invalid value') unless hp_values.include?(ring2['strength'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def over_mastery_attack_matches_hp
|
##
|
||||||
return if ring1[:modifier].nil? && ring2[:modifier].nil?
|
# Validates over mastery values by invoking individual and cross-field validations.
|
||||||
|
#
|
||||||
return if ring2[:strength] == (ring1[:strength] / 2)
|
# This method triggers:
|
||||||
|
# - Validation for individual over mastery values for rings 1-4.
|
||||||
errors.add(:over_mastery,
|
# - Validation ensuring that ring1's attack and ring2's HP values are consistent.
|
||||||
'over mastery attack and hp values do not match')
|
#
|
||||||
|
# @return [void]
|
||||||
|
def validate_over_mastery_values
|
||||||
|
validate_individual_over_mastery_values
|
||||||
|
validate_over_mastery_attack_matches_hp
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_over_mastery_values
|
##
|
||||||
|
# Validates individual over mastery values for each ring (ring1 to ring4).
|
||||||
|
#
|
||||||
|
# Iterates over each ring and, if a modifier is present, uses a helper to verify that the associated strength
|
||||||
|
# is within the permitted range based on over mastery rules.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def validate_individual_over_mastery_values
|
||||||
|
# Iterate over rings 1-4 and check each ring’s value.
|
||||||
[ring1, ring2, ring3, ring4].each_with_index do |ring, index|
|
[ring1, ring2, ring3, ring4].each_with_index do |ring, index|
|
||||||
next if ring['modifier'].nil?
|
next if ring['modifier'].nil?
|
||||||
|
|
||||||
modifier = over_mastery_modifiers[ring['modifier']]
|
modifier = over_mastery_modifiers[ring['modifier']]
|
||||||
check_value({ "ring#{index}": { ring[modifier] => ring['strength'] } },
|
# Use a helper to add errors if the value is out-of-range.
|
||||||
'over_mastery')
|
check_value({ "ring#{index}": { ring[modifier] => ring['strength'] } }, 'over_mastery')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates that the over mastery attack value matches the HP value appropriately.
|
||||||
|
#
|
||||||
|
# Converts ring1 and ring2 hashes to use indifferent access, and if either ring has a modifier set,
|
||||||
|
# checks that ring2's strength is exactly half of ring1's strength.
|
||||||
|
# Adds an error if the values do not match.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def validate_over_mastery_attack_matches_hp
|
||||||
|
# Convert ring1 and ring2 to use indifferent access so that keys (symbols or strings)
|
||||||
|
# can be accessed uniformly.
|
||||||
|
r1 = ring1.with_indifferent_access
|
||||||
|
r2 = ring2.with_indifferent_access
|
||||||
|
# Only check if either ring has a modifier set.
|
||||||
|
if r1[:modifier].present? || r2[:modifier].present?
|
||||||
|
# Ensure that ring2's strength equals exactly half of ring1's strength.
|
||||||
|
unless r2[:strength].to_f == (r1[:strength].to_f / 2)
|
||||||
|
errors.add(:over_mastery, 'over mastery attack and hp values do not match')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the aetherial mastery value for the earring.
|
||||||
|
#
|
||||||
|
# If the earring's modifier is present and positive, it uses a helper method to check that the strength
|
||||||
|
# falls within the allowed range for aetherial mastery.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def validate_aetherial_mastery_value
|
def validate_aetherial_mastery_value
|
||||||
return if earring['modifier'].nil?
|
return if earring['modifier'].nil?
|
||||||
|
|
||||||
|
|
@ -87,22 +175,40 @@ class GridCharacter < ApplicationRecord
|
||||||
'aetherial_mastery')
|
'aetherial_mastery')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns the blueprint for rendering the grid character.
|
||||||
|
#
|
||||||
|
# @return [GridCharacterBlueprint] the blueprint class used for grid character representation.
|
||||||
def blueprint
|
def blueprint
|
||||||
GridCharacterBlueprint
|
GridCharacterBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
##
|
||||||
|
# Adds a default awakening to the character before saving if none is set.
|
||||||
|
#
|
||||||
|
# Retrieves the Awakening record with slug 'character-balanced' and assigns it.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def add_awakening
|
def add_awakening
|
||||||
return unless awakening.nil?
|
return unless awakening.nil?
|
||||||
|
|
||||||
self.awakening = Awakening.where(slug: 'character-balanced').sole
|
self.awakening = Awakening.where(slug: 'character-balanced').sole
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Applies new ring configurations from the virtual attribute +new_rings+.
|
||||||
|
#
|
||||||
|
# Expects +new_rings+ to be an array of hashes with keys "modifier" and "strength".
|
||||||
|
# Pads the array with default ring hashes to ensure there are exactly four rings, then assigns them to
|
||||||
|
# ring1, ring2, ring3, and ring4.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def apply_new_rings
|
def apply_new_rings
|
||||||
# Expect new_rings to be an array of hashes, e.g.,
|
# Expect new_rings to be an array of hashes, e.g.,
|
||||||
# [{"modifier" => "1", "strength" => "1500"}, {"modifier" => "2", "strength" => "750"}]
|
# [{"modifier" => "1", "strength" => "1500"}, {"modifier" => "2", "strength" => "750"}]
|
||||||
default_ring = { "modifier" => nil, "strength" => nil }
|
default_ring = { 'modifier' => nil, 'strength' => nil }
|
||||||
rings_array = Array(new_rings).map(&:to_h)
|
rings_array = Array(new_rings).map(&:to_h)
|
||||||
# Pad with defaults so there are exactly four rings
|
# Pad with defaults so there are exactly four rings
|
||||||
rings_array.fill(default_ring, rings_array.size...4)
|
rings_array.fill(default_ring, rings_array.size...4)
|
||||||
|
|
@ -112,11 +218,29 @@ class GridCharacter < ApplicationRecord
|
||||||
self.ring4 = rings_array[3]
|
self.ring4 = rings_array[3]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Applies new awakening configuration from the virtual attribute +new_awakening+.
|
||||||
|
#
|
||||||
|
# Sets the +awakening_id+ and +awakening_level+ based on the provided hash.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def apply_new_awakening
|
def apply_new_awakening
|
||||||
self.awakening_id = new_awakening[:id]
|
self.awakening_id = new_awakening[:id]
|
||||||
self.awakening_level = new_awakening[:level].present? ? new_awakening[:level].to_i : 1
|
self.awakening_level = new_awakening[:level].present? ? new_awakening[:level].to_i : 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks that a given property value falls within the allowed range based on the specified mastery type.
|
||||||
|
#
|
||||||
|
# The +property+ parameter is expected to be a hash in the following format:
|
||||||
|
# { ring1: { atk: 300 } }
|
||||||
|
#
|
||||||
|
# Depending on the +type+, it validates against either over mastery or aetherial mastery values.
|
||||||
|
# Adds an error to the record if the value is not within the permitted range.
|
||||||
|
#
|
||||||
|
# @param property [Hash] the property hash containing the attribute and its value.
|
||||||
|
# @param type [String] the type of mastery validation to perform ('over_mastery' or 'aetherial_mastery').
|
||||||
|
# @return [void]
|
||||||
def check_value(property, type)
|
def check_value(property, type)
|
||||||
# Input format
|
# Input format
|
||||||
# { ring1: { atk: 300 } }
|
# { ring1: { atk: 300 } }
|
||||||
|
|
@ -135,6 +259,10 @@ class GridCharacter < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns a hash mapping over mastery modifier keys to their corresponding attribute names.
|
||||||
|
#
|
||||||
|
# @return [Hash{Integer => String}] mapping of modifier codes to attribute names.
|
||||||
def over_mastery_modifiers
|
def over_mastery_modifiers
|
||||||
{
|
{
|
||||||
1 => 'atk',
|
1 => 'atk',
|
||||||
|
|
@ -155,6 +283,10 @@ class GridCharacter < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns a hash containing allowed values for over mastery attributes.
|
||||||
|
#
|
||||||
|
# @return [Hash{Symbol => Array<Integer>}] mapping of attribute names to their valid values.
|
||||||
def over_mastery_values
|
def over_mastery_values
|
||||||
{
|
{
|
||||||
atk: [300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000],
|
atk: [300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000],
|
||||||
|
|
@ -175,6 +307,9 @@ class GridCharacter < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns a hash mapping aetherial mastery modifier keys to their corresponding attribute names.
|
||||||
|
#
|
||||||
|
# @return [Hash{Integer => String}] mapping of aetherial mastery modifier codes to attribute names.
|
||||||
def aetherial_mastery_modifiers
|
def aetherial_mastery_modifiers
|
||||||
{
|
{
|
||||||
1 => 'da',
|
1 => 'da',
|
||||||
|
|
@ -190,6 +325,10 @@ class GridCharacter < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns a hash containing allowed values for aetherial mastery attributes.
|
||||||
|
#
|
||||||
|
# @return [Hash{Symbol => Hash{Symbol => Integer}}] mapping of attribute names to their minimum and maximum values.
|
||||||
def aetherial_mastery_values
|
def aetherial_mastery_values
|
||||||
{
|
{
|
||||||
da: {
|
da: {
|
||||||
|
|
@ -235,10 +374,18 @@ class GridCharacter < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns an array of valid attack values for over mastery validation.
|
||||||
|
#
|
||||||
|
# @return [Array<Integer>] list of allowed attack values.
|
||||||
def atk_values
|
def atk_values
|
||||||
[300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000]
|
[300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns an array of valid HP values for over mastery validation.
|
||||||
|
#
|
||||||
|
# @return [Array<Integer>] list of allowed HP values.
|
||||||
def hp_values
|
def hp_values
|
||||||
[150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500]
|
[150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
20
spec/factories/grid_characters.rb
Normal file
20
spec/factories/grid_characters.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :grid_character do
|
||||||
|
association :party
|
||||||
|
# Use the canonical (seeded) Character record.
|
||||||
|
# Make sure your CSV canonical data (loaded via canonical.rb) includes a Character with the specified granblue_id.
|
||||||
|
character { Character.find_by!(granblue_id: '3040087000') }
|
||||||
|
position { 0 }
|
||||||
|
uncap_level { 3 }
|
||||||
|
transcendence_step { 0 }
|
||||||
|
# Virtual attributes default to nil.
|
||||||
|
new_rings { nil }
|
||||||
|
new_awakening { nil }
|
||||||
|
# JSON columns for ring data are set to default hashes.
|
||||||
|
ring1 { { 'modifier' => nil, 'strength' => nil } }
|
||||||
|
ring2 { { 'modifier' => nil, 'strength' => nil } }
|
||||||
|
ring3 { { 'modifier' => nil, 'strength' => nil } }
|
||||||
|
ring4 { { 'modifier' => nil, 'strength' => nil } }
|
||||||
|
earring { { 'modifier' => nil, 'strength' => nil } }
|
||||||
|
end
|
||||||
|
end
|
||||||
166
spec/models/grid_characters_spec.rb
Normal file
166
spec/models/grid_characters_spec.rb
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
# 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 }
|
||||||
|
|
||||||
|
# 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
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue