chop 0.38.7 → 0.38.8

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: 0c453a9613684dc9598f926969f736b6dcf97f2e3ff15103ab1e41f6f73e19d6
4
- data.tar.gz: 70e8130184d8e8a443b7fd8db27e19512963eb288b4db200e98665cfa040345b
3
+ metadata.gz: dd1b3c30a7783aa60c7110571a7e7a923cd611f954e7f4f99bb24957c685fb36
4
+ data.tar.gz: 13d01c3c911a879fb0e4e665df653f6bd2e11892cfee27e79ece48afffeb9736
5
5
  SHA512:
6
- metadata.gz: cdbc3afa528f1608df517ab34eb4d215ea462608377ef4d1cf0b5e073303d961ab37264ecd5aa26789640b9da06f279c95ac0aea7869a87260f8b136af13138f
7
- data.tar.gz: aa9e605301db184c994ad5220a9c8b5ca299fec728088fd7bbc3ebbb6b57cfdf625c66fd387da3024192b2762cd31e2d5aa8f5afef3eb7b154309197dc9f6319
6
+ metadata.gz: c17761d8ae82a8365f784ab32d87273d8e88715f54b5eff65d982d1082f4b6a6c9fc92521fb9d19b1ee36fbf7d1ea0cd6cbaa04d2d700351793a51a96862142b
7
+ data.tar.gz: 7ac7934e482106c3c7ce468e517d4697590a8d994baa0a6e61c73c7056bae69c2627fd19495432737eea6076652756ec847c9ebb5f5fbe16851c5083562f1481
data/CLAUDE.md CHANGED
@@ -69,6 +69,20 @@ Diffing works by:
69
69
  3. Comparing against the expected table using `diff!` from Cucumber
70
70
  4. Supporting element-specific extraction logic (e.g., images in table cells)
71
71
 
72
+ Every `diff!`, including `Chop::Form`'s, wraps the read in `synchronize` and
73
+ re-reads until it matches or the timeout expires. Capybara's `synchronize` is not
74
+ re-entrant, so finders inside that block get a single attempt each and their
75
+ errors have to be retriable at the outer level. A block that post-processes
76
+ `actual` should raise `Capybara::ElementNotFound` to request a re-read.
77
+
78
+ When `Chop.atomic_diff` is on, an atomic read first waits for the page to go idle
79
+ (`Diff#wait_for_idle!`), then disables script execution for the duration of the
80
+ read. Idle means no in-flight network and no frame Turbo is actually fetching —
81
+ lazy and disabled frames carry a `src` that is never fetched, so counting them
82
+ would mean the page is never idle. The check has to hold for
83
+ `IDLE_SAMPLES_REQUIRED` consecutive samples, since the gap between a click and
84
+ the request it triggers also looks idle.
85
+
72
86
  ## Testing Patterns
73
87
 
74
88
  - Uses RSpec with `spec_helper.rb` configuring focus filtering
data/README.md CHANGED
@@ -117,6 +117,31 @@ All these methods are implemented in terms of the following low-level methods, u
117
117
  * `#hash_transformation`: performs arbitrary transformations on an array of hashes constructed by assuming a header row. Hash keys are downcased and underscored.
118
118
  * `#transformation`: performs arbitrary transformations on the 2d array of Capybara nodes.
119
119
 
120
+ ### Diffing a `<form>`:
121
+
122
+ Form diffs take a block that post-processes the rows before comparison:
123
+
124
+ ``` ruby
125
+ table.diff! "#ticket_1", as: :form do |actual, root|
126
+ actual.insert 0, ["Owned by", root.all("owners-select option[selected]").map(&:text).join(" ")]
127
+ end
128
+ ```
129
+
130
+ Like every other `#diff!`, the read is retried until it matches or the timeout
131
+ expires, so the block can be handed a form that is still rendering. Raise
132
+ `Capybara::ElementNotFound` to ask for a re-read rather than working with what is
133
+ there:
134
+
135
+ ``` ruby
136
+ index = actual.index { |row| row.first == "Requested by" }
137
+ raise Capybara::ElementNotFound, "no \"Requested by\" row yet" unless index
138
+ ```
139
+
140
+ Reads inside the block should pass `minimum: 0` to Capybara's `#all`. Its default
141
+ of `minimum: 1` waits out the whole timeout before conceding that a collection is
142
+ legitimately empty, and the retry above already covers a collection that has not
143
+ rendered yet.
144
+
120
145
  ### Block methods for `#fill_in!`:
121
146
 
122
147
  TODO: Does this make sense?
data/lib/chop/diff.rb CHANGED
@@ -202,16 +202,30 @@ module Chop
202
202
  nil
203
203
  end
204
204
 
205
+ # The page has to look quiet for a continuous window before we believe it,
206
+ # since the gap between a click and the fetch it triggers also looks quiet.
207
+ # Widen this on slow or oversubscribed machines, where "quiet" more often
208
+ # means "descheduled" than "finished".
209
+ IDLE_POLL_INTERVAL = 0.05
210
+ IDLE_SAMPLES_REQUIRED = 10
211
+
212
+ # A `src` on a lazy or disabled frame is never fetched, so counting those as
213
+ # pending means the page never goes idle and every atomic diff burns its
214
+ # whole timeout. `busy` covers a lazy frame that has just come into view.
215
+ PENDING_FRAMES_SELECTOR = 'turbo-frame[busy], turbo-frame[src]:not([complete]):not([disabled]):not([loading="lazy"])'
216
+
205
217
  def wait_for_idle!
206
218
  return unless cdp_page
207
219
 
208
220
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
221
+ idle_samples = 0
209
222
  loop do
210
223
  break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - start > timeout
211
- pending_frames = session.evaluate_script("document.querySelectorAll('turbo-frame[src]:not([complete]), turbo-frame[busy]').length")
224
+ pending_frames = session.evaluate_script("document.querySelectorAll(#{PENDING_FRAMES_SELECTOR.inspect}).length")
212
225
  pending_network = session.driver.browser.network.pending_connections
213
- break if pending_frames == 0 && pending_network == 0
214
- sleep 0.05
226
+ idle_samples = (pending_frames == 0 && pending_network == 0) ? idle_samples + 1 : 0
227
+ break if idle_samples >= IDLE_SAMPLES_REQUIRED
228
+ sleep IDLE_POLL_INTERVAL
215
229
  end
216
230
  end
217
231
 
data/lib/chop/form.rb CHANGED
@@ -7,7 +7,21 @@ module Chop
7
7
  new(table, session, path).fill_in!
8
8
  end
9
9
 
10
- def self.diff! selector, table, session: Capybara.current_session, &block
10
+ # Capybara's #synchronize is not re-entrant, so the finds below get a single
11
+ # attempt each and have to retry out here instead — hence ElementNotFound in
12
+ # the error list. A block that post-processes `actual` should raise
13
+ # Capybara::ElementNotFound to ask for a re-read of a half-rendered form.
14
+ def self.diff! selector, table, session: Capybara.current_session, timeout: nil, &block
15
+ errors = session.driver.invalid_element_errors + [
16
+ Cucumber::MultilineArgument::DataTable::Different,
17
+ Capybara::ElementNotFound,
18
+ ]
19
+ session.document.synchronize timeout || Capybara.default_max_wait_time, errors: errors do
20
+ diff_once! selector, table, session: session, &block
21
+ end
22
+ end
23
+
24
+ def self.diff_once! selector, table, session:, &block
11
25
  root = begin
12
26
  if selector.is_a?(Capybara::Node::Element)
13
27
  selector
@@ -28,7 +42,7 @@ module Chop
28
42
  block.call(actual, root) if block_given?
29
43
  table.diff! actual, surplus_row: false, misplaced_col: false
30
44
  end
31
-
45
+ private_class_method :diff_once!
32
46
 
33
47
  def fill_in!
34
48
  table.rows_hash.each do |label, value|
data/lib/chop/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chop
2
- VERSION = "0.38.7"
2
+ VERSION = "0.38.8"
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.38.7
4
+ version: 0.38.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-03 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord