forked from fixin.me/fixin.me
Test Unit using float multiplier
Improve string randomization. Use Capybara table selectors.
This commit is contained in:
@@ -4,6 +4,9 @@ require 'core_ext/range'
|
|||||||
|
|
||||||
ActiveSupport.on_load :action_dispatch_system_test_case do
|
ActiveSupport.on_load :action_dispatch_system_test_case do
|
||||||
prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId
|
prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId
|
||||||
|
# Allow denormalized string comparison,
|
||||||
|
# https://github.com/orgs/teamcapybara/discussions/2841
|
||||||
|
XPath::DSL.prepend CoreExt::XPath::DSL::NormalizeSpaceDefault
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSupport.on_load :action_view do
|
ActiveSupport.on_load :action_view do
|
||||||
|
|||||||
@@ -41,6 +41,24 @@ en:
|
|||||||
descendant_reference: cannot be changed to any of descendants
|
descendant_reference: cannot be changed to any of descendants
|
||||||
self_reference: of the quantitiy cannot be the quantity itself
|
self_reference: of the quantitiy cannot be the quantity itself
|
||||||
user_mismatch: has to belong to the same user as quantity
|
user_mismatch: has to belong to the same user as quantity
|
||||||
|
activesupport:
|
||||||
|
# Based on: https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-misc-full/main/en/characters.json
|
||||||
|
characters:
|
||||||
|
subsets:
|
||||||
|
# :exemplarCharacters[EN]
|
||||||
|
exemplar: abcdefghijklmnopqrstuvwxyz
|
||||||
|
# :index[EN]
|
||||||
|
index: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
numbers: 0123456789
|
||||||
|
# :exemplarCharacters[DE, PL] - exemplarCharacters[EN] +
|
||||||
|
# :index[DE, PL] - :index[EN]
|
||||||
|
auxiliary: äößüĆŁÓŚŹŻąćęłńóśźż
|
||||||
|
# :punctuation* + :numbers* - space - numbers
|
||||||
|
punctuation: >-
|
||||||
|
!\"#%&'()*+,-./:;?@[]{|}~§«°»×‐‑–—‘’‚“”„†‡…‰′″−
|
||||||
|
# :exemplarCharacters[EL] + :index[EL]
|
||||||
|
symbols: αάβγδεέζηήθιίϊΐκλμνξοόπρσςτυύϋΰφχψωώΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
|
||||||
|
space: ' '
|
||||||
actioncontroller:
|
actioncontroller:
|
||||||
exceptions:
|
exceptions:
|
||||||
status:
|
status:
|
||||||
|
|||||||
8
lib/core_ext/x_path/d_s_l/normalize_space_default.rb
Normal file
8
lib/core_ext/x_path/d_s_l/normalize_space_default.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module CoreExt::XPath::DSL::NormalizeSpaceDefault
|
||||||
|
def normalize_space(...)
|
||||||
|
Capybara.default_normalize_ws ? super : current
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :n, :normalize_space
|
||||||
|
alias_method :normalize, :normalize_space
|
||||||
|
end
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||||
include ActionView::Helpers::SanitizeHelper
|
|
||||||
include ActionView::Helpers::UrlHelper
|
include ActionView::Helpers::UrlHelper
|
||||||
|
|
||||||
# NOTE: geckodriver installed with Firefox, ignore incompatibility warning
|
# NOTE: geckodriver installed with Firefox, ignore incompatibility warning
|
||||||
@@ -33,14 +32,6 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|||||||
button.html_safe)
|
button.html_safe)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Allow skipping interpolations when translating for testing purposes
|
|
||||||
INTERPOLATION_PATTERNS = Regexp.union(I18n.config.interpolation_patterns)
|
|
||||||
def translate(key, **options)
|
|
||||||
translation = options.empty? ? super.split(INTERPOLATION_PATTERNS, 2).first : super
|
|
||||||
sanitize(translation, tags: [])
|
|
||||||
end
|
|
||||||
alias :t :translate
|
|
||||||
|
|
||||||
DB_CONFIGS = ActiveRecord::Base.configurations.configs_for(env_name: "test")
|
DB_CONFIGS = ActiveRecord::Base.configurations.configs_for(env_name: "test")
|
||||||
TEST_CONFIGS = Hash.new(DB_CONFIGS.first.name.to_sym)
|
TEST_CONFIGS = Hash.new(DB_CONFIGS.first.name.to_sym)
|
||||||
class << self
|
class << self
|
||||||
@@ -73,5 +64,43 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This regex has to be only strict enough to properly reconstruct number. It
|
||||||
|
# is not used for format validation.
|
||||||
|
NUMBER = /\A(
|
||||||
|
(?<base>\-?\d[\d\.]*)
|
||||||
|
(×(?<pow>10(?<ws>\s)?(?<exp>(?(<-2>)()|(\-))[\d]+)))?
|
||||||
|
|
|
||||||
|
\g<pow>
|
||||||
|
)\z/x
|
||||||
|
# Finds table cell in current row corresponding to column name given in locator.
|
||||||
|
# Based on Capybara :table and :table_row selectors.
|
||||||
|
Capybara.add_selector(:table_cell, locator_type: [String, Symbol]) do
|
||||||
|
xpath do |locator|
|
||||||
|
column = XPath.string.n.is(locator.to_s)
|
||||||
|
header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[column]
|
||||||
|
position_xp = XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
|
||||||
|
XPath.descendant(:td).where(header_xp.boolean & position_xp)
|
||||||
|
end
|
||||||
|
|
||||||
|
node_filter(:with_value) do |node, expected|
|
||||||
|
value =
|
||||||
|
case expected
|
||||||
|
when Float
|
||||||
|
node.text.match(NUMBER) { |m| "#{m['base'] || '1'}e#{m['exp']&.lstrip}".to_f }
|
||||||
|
else
|
||||||
|
node.text
|
||||||
|
end
|
||||||
|
(expected == value).tap do |result|
|
||||||
|
add_error("Expected value to be #{expected.inspect}" \
|
||||||
|
" but was #{value.inspect}") unless result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.modify_selector(:table_row) do
|
||||||
|
# Beware: filters of #boolean? type are called within different context.
|
||||||
|
node_filter(:with_focus, :boolean) do |node, value|
|
||||||
|
node.has_selector?(':focus') == value
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ class UnitsTest < ApplicationSystemTestCase
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_symbols
|
||||||
|
all(:table_cell, column_title(:symbol)).map(&:text)
|
||||||
|
end
|
||||||
|
|
||||||
test "index" do
|
test "index" do
|
||||||
sign_in
|
sign_in
|
||||||
# Wait for the table to appear first, only then check row count.
|
# Wait for the table to appear first, only then check row count.
|
||||||
@@ -39,6 +43,8 @@ class UnitsTest < ApplicationSystemTestCase
|
|||||||
|
|
||||||
test "new and create" do
|
test "new and create" do
|
||||||
sign_in
|
sign_in
|
||||||
|
symbols = list_symbols
|
||||||
|
|
||||||
link_labels.slice!(:new_unit, :new_subunit)
|
link_labels.slice!(:new_unit, :new_subunit)
|
||||||
type, label = link_labels.to_a.sample
|
type, label = link_labels.to_a.sample
|
||||||
all(:link, exact_text: label).sample.then do |link|
|
all(:link, exact_text: label).sample.then do |link|
|
||||||
@@ -46,38 +52,50 @@ class UnitsTest < ApplicationSystemTestCase
|
|||||||
link.assert_matches_selector :link, disabled: true
|
link.assert_matches_selector :link, disabled: true
|
||||||
end
|
end
|
||||||
|
|
||||||
values = nil
|
attributes = [:symbol, :description]
|
||||||
within 'tbody > tr:has(input[type=text], textarea)' do
|
attributes << :multiplier if type == :new_subunit
|
||||||
assert_selector ':focus'
|
within :table_row, {}, with_focus: true do
|
||||||
|
attributes.map! do |name|
|
||||||
maxlength = all(:fillable_field).to_h do |field|
|
field = within(:table_cell, column_title(name)) { find(:fillable_field) }
|
||||||
[field[:name], field[:maxlength].to_i || 2**16]
|
value = case name
|
||||||
|
when :symbol
|
||||||
|
random_string(1..3, 4..field[:maxlength].to_i,
|
||||||
|
allow_blank: false, except: symbols)
|
||||||
|
when :description
|
||||||
|
random_string(0, 1..field[:maxlength].to_i)
|
||||||
|
when :multiplier
|
||||||
|
if rand < 0.5
|
||||||
|
random_float(min: field[:min].to_f, max: field[:max].to_f)
|
||||||
|
else
|
||||||
|
10.0**rand(-15..15)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
field.fill_in with: value
|
||||||
|
[name, value]
|
||||||
end
|
end
|
||||||
values = {
|
|
||||||
symbol: random_string(deep_rand(1..3, 4..maxlength['unit[symbol]']),
|
|
||||||
except: @user.units.map(&:symbol), allow_blank: false),
|
|
||||||
description: random_string(rand(0..maxlength['unit[description]']))
|
|
||||||
}.with_indifferent_access
|
|
||||||
within :field, 'unit[multiplier]' do |field|
|
|
||||||
values[:multiplier] = random_number(field[:max], field[:step])
|
|
||||||
end if type == :new_subunit
|
|
||||||
|
|
||||||
values.each_pair { |name, value| fill_in "unit[#{name}]", with: value }
|
|
||||||
|
|
||||||
assert_difference ->{ Unit.count }, 1 do
|
|
||||||
click_on t('helpers.submit.create')
|
click_on t('helpers.submit.create')
|
||||||
end
|
end
|
||||||
end
|
attributes = attributes.to_h
|
||||||
|
|
||||||
assert_selector '.flash.notice',
|
assert_selector '.flash.notice', exact_text: t('units.create.success',
|
||||||
text: t('units.create.success', unit: Unit.last.symbol)
|
unit: attributes[:symbol])
|
||||||
within 'tbody' do
|
new_symbols = list_symbols
|
||||||
|
|
||||||
|
within :table do
|
||||||
assert_no_selector :fillable_field
|
assert_no_selector :fillable_field
|
||||||
assert_selector 'tr', count: @user.units.count
|
assert_equal symbols.length + 1, new_symbols.length
|
||||||
|
|
||||||
|
m = attributes.delete(:multiplier).to_f if type == :new_subunit
|
||||||
|
within :table_row, attributes.transform_keys { |k| column_title(k) } do
|
||||||
|
assert_selector :table_cell, column_title(:multiplier), with_value: m if m
|
||||||
|
end
|
||||||
end
|
end
|
||||||
assert_no_selector :link, disabled: true,
|
assert_no_selector :link, disabled: true,
|
||||||
exact_text: Regexp.union(link_labels.values)
|
exact_text: Regexp.union(link_labels.values)
|
||||||
assert_equal values, Unit.last.attributes.slice(*values.keys)
|
|
||||||
|
refresh
|
||||||
|
assert_equal new_symbols, list_symbols
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create updates view in order" do
|
test "create updates view in order" do
|
||||||
|
|||||||
@@ -7,92 +7,46 @@ class ActiveSupport::TestCase
|
|||||||
fixtures :all
|
fixtures :all
|
||||||
|
|
||||||
include ActionMailer::TestHelper
|
include ActionMailer::TestHelper
|
||||||
|
include ActionView::Helpers::SanitizeHelper
|
||||||
include ActionView::Helpers::TranslationHelper
|
include ActionView::Helpers::TranslationHelper
|
||||||
|
|
||||||
# List of categorized Unicode characters:
|
# Allow skipping interpolations when translating for testing purposes
|
||||||
# * source: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
INTERPOLATION_PATTERNS = Regexp.union(I18n.config.interpolation_patterns)
|
||||||
# * file format: http://www.unicode.org/L2/L1999/UnicodeData.html
|
def translate(key, **options)
|
||||||
# * select from graphic ranges: L, M, N, P, S, Zs
|
translation = options.empty? ? super.split(INTERPOLATION_PATTERNS, 2).first : super
|
||||||
UNICODE_CHARS = [
|
sanitize(translation, tags: [])
|
||||||
*"\u0020".."\u007E",
|
end
|
||||||
*"\u00A0".."\u00AC",
|
alias :t :translate
|
||||||
*"\u00AE".."\u05FF",
|
|
||||||
*"\u0606".."\u061B",
|
CHARACTER_SET = {exemplar: 5, index: 1, numbers: 3, auxiliary: 1,
|
||||||
*"\u061D".."\u06DC",
|
punctuation: 1/16r, symbols: 1/16r, space: 1/6r}
|
||||||
*"\u06DE".."\u070E",
|
# Initialize after setting Minitest.seed to ensure reproducibility.
|
||||||
*"\u0710".."\u07FF"
|
def character_set
|
||||||
]
|
@@character_set ||=
|
||||||
def random_string(length, except: [], allow_blank: true)
|
CHARACTER_SET.reduce([]) do |chars, (subset, factor)|
|
||||||
# Rails String#blank? is based on [[:space:]] character class (String::BLANK_RE).
|
s = t("activesupport.characters.subsets.#{subset}").chars
|
||||||
# Here we use more general class including tabs, cariage returns and newlines.
|
if factor.is_a?(Rational)
|
||||||
except << /^\p{Space}+$/ unless allow_blank
|
elements = factor * chars.length
|
||||||
|
s *= (elements / s.length).ceil
|
||||||
|
chars + s.sample(elements)
|
||||||
|
else
|
||||||
|
chars + s * factor
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_string(*length_ranges, allow_blank: true, except: [])
|
||||||
|
length = deep_rand(length_ranges)
|
||||||
|
raise ArgumentError, "Allow blanks for 0 length" if length == 0 && !allow_blank
|
||||||
begin
|
begin
|
||||||
result = UNICODE_CHARS.sample(length).join
|
result = character_set.sample(length).join
|
||||||
|
# Rails String#blank? is based on [[:space:]] character class (String::BLANK_RE).
|
||||||
|
# Here we use more general class including tabs, carriage returns and newlines.
|
||||||
|
redo if !allow_blank && /^\p{Space}+$/ === result
|
||||||
end while except.any? { |e| e === result }
|
end while except.any? { |e| e === result }
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
# Range based string generation. Not finished, but saved as a potential
|
|
||||||
# reference for future work. To work properly it needs to sort in collation
|
|
||||||
# order of database.
|
|
||||||
#def random_between(from, to, maxlength)
|
|
||||||
# # TODO: sort UNICODE_CHARS
|
|
||||||
# byebug
|
|
||||||
# result = ''
|
|
||||||
# maxlength.times do |i|
|
|
||||||
# case
|
|
||||||
# when from[i] == to[i]
|
|
||||||
# result += from[i]
|
|
||||||
# else
|
|
||||||
# from_index = UNICODE_CHARS.bsearch_index(from[i] || UNICODE_CHARS[0])
|
|
||||||
# to_index = UNICODE_CHARS.bsearch_index(to[i])
|
|
||||||
# index = rand(from_index..to_index)
|
|
||||||
# case
|
|
||||||
# when index == from_index
|
|
||||||
# result += UNICODE_CHARS[index]
|
|
||||||
# from[i+1..].each_char do |c|
|
|
||||||
# from_index = UNICODE_CHARS.bsearch_index(from[i])
|
|
||||||
# index = rand(from_index..UNICODE_CHARS.length-1)
|
|
||||||
# result += UNICODE_CHARS[index]
|
|
||||||
# break if index != from_index
|
|
||||||
# end
|
|
||||||
# if result == from
|
|
||||||
# if result.length < maxlength
|
|
||||||
# result += UNICODE_CHARS.sample
|
|
||||||
# else
|
|
||||||
# # TODO: succ result[i..]
|
|
||||||
# # raise if result == to
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# break
|
|
||||||
# when index == to_index
|
|
||||||
# result += UNICODE_CHARS[index]
|
|
||||||
# to[i+1..].each_char do |c|
|
|
||||||
# to_index = UNICODE_CHARS.bsearch_index(to[i])
|
|
||||||
# index = rand(-1..to_index)
|
|
||||||
# break if index == -1
|
|
||||||
# result += UNICODE_CHARS[index]
|
|
||||||
# break if index != to_index
|
|
||||||
# end
|
|
||||||
# if result == to
|
|
||||||
# if result.length > i+1
|
|
||||||
# result = result[..-2]
|
|
||||||
# else
|
|
||||||
# # TODO: prev result[i..]
|
|
||||||
# # raise if result == from
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# break
|
|
||||||
# else
|
|
||||||
# result += UNICODE_CHARS[index]
|
|
||||||
# break
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# return result += UNICODE_CHARS.sample(rand(0..maxlength-i-1)).join
|
|
||||||
# # if result == from/to ...
|
|
||||||
#end
|
|
||||||
|
|
||||||
def deep_rand(*args)
|
def deep_rand(*args)
|
||||||
case args
|
case args
|
||||||
when Array
|
when Array
|
||||||
@@ -104,16 +58,45 @@ class ActiveSupport::TestCase
|
|||||||
end while true
|
end while true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Assumes: max >= step and step = 1e[-]N, both as strings
|
# Return string representation of number selected randomly within given limits.
|
||||||
def random_number(max, step)
|
# Distrbiution is roughly uniform first with regard to: sign and exponent, then
|
||||||
max.delete!('.')
|
# significand, with some extra skew towards most expected values and boundaries.
|
||||||
precision = max.length
|
# Variants:
|
||||||
start = rand(precision) + 1
|
# min max exp_range range (magnitude)
|
||||||
d = (rand(max.to_i) + 1) % 10**start
|
# - + MIN_EXP..min/max 0.5..min/max if min/max.exp
|
||||||
length = rand([0, 1..4, 4..precision].sample)
|
# - - max..min 0.5..min if min.exp, max...1.0 if max.exp
|
||||||
d = d.truncate(-start + length)
|
# - 0 MIN_EXP..min 0.5..min if min.exp
|
||||||
d = 10**(start - length) if d.zero?
|
# 0 + MIN_EXP..max 0.5..max if max.exp
|
||||||
BigDecimal(step) * d
|
# + + min..max 0.5..max if max.exp, min...1.0 if min.exp
|
||||||
|
# TODO: alternate between floating-point and exponential representation.
|
||||||
|
def random_float(min: -Float::MAX_15, max: Float::MAX_15, except: [])
|
||||||
|
# For now assume ranges are same on both sides of 0.
|
||||||
|
assert_equal -min, max if min.negative? && max.positive?
|
||||||
|
|
||||||
|
include_zero = (min.negative? && max.positive?) || min.zero? || max.zero?
|
||||||
|
precision_range = [1..2, 3..4, 5..Float::DIG-1, Float::DIG]
|
||||||
|
# Returns 0.0 <=> (precision == 0).
|
||||||
|
precision_range << 0 if include_zero
|
||||||
|
|
||||||
|
min_frexp, max_frexp = [min, max].map(&:abs).minmax.map { |f| Math.frexp(f) }
|
||||||
|
fractions, exponents = min_frexp.zip(max_frexp)
|
||||||
|
exponents[0] = Float::MIN_EXP if include_zero
|
||||||
|
exp_range = Range.new(*exponents)
|
||||||
|
exp_range &= [Float::MIN_EXP..-10, -9..-4, -3..0, 1..4, 5..10, 11..Float::MAX_EXP]
|
||||||
|
begin
|
||||||
|
precision = deep_rand(*precision_range)
|
||||||
|
exp = deep_rand(*exp_range)
|
||||||
|
|
||||||
|
range = [0.5, 1.0, true]
|
||||||
|
range = [range[0], fractions[1], false] if exp == exponents[1]
|
||||||
|
range = [fractions[0], range[1], range[2]] if exp == exponents[0] && !include_zero
|
||||||
|
|
||||||
|
fr = rand(Range.new(*range))
|
||||||
|
number = Math.ldexp(fr, exp)
|
||||||
|
number *= [min.negative? ? -1.0 : 1.0, max.positive? ? 1.0 : -1.0].sample
|
||||||
|
number = number.limit(precision).clamp(min, max)
|
||||||
|
end while except.any? { |e| e === number }
|
||||||
|
number.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def randomize_user_password!(user)
|
def randomize_user_password!(user)
|
||||||
@@ -131,4 +114,9 @@ class ActiveSupport::TestCase
|
|||||||
def with_last_email
|
def with_last_email
|
||||||
yield(ActionMailer::Base.deliveries.last)
|
yield(ActionMailer::Base.deliveries.last)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def column_title(attribute)
|
||||||
|
model = self.class.name.delete_suffix('Test').singularize.constantize
|
||||||
|
model.human_attribute_name(attribute)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user