chop 0.33.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14c0cb6f9aa8014bb534f6ba37a1c349a350b6cc46965a6ff53396e5d3109392
4
- data.tar.gz: c30c721f89e926e8863f9922be49b56637857da63dbc30bc9bbd11b4a6434d73
3
+ metadata.gz: 97c2fe11438b99e52e5f3165ec8164eb7447c1a6238dffd4855d5a7f748b20af
4
+ data.tar.gz: 7e14858a804e3f58a44effecc1c5d92b24b79c59db4186c20625134f5c30c731
5
5
  SHA512:
6
- metadata.gz: 304b8d1f6476118b823cde46c7c879e46b22a1d7841fbddbe557e4990aea2fb90a8e1d90375eb80f93fd7fb32b892881842aecaf817dd93962d6349a2adabf8e
7
- data.tar.gz: 1ee77efff5587d689ca58d81452807cb0a2c8f0b8dde2b563ba4f36ea7f336b3aabc12c74fbf9d7efb70139b91e8b01a05a5f545ef8918d05c9bd42020ed5605
6
+ metadata.gz: edf192cddad2d07ebe2e8db22f0ab193600aa8d55c47e7450fc307e64cf994fafd54a899aaa8221e0c228172957e0d4ed14bb509c5493e7dba7c1358ccce78fa
7
+ data.tar.gz: c473cc071556c3b3142cf7027fb4015c1dbd2d4408c26d971e84917493bed80871cfd8641d222759cc9a9a2c2e430d8fb1cebefe8c8da4b184a3a67b913d80a0
data/CLAUDE.md ADDED
@@ -0,0 +1,61 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Chop is a Ruby gem that enhances Cucumber tables with three main methods: `#create!` for creating entities (ActiveRecord/FactoryGirl support), `#diff!` for comparing tables against HTML elements (table, dl, ul, form), and `#fill_in!` for form filling. It works by monkey-patching Cucumber's DataTable class.
8
+
9
+ ## Development Commands
10
+
11
+ ### Testing
12
+ - `rake spec` or `bundle exec rspec` - Run the full test suite
13
+ - `rspec spec/chop/table_spec.rb` - Run a specific test file
14
+ - `rspec spec/chop/table_spec.rb:42` - Run a specific test by line number
15
+
16
+ ### Development Setup
17
+ - `bin/setup` - Install dependencies after checkout
18
+ - `bin/console` - Interactive console for experimentation
19
+
20
+ ### Multi-Version Testing
21
+ - `bundle exec appraisal install` - Install gems for all Rails versions
22
+ - `bundle exec appraisal rails-7.0 rspec` - Test against specific Rails version
23
+ - `bundle exec appraisal rspec` - Test against all Rails versions
24
+
25
+ ### Gem Management
26
+ - `rake build` - Build the gem
27
+ - `rake release` - Release new version (updates version, tags, pushes)
28
+
29
+ ## Architecture
30
+
31
+ ### Core Components
32
+ - **`Chop::DSL`** (lib/chop/dsl.rb) - Main interface providing `create!`, `diff!`, `fill_in!` methods
33
+ - **`Chop::Create`** (lib/chop/create.rb) - Handles entity creation with transformation DSL
34
+ - **`Chop::Diff`** (lib/chop/diff.rb) - Base class for HTML element diffing with Capybara
35
+ - **Element-specific diffing classes**:
36
+ - `Chop::Table` - HTML table diffing
37
+ - `Chop::DefinitionList` - Definition list (`<dl>`) diffing
38
+ - `Chop::UnorderedList` - Unordered list (`<ul>`) diffing
39
+ - `Chop::Form` - Form filling functionality
40
+
41
+ ### Monkey-patching Strategy
42
+ The gem extends `Cucumber::MultilineArgument::DataTable` by prepending a module that adds the three main methods. The DSL module can also be called directly as `Chop.create!`, `Chop.diff!`, `Chop.fill_in!` to avoid monkey-patching.
43
+
44
+ ### Creation Strategies
45
+ Supports pluggable creation strategies via `Chop::Create.register_creation_strategy`:
46
+ - Default: ActiveRecord's `create!`
47
+ - FactoryGirl/FactoryBot support built-in
48
+ - Custom strategies can be registered
49
+
50
+ ### Transformation DSL
51
+ The `#create!` method supports a rich DSL for transforming table data:
52
+ - Field transformations: `file`, `files`, `has_one`, `has_many`, `default`, `copy`, `rename`, `delete`
53
+ - Low-level: `field`, `transformation` for custom logic
54
+ - Lifecycle hooks: `after` for post-creation logic
55
+
56
+ ## Testing Patterns
57
+
58
+ - Uses RSpec with `spec_helper.rb` configuring focus and run-all behavior
59
+ - Tests organized by component in `spec/chop/` matching `lib/chop/` structure
60
+ - Tests cover both direct method calls and monkey-patched table behavior
61
+ - Uses Capybara for integration testing of diffing functionality
data/lib/chop/create.rb CHANGED
@@ -157,7 +157,8 @@ module Chop
157
157
  end
158
158
  end
159
159
 
160
- def has_many key, klass=nil, delimiter: ", ", field: :name
160
+ def has_many key, klass=nil, delimiter: ", ", field: :name, find_by: nil
161
+ field = find_by if find_by
161
162
  klass ||= key.to_s.classify.constantize
162
163
  self.field key do |names|
163
164
  names.split(delimiter).map do |name|
@@ -166,7 +167,8 @@ module Chop
166
167
  end
167
168
  end
168
169
 
169
- def has_one key, klass=nil, field: :name
170
+ def has_one key, klass=nil, field: :name, find_by: nil
171
+ field = find_by if find_by
170
172
  klass ||= key.to_s.classify.constantize
171
173
  self.field key do |name|
172
174
  klass.find_by!(field => name) if name.present?
data/lib/chop/form.rb CHANGED
@@ -19,33 +19,16 @@ module Chop
19
19
  Node("")
20
20
  end
21
21
 
22
- all_fields = root.all("input, textarea, select")
23
- relevant_fields = all_fields.inject([]) do |fields, field|
24
- next fields if field[:name].blank?
25
- next fields if field[:type] == "submit"
26
- next fields if field[:type] == "hidden"
27
- fields + [field]
28
- end
29
- deduplicated_fields = relevant_fields.inject([]) do |fields, field|
30
- next fields if fields.map { |field| field[:name] }.include?(field[:name])
31
- fields + [field]
32
- end
33
- actual = deduplicated_fields.inject([]) do |fields, field|
34
- next fields unless label = find_label_for(field)
35
- field = Field.from(session, field)
36
- fields + [[label.text(:all), field.get_value.to_s]]
37
- end
22
+ actual = root.all(Field.combined_css_selector)
23
+ .filter_map { |field_element| Field.from(session, field_element) }
24
+ .select(&:should_include_in_diff?)
25
+ .uniq { |field| field.field[:name] }
26
+ .filter_map(&:to_diff_row)
27
+
38
28
  block.call(actual, root) if block_given?
39
29
  table.diff! actual, surplus_row: false, misplaced_col: false
40
30
  end
41
31
 
42
- def self.find_label_for field, session: Capybara.current_session
43
- if field[:id].present?
44
- session.first("label[for='#{field[:id]}']", visible: :all, minimum: 0, wait: 0.1)
45
- else
46
- puts "cannot find label without id for #{field[:name]}"
47
- end
48
- end
49
32
 
50
33
  def fill_in!
51
34
  table.rows_hash.each do |label, value|
@@ -53,9 +36,63 @@ module Chop
53
36
  end
54
37
  end
55
38
 
39
+ class FieldFinder
40
+ def initialize(session, css_selector)
41
+ @session = session
42
+ @css_selector = css_selector
43
+ end
44
+
45
+ def find(locator)
46
+ return nil if locator.nil?
47
+
48
+ @locator = locator.to_s
49
+ @all_fields = @session.all(@css_selector)
50
+
51
+ find_by_direct_attributes ||
52
+ find_by_aria_label ||
53
+ find_by_associated_label ||
54
+ find_by_wrapping_label ||
55
+ raise_not_found
56
+ end
57
+
58
+ private
59
+
60
+ def find_by_direct_attributes
61
+ @all_fields.find do |field|
62
+ field[:id] == @locator ||
63
+ field[:name] == @locator ||
64
+ field[:placeholder] == @locator
65
+ end
66
+ end
67
+
68
+ def find_by_aria_label
69
+ @all_fields.find { |field| field[:'aria-label'] == @locator }
70
+ end
71
+
72
+ def find_by_associated_label
73
+ @all_fields.find do |field|
74
+ field[:id].present? &&
75
+ @session.first("label[for='#{field[:id]}']", visible: :all, minimum: 0, wait: 0.1)&.text(:all) == @locator
76
+ end
77
+ end
78
+
79
+ def find_by_wrapping_label
80
+ wrapping_label = @session.all("label", text: @locator, visible: :all, minimum: 0, wait: 0.1).find do |label|
81
+ label.find(@css_selector, visible: :all, minimum: 0, wait: 0.1)
82
+ rescue Capybara::ElementNotFound
83
+ false
84
+ end
85
+ wrapping_label&.find(@css_selector, visible: :all, minimum: 0, wait: 0.1)
86
+ end
87
+
88
+ def raise_not_found
89
+ raise Capybara::ElementNotFound, "Unable to find field #{@locator.inspect}"
90
+ end
91
+ end
92
+
56
93
  class Field < Struct.new(:session, :label, :value, :path, :field)
57
94
  def self.for session, label, value, path
58
- field = session.find_field(label)
95
+ field = FieldFinder.new(session, combined_css_selector).find(label)
59
96
  candidates.map do |klass|
60
97
  klass.new(session, label, value, path, field)
61
98
  end.find(&:matches?)
@@ -73,12 +110,49 @@ module Chop
73
110
  end
74
111
  end
75
112
 
113
+ def self.css_selector
114
+ "input"
115
+ end
116
+
117
+ def self.combined_css_selector
118
+ candidates.map(&:css_selector).uniq.join(", ")
119
+ end
120
+
76
121
  def get_value
77
122
  field.value
78
123
  end
124
+
125
+ def should_include_in_diff?
126
+ field[:name].present? &&
127
+ field[:type] != "submit" &&
128
+ field[:type] != "hidden"
129
+ end
130
+
131
+ def label_text
132
+ return nil unless field[:id].present?
133
+ label_element = session.first("label[for='#{field[:id]}']", visible: :all, minimum: 0, wait: 0.1)
134
+ label_element&.text(:all)
135
+ end
136
+
137
+ def to_diff_row
138
+ return nil unless label = label_text
139
+ [label, diff_value]
140
+ end
141
+
142
+ def diff_value
143
+ get_value.to_s
144
+ end
145
+
146
+ def fill_in!
147
+ field.set value
148
+ end
79
149
  end
80
150
 
81
151
  class MultipleSelect < Field
152
+ def self.css_selector
153
+ "select"
154
+ end
155
+
82
156
  def matches?
83
157
  field.tag_name == "select" && field[:multiple].to_s == "true"
84
158
  end
@@ -94,6 +168,10 @@ module Chop
94
168
  end
95
169
 
96
170
  class Select < Field
171
+ def self.css_selector
172
+ "select"
173
+ end
174
+
97
175
  def matches?
98
176
  field.tag_name == "select" && field[:multiple].to_s == "false"
99
177
  end
@@ -107,6 +185,10 @@ module Chop
107
185
  field.find("option[value='#{selected_value}']").text
108
186
  end
109
187
  end
188
+
189
+ def diff_value
190
+ get_value.to_s
191
+ end
110
192
  end
111
193
 
112
194
  class MultipleCheckbox < Field
@@ -124,6 +206,10 @@ module Chop
124
206
  checkboxes.select(&:checked?).map(&:value).join(", ")
125
207
  end
126
208
 
209
+ def diff_value
210
+ get_value
211
+ end
212
+
127
213
  private
128
214
 
129
215
  def checkboxes
@@ -149,6 +235,10 @@ module Chop
149
235
  def get_value
150
236
  field.checked? ? "✓" : ""
151
237
  end
238
+
239
+ def diff_value
240
+ get_value
241
+ end
152
242
  end
153
243
 
154
244
  class Radio < Field
@@ -236,13 +326,19 @@ module Chop
236
326
  end
237
327
  end
238
328
 
239
- class Default < Field
329
+ class Textarea < Field
330
+ def self.css_selector
331
+ "textarea"
332
+ end
333
+
240
334
  def matches?
241
- true
335
+ field.tag_name == "textarea"
242
336
  end
337
+ end
243
338
 
244
- def fill_in!
245
- field.set value
339
+ class Default < Field
340
+ def matches?
341
+ true
246
342
  end
247
343
  end
248
344
  end
data/lib/chop/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chop
2
- VERSION = "0.33.1"
2
+ VERSION = "0.35.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.1
4
+ version: 0.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-10 00:00:00.000000000 Z
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -133,6 +133,7 @@ files:
133
133
  - ".gitignore"
134
134
  - ".rspec"
135
135
  - Appraisals
136
+ - CLAUDE.md
136
137
  - Gemfile
137
138
  - LICENSE.txt
138
139
  - README.md