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 +4 -4
- data/CLAUDE.md +61 -0
- data/lib/chop/create.rb +4 -2
- data/lib/chop/form.rb +124 -28
- data/lib/chop/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97c2fe11438b99e52e5f3165ec8164eb7447c1a6238dffd4855d5a7f748b20af
|
4
|
+
data.tar.gz: 7e14858a804e3f58a44effecc1c5d92b24b79c59db4186c20625134f5c30c731
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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.
|
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
|
329
|
+
class Textarea < Field
|
330
|
+
def self.css_selector
|
331
|
+
"textarea"
|
332
|
+
end
|
333
|
+
|
240
334
|
def matches?
|
241
|
-
|
335
|
+
field.tag_name == "textarea"
|
242
336
|
end
|
337
|
+
end
|
243
338
|
|
244
|
-
|
245
|
-
|
339
|
+
class Default < Field
|
340
|
+
def matches?
|
341
|
+
true
|
246
342
|
end
|
247
343
|
end
|
248
344
|
end
|
data/lib/chop/version.rb
CHANGED
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.
|
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-
|
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
|