lib/chop/form.rb
require "active_support/core_ext/object/blank"
require "active_support/core_ext/class/subclasses"
module Chop
class Form < Struct.new(:table, :session, :path)
def self.fill_in! table, session: Capybara.current_session, path: "features/support/fixtures"
new(table, session, path).fill_in!
end
def self.diff! selector, table, session: Capybara.current_session, &block
root = begin
if selector.is_a?(Capybara::Node::Element)
selector
else
session.find(selector)
end
rescue Capybara::ElementNotFound
raise unless @allow_not_found
Node("")
end
all_fields = root.all("input, textarea, select")
relevant_fields = all_fields.inject([]) do |fields, field|
next fields if field[:name].blank?
next fields if field[:type] == "submit"
next fields if field[:type] == "hidden"
fields + [field]
end
deduplicated_fields = relevant_fields.inject([]) do |fields, field|
next fields if fields.map { |field| field[:name] }.include?(field[:name])
fields + [field]
end
actual = deduplicated_fields.inject([]) do |fields, field|
next fields unless label = find_label_for(field)
field = Field.from(session, field)
fields + [[label.text(:all), field.get_value.to_s]]
end
table.diff! actual, surplus_row: false, misplaced_col: false
end
def self.find_label_for field, session: Capybara.current_session
if field[:id].present?
session.first("label[for='#{field[:id]}']", visible: :all, minimum: 0, wait: 0.1)
else
puts "cannot find label without id for #{field[:name]}"
end
end
def fill_in!
table.rows_hash.each do |label, value|
Field.for(session, label, value, path).fill_in!
end
end
class Field < Struct.new(:session, :label, :value, :path, :field)
def self.for session, label, value, path
field = session.find_field(label)
candidates.map do |klass|
klass.new(session, label, value, path, field)
end.find(&:matches?)
end
def self.from session, field
candidates.map do |klass|
klass.new(session, nil, nil, nil, field)
end.find(&:matches?)
end
def self.candidates
descendants.sort_by do |a|
a == Chop::Form::Default ? 1 : -1 # ensure Default comes last
end
end
def get_value
field.value
end
end
class MultipleSelect < Field
def matches?
field.tag_name == "select" && field[:multiple]
end
def fill_in!
field.all("option").map(&:text).each do |value|
session.unselect value, from: label
end
value.split(", ").each do |value|
session.select value, from: label
end
end
end
class Select < Field
def matches?
field.tag_name == "select" && !field[:multiple]
end
def fill_in!
session.select value, from: label
end
end
class MultipleCheckbox < Field
def matches?
field[:type] == "checkbox" && field[:name].to_s.end_with?("[]")
end
def fill_in!
checkboxes.each do |checkbox|
checkbox.set checkbox_label_in_values?(checkbox)
end
end
def get_value
checkboxes.select(&:checked?).map(&:value).join(", ")
end
private
def checkboxes
session.all("[name='#{field[:name]}']")
end
def checkbox_label_in_values? checkbox
values = value.split(", ")
labels = session.all("label[for='#{checkbox[:id]}']").map(&:text)
(values & labels).any?
end
end
class Checkbox < Field
def matches?
field[:type] == "checkbox" && !field[:name].to_s.end_with?("[]")
end
def fill_in!
field.set value.present?
end
def get_value
field.checked? ? "✓" : ""
end
end
class Radio < Field
def matches?
field[:type] == "radio"
end
def fill_in!
if nonstandard_labelling?
value_field.click
else
session.choose label
end
end
def get_value
session.all("[name='#{field[:name]}']").find(&:checked?).try(:value)
end
private
def nonstandard_labelling?
value_field[:name] == field[:name]
end
def value_field
session.all(:field, value).select { |el| el[:name] == field[:name] }.last || {}
end
end
class SingleFile < Field
def matches?
field[:type] == "file" && !field[:multiple]
end
def fill_in!
assert_single_file!
field.set file_path
end
private
def file_path
return nil unless value.present?
::File.expand_path(::File.join(path, value)).tap do |path|
::File.open(path){} # raise Errno::ENOENT if file doesn't exist
end
end
def assert_single_file!
if value.include?(" ")
raise TypeError.new("Cannot attach multiple files to file field '#{label}' without the multiple attribute present")
end
end
end
class MultipleFile < Field
def matches?
field[:type] == "file" && field[:multiple]
end
def fill_in!
field.set file_paths
end
private
def file_paths
value.split(" ").map do |filename|
next nil unless filename.present?
::File.expand_path(::File.join(path, filename)).tap do |path|
::File.open(path){} # raise Errno::ENOENT if file doesn't exist
end
end.compact
end
end
class ViaJavascript < Field
def matches?
%w[time week month range].include?(field[:type])
end
def fill_in!
session.execute_script("document.getElementById('#{field[:id]}').value = '#{value}'")
end
end
class Default < Field
def matches?
true
end
def fill_in!
field.set value
end
end
end
end