chop 0.35.2 → 0.36.1

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: 88bedb7f454adc2966b82d3c5748b88ca81498d6de9f46de02f5ee0256b9c641
4
- data.tar.gz: a727470df4d49a8f170724367891caa6f8cd380ef637af75f867b5666924163b
3
+ metadata.gz: a932a3e8344d082e310dceec42f21dfbba56cd11264e330aa06245e66e6af07f
4
+ data.tar.gz: 0b904e3f1dbb926a0138614fee3b96e2d9aeb1363711c04b758058328904afd3
5
5
  SHA512:
6
- metadata.gz: aa9383445e2c7333c58a107a00c570b20c3bc4f59e47a1b08bbc55682af675af05f9af0a45c61821e04142f99fe6c9a2244ff6c8fa4ab554d77ef5eb2f1e102d
7
- data.tar.gz: 2c1b982d7f1a30cf2a2603927e98cb32b496efbe8c6eb83f3e5c14434f1a8061c33f42a5d50b882e914d55f80c9bd18cadfd4e51a66126f5bfd08bc4a6aa49cd
6
+ metadata.gz: 4010088d237ee4e02e40161da7d2f9d0caa049bf07b6b3d63267e08419a30ac5b2582e433e1176c79b6a07e885294e090b6d4f6c94c466eebe37479efc32fe30
7
+ data.tar.gz: c5a21854d4c7ca77e07edd9c3145679953ddf7bd52dd356777a2fc88cfdcea83af7048be55a5a2cacd064bf72792eab9b5e40e221a26514db3ccedd50cf1cb2c
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 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.
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: `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
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 and run-all behavior
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 method calls and monkey-patched table behavior
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,14 +2,27 @@ 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)
8
9
  def self.diff! selector, table, session: Capybara.current_session, timeout: Capybara.default_max_wait_time, errors: [], **kwargs, &block
9
10
  errors += session.driver.invalid_element_errors
10
11
  errors += [Cucumber::MultilineArgument::DataTable::Different]
11
- session.document.synchronize timeout, errors: errors do
12
- new(selector, table, session, timeout, block).diff! **kwargs
12
+ synchronize_with_retry(session, timeout, errors) do
13
+ new(selector, table, session, timeout, block).diff!(**kwargs)
14
+ end
15
+ end
16
+
17
+ def self.synchronize_with_retry(session, timeout, errors)
18
+ interval = session.config.default_retry_interval
19
+ timer = Capybara::Helpers.timer(expire_in: timeout)
20
+ begin
21
+ yield
22
+ rescue *errors => e
23
+ raise e if timer.expired?
24
+ sleep interval
25
+ retry
13
26
  end
14
27
  end
15
28
 
@@ -27,6 +40,8 @@ module Chop
27
40
 
28
41
  attr_accessor :header_transformations, :transformations
29
42
 
43
+ attr_accessor :regex_templates_enabled, :regex_fields
44
+
30
45
  def initialize selector = nil, table = nil, session = Capybara.current_session, timeout = Capybara.default_max_wait_time, block = nil, &other_block
31
46
  super
32
47
  self.selector ||= default_selector
@@ -66,6 +81,13 @@ module Chop
66
81
  transformations << block
67
82
  end
68
83
 
84
+ # Enable embedded-regex templates within cells.
85
+ # Optionally restrict application to specific fields (by header name or 1-based index).
86
+ def regex *fields
87
+ self.regex_templates_enabled = true
88
+ self.regex_fields = fields unless fields.empty?
89
+ end
90
+
69
91
  def hash_transformation &block
70
92
  transformation do |rows|
71
93
  header = rows[0]
@@ -143,6 +165,9 @@ module Chop
143
165
  actual = to_a
144
166
  # FIXME should just delegate to Cucumber's #diff!. Cucumber needs to handle empty tables better.
145
167
  if !cucumber_table.raw.flatten.empty? && !actual.flatten.empty?
168
+ if regex_templates_enabled
169
+ cucumber_table = Chop::RegexTemplates.apply(cucumber_table, actual, regex_fields)
170
+ end
146
171
  cucumber_table.diff! actual, **kwargs
147
172
  elsif cucumber_table.raw.flatten != actual.flatten
148
173
  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
@@ -1,3 +1,3 @@
1
1
  module Chop
2
- VERSION = "0.35.2"
2
+ VERSION = "0.36.1"
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.35.2
4
+ version: 0.36.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-03 00:00:00.000000000 Z
11
+ date: 2025-12-22 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
@@ -160,7 +161,7 @@ homepage: http://github.com/botandrose/chop
160
161
  licenses:
161
162
  - MIT
162
163
  metadata: {}
163
- post_install_message:
164
+ post_install_message:
164
165
  rdoc_options: []
165
166
  require_paths:
166
167
  - lib
@@ -175,8 +176,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
176
  - !ruby/object:Gem::Version
176
177
  version: '0'
177
178
  requirements: []
178
- rubygems_version: 3.5.6
179
- signing_key:
179
+ rubygems_version: 3.4.19
180
+ signing_key:
180
181
  specification_version: 4
181
182
  summary: Slice and dice your cucumber tables with ease!
182
183
  test_files: []