chop 0.35.2 → 0.36.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 +25 -9
- data/README.md +42 -1
- data/lib/chop/diff.rb +13 -0
- data/lib/chop/regex_templates.rb +97 -0
- 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: 8af1c5a00064df8f6b191c22e8724dcea6f4be1cf6ba940d7976a316efdd9b18
|
|
4
|
+
data.tar.gz: 4c04d4ec5e3ef08fad6f2b7b4534bd182d353427f630526d40d3b2987a7c2912
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75c8391e2783ca33dbe20f19e8a0cb0d4f46923c36ea3a0941d4a726f93817b89abcf3c0f33ff2fa40f3a5a8aeaf7eea3bfea563b0453e513360c7c0634659cf
|
|
7
|
+
data.tar.gz: fbd050596b406cd0624e1dd20daf058c713251604103685d678a5a7fd652445fd1f91239dcb901104e7e38f3c8a1cfe302a70c63f0d2d1d5bf2544aac3c3faa2
|
data/CLAUDE.md
CHANGED
|
@@ -10,14 +10,17 @@ Chop is a Ruby gem that enhances Cucumber tables with three main methods: `#crea
|
|
|
10
10
|
|
|
11
11
|
### Testing
|
|
12
12
|
- `rake spec` or `bundle exec rspec` - Run the full test suite
|
|
13
|
-
- `rspec spec/chop/table_spec.rb` - Run a specific test file
|
|
13
|
+
- `rspec spec/chop/table_spec.rb` - Run a specific test file
|
|
14
14
|
- `rspec spec/chop/table_spec.rb:42` - Run a specific test by line number
|
|
15
15
|
|
|
16
|
+
Note: RSpec is configured to run only tests marked with `focus: true`. When no focused tests exist, all tests run automatically.
|
|
17
|
+
|
|
16
18
|
### Development Setup
|
|
17
19
|
- `bin/setup` - Install dependencies after checkout
|
|
18
20
|
- `bin/console` - Interactive console for experimentation
|
|
19
21
|
|
|
20
22
|
### Multi-Version Testing
|
|
23
|
+
Tests must pass against Rails 7.0, 7.1, and 7.2 on Ruby 3.1, 3.2, and 3.3:
|
|
21
24
|
- `bundle exec appraisal install` - Install gems for all Rails versions
|
|
22
25
|
- `bundle exec appraisal rails-7.0 rspec` - Test against specific Rails version
|
|
23
26
|
- `bundle exec appraisal rspec` - Test against all Rails versions
|
|
@@ -34,12 +37,13 @@ Chop is a Ruby gem that enhances Cucumber tables with three main methods: `#crea
|
|
|
34
37
|
- **`Chop::Diff`** (lib/chop/diff.rb) - Base class for HTML element diffing with Capybara
|
|
35
38
|
- **Element-specific diffing classes**:
|
|
36
39
|
- `Chop::Table` - HTML table diffing
|
|
37
|
-
- `Chop::DefinitionList` - Definition list (`<dl>`) diffing
|
|
40
|
+
- `Chop::DefinitionList` - Definition list (`<dl>`) diffing
|
|
41
|
+
- `Chop::DfnDl` - Definition list with `<dfn>` headings
|
|
38
42
|
- `Chop::UnorderedList` - Unordered list (`<ul>`) diffing
|
|
39
43
|
- `Chop::Form` - Form filling functionality
|
|
40
44
|
|
|
41
45
|
### Monkey-patching Strategy
|
|
42
|
-
The gem extends `Cucumber::MultilineArgument::DataTable` by prepending
|
|
46
|
+
The gem extends `Cucumber::MultilineArgument::DataTable` by prepending the DSL module, which 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 entirely.
|
|
43
47
|
|
|
44
48
|
### Creation Strategies
|
|
45
49
|
Supports pluggable creation strategies via `Chop::Create.register_creation_strategy`:
|
|
@@ -49,13 +53,25 @@ Supports pluggable creation strategies via `Chop::Create.register_creation_strat
|
|
|
49
53
|
|
|
50
54
|
### Transformation DSL
|
|
51
55
|
The `#create!` method supports a rich DSL for transforming table data:
|
|
52
|
-
- Field transformations
|
|
53
|
-
- Low-level
|
|
54
|
-
- Lifecycle hooks
|
|
56
|
+
- **Field transformations**: `file`, `files`, `has_one`, `has_many`, `default`, `copy`, `rename`, `delete`
|
|
57
|
+
- **Low-level**: `field`, `transformation` for custom logic
|
|
58
|
+
- **Lifecycle hooks**: `after` for post-creation logic, `create` to override creation strategy
|
|
59
|
+
|
|
60
|
+
The `#diff!` method also supports transformations:
|
|
61
|
+
- **Capybara finders**: `rows`, `cells`, `text` to override default element selection
|
|
62
|
+
- **High-level transforms**: `image` to extract image filenames, `allow_not_found` for optional elements
|
|
63
|
+
- **Low-level**: `header`, `field`, `hash_transformation`, `transformation` for custom logic
|
|
64
|
+
|
|
65
|
+
### Diffing Architecture
|
|
66
|
+
Diffing works by:
|
|
67
|
+
1. Selecting HTML elements using Capybara finders (customizable via `rows`, `cells` blocks)
|
|
68
|
+
2. Extracting text content from those elements
|
|
69
|
+
3. Comparing against the expected table using `diff!` from Cucumber
|
|
70
|
+
4. Supporting element-specific extraction logic (e.g., images in table cells)
|
|
55
71
|
|
|
56
72
|
## Testing Patterns
|
|
57
73
|
|
|
58
|
-
- Uses RSpec with `spec_helper.rb` configuring focus
|
|
74
|
+
- Uses RSpec with `spec_helper.rb` configuring focus filtering
|
|
59
75
|
- Tests organized by component in `spec/chop/` matching `lib/chop/` structure
|
|
60
|
-
- Tests cover both direct
|
|
61
|
-
- Uses Capybara for integration testing of diffing functionality
|
|
76
|
+
- Tests cover both direct DSL calls and monkey-patched table behavior
|
|
77
|
+
- Uses Capybara for integration testing of diffing functionality with Cuprite headless browser
|
data/README.md
CHANGED
|
@@ -64,6 +64,48 @@ Overide Capybara finders:
|
|
|
64
64
|
High-level declarative transformations:
|
|
65
65
|
* `#image`: Replaces the specified cell with the filename of the first image within it, stripped of path and cachebusters.
|
|
66
66
|
|
|
67
|
+
Regex templates (opt‑in):
|
|
68
|
+
* `#regex(*fields)`: Enables embedded regex templates inside expected cells for flexible matching. By default applies to all fields; optionally whitelist columns by header name (symbol/string) or by 1‑based column index.
|
|
69
|
+
|
|
70
|
+
- Syntax: write literal text with embedded regex tokens using Ruby‑style interpolation markers: `#{/pattern/flags}`. Flags support `i`, `m`, `x`.
|
|
71
|
+
- Matching: builds a single anchored regex for the entire cell by escaping literal segments and splicing regex tokens. The whole cell must match.
|
|
72
|
+
- Multiple tokens: allowed; flags across tokens are OR’ed together.
|
|
73
|
+
- Escaping: write `\#{/…/}` to render a token literally (no matching). The backslash is removed before comparing.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# All fields enabled (table header present)
|
|
79
|
+
expected = [
|
|
80
|
+
["Attachments"],
|
|
81
|
+
['attachment.jpg 23.4 KB browser-report.txt #{/1\.\d{2} KB/}']
|
|
82
|
+
]
|
|
83
|
+
expected.diff!("table") { regex }
|
|
84
|
+
|
|
85
|
+
# Whitelist by header name (normalized like header keys used elsewhere)
|
|
86
|
+
expected = [
|
|
87
|
+
["A", "B"],
|
|
88
|
+
["foo 123", 'bar #{/\d{3}/}']
|
|
89
|
+
]
|
|
90
|
+
expected.diff!("table") { regex :b }
|
|
91
|
+
|
|
92
|
+
# Whitelist by 1-based column index
|
|
93
|
+
expected = [
|
|
94
|
+
["A", "B"],
|
|
95
|
+
["foo 123", 'bar #{/\d{3}/}']
|
|
96
|
+
]
|
|
97
|
+
expected.diff!("table") { regex 2 }
|
|
98
|
+
|
|
99
|
+
# Non-whitelisted columns treat tokens as literal
|
|
100
|
+
expected = [
|
|
101
|
+
["A", "B"],
|
|
102
|
+
['#{/\w+ \d{3}/}', "bar 456"]
|
|
103
|
+
]
|
|
104
|
+
expect {
|
|
105
|
+
expected.diff!("table") { regex :b }
|
|
106
|
+
}.to raise_error(Cucumber::MultilineArgument::DataTable::Different)
|
|
107
|
+
```
|
|
108
|
+
|
|
67
109
|
All these methods are implemented in terms of the following low-level methods, useful for when you need more control over the transformation:
|
|
68
110
|
* `#header`: add or transform the table header, depending on block arity.
|
|
69
111
|
* `#header(key)`: transform the specified header column, specified either by numeric index, or by hash key.
|
|
@@ -169,4 +211,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/botand
|
|
|
169
211
|
## License
|
|
170
212
|
|
|
171
213
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
172
|
-
|
data/lib/chop/diff.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "active_support/core_ext/string/inflections"
|
|
|
2
2
|
require "active_support/core_ext/object/blank"
|
|
3
3
|
require "active_support/core_ext/class/attribute"
|
|
4
4
|
require "active_support/hash_with_indifferent_access"
|
|
5
|
+
require "chop/regex_templates"
|
|
5
6
|
|
|
6
7
|
module Chop
|
|
7
8
|
class Diff < Struct.new(:selector, :table, :session, :timeout, :block)
|
|
@@ -27,6 +28,8 @@ module Chop
|
|
|
27
28
|
|
|
28
29
|
attr_accessor :header_transformations, :transformations
|
|
29
30
|
|
|
31
|
+
attr_accessor :regex_templates_enabled, :regex_fields
|
|
32
|
+
|
|
30
33
|
def initialize selector = nil, table = nil, session = Capybara.current_session, timeout = Capybara.default_max_wait_time, block = nil, &other_block
|
|
31
34
|
super
|
|
32
35
|
self.selector ||= default_selector
|
|
@@ -66,6 +69,13 @@ module Chop
|
|
|
66
69
|
transformations << block
|
|
67
70
|
end
|
|
68
71
|
|
|
72
|
+
# Enable embedded-regex templates within cells.
|
|
73
|
+
# Optionally restrict application to specific fields (by header name or 1-based index).
|
|
74
|
+
def regex *fields
|
|
75
|
+
self.regex_templates_enabled = true
|
|
76
|
+
self.regex_fields = fields unless fields.empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
69
79
|
def hash_transformation &block
|
|
70
80
|
transformation do |rows|
|
|
71
81
|
header = rows[0]
|
|
@@ -143,6 +153,9 @@ module Chop
|
|
|
143
153
|
actual = to_a
|
|
144
154
|
# FIXME should just delegate to Cucumber's #diff!. Cucumber needs to handle empty tables better.
|
|
145
155
|
if !cucumber_table.raw.flatten.empty? && !actual.flatten.empty?
|
|
156
|
+
if regex_templates_enabled
|
|
157
|
+
cucumber_table = Chop::RegexTemplates.apply(cucumber_table, actual, regex_fields)
|
|
158
|
+
end
|
|
146
159
|
cucumber_table.diff! actual, **kwargs
|
|
147
160
|
elsif cucumber_table.raw.flatten != actual.flatten
|
|
148
161
|
raise Cucumber::MultilineArgument::DataTable::Different.new(cucumber_table)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Chop
|
|
2
|
+
module RegexTemplates
|
|
3
|
+
TOKEN = /
|
|
4
|
+
(?<!\\) # not preceded by backslash
|
|
5
|
+
\#\{ # start of token
|
|
6
|
+
\/ # opening slash
|
|
7
|
+
(.*?) # pattern (non-greedy)
|
|
8
|
+
\/ # closing slash
|
|
9
|
+
([imx]*) # optional flags
|
|
10
|
+
\} # end of token
|
|
11
|
+
/mx
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def apply(cucumber_table, actual, fields)
|
|
16
|
+
allowed_columns = columns_for(fields, cucumber_table.raw.first)
|
|
17
|
+
|
|
18
|
+
expected = cucumber_table.raw.map.with_index do |row, i|
|
|
19
|
+
row.map.with_index do |cell, j|
|
|
20
|
+
str = cell.to_s
|
|
21
|
+
# De-escape literal token markers so \#{/.../} becomes literal '#{...}'
|
|
22
|
+
deescaped = str.gsub('\\#{', '#{')
|
|
23
|
+
|
|
24
|
+
if allowed?(allowed_columns, j) && deescaped.include?('#{') && deescaped.match?(TOKEN)
|
|
25
|
+
regex = expand_template(deescaped)
|
|
26
|
+
actual_cell = (actual.dig(i, j) || "").to_s
|
|
27
|
+
if regex.match?(actual_cell)
|
|
28
|
+
actual_cell
|
|
29
|
+
else
|
|
30
|
+
deescaped
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
deescaped
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Cucumber::MultilineArgument::DataTable.from(expected)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def columns_for(fields, expected_header)
|
|
42
|
+
return :all if fields.nil? || fields.empty?
|
|
43
|
+
|
|
44
|
+
idxs = []
|
|
45
|
+
|
|
46
|
+
fields.each do |f|
|
|
47
|
+
case f
|
|
48
|
+
when Integer
|
|
49
|
+
idxs << (f - 1)
|
|
50
|
+
when Symbol, String
|
|
51
|
+
next unless expected_header
|
|
52
|
+
normalized = f.to_s.parameterize.underscore
|
|
53
|
+
header_keys = expected_header.map.with_index do |text, idx|
|
|
54
|
+
t = text.to_s
|
|
55
|
+
key = t.parameterize.underscore
|
|
56
|
+
key = t if key.blank? && t.present?
|
|
57
|
+
key = (idx + 1).to_s if key.blank?
|
|
58
|
+
key
|
|
59
|
+
end
|
|
60
|
+
header_keys.each_with_index do |key, idx|
|
|
61
|
+
idxs << idx if key == normalized
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
idxs.uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def allowed?(allowed_columns, j)
|
|
70
|
+
return true if allowed_columns == :all
|
|
71
|
+
allowed_columns.include?(j)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def expand_template(str)
|
|
75
|
+
parts = []
|
|
76
|
+
last = 0
|
|
77
|
+
|
|
78
|
+
str.to_enum(:scan, TOKEN).each do
|
|
79
|
+
m = Regexp.last_match
|
|
80
|
+
literal = str[last...m.begin(0)]
|
|
81
|
+
parts << Regexp.escape(literal)
|
|
82
|
+
pattern, flags = m.captures
|
|
83
|
+
if flags.to_s.empty?
|
|
84
|
+
parts << "(?:#{pattern})"
|
|
85
|
+
else
|
|
86
|
+
parts << "(?#{flags}:#{pattern})"
|
|
87
|
+
end
|
|
88
|
+
last = m.end(0)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
tail = str[last..-1] || ""
|
|
92
|
+
parts << Regexp.escape(tail)
|
|
93
|
+
|
|
94
|
+
Regexp.new("\\A(?:#{parts.join})\\z")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
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.36.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-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -153,6 +153,7 @@ files:
|
|
|
153
153
|
- lib/chop/diff.rb
|
|
154
154
|
- lib/chop/dsl.rb
|
|
155
155
|
- lib/chop/form.rb
|
|
156
|
+
- lib/chop/regex_templates.rb
|
|
156
157
|
- lib/chop/table.rb
|
|
157
158
|
- lib/chop/unordered_list.rb
|
|
158
159
|
- lib/chop/version.rb
|