chop 0.38.6 → 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 +4 -4
- data/CLAUDE.md +14 -0
- data/README.md +25 -0
- data/lib/chop/diff.rb +19 -4
- data/lib/chop/form.rb +16 -2
- data/lib/chop/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd1b3c30a7783aa60c7110571a7e7a923cd611f954e7f4f99bb24957c685fb36
|
|
4
|
+
data.tar.gz: 13d01c3c911a879fb0e4e665df653f6bd2e11892cfee27e79ece48afffeb9736
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -59,7 +59,7 @@ module Chop
|
|
|
59
59
|
end
|
|
60
60
|
else
|
|
61
61
|
if block.arity.zero?
|
|
62
|
-
@
|
|
62
|
+
@new_header_block = block
|
|
63
63
|
else
|
|
64
64
|
header_transformation do |row|
|
|
65
65
|
yield(row)
|
|
@@ -130,6 +130,7 @@ module Chop
|
|
|
130
130
|
def to_a
|
|
131
131
|
wait_for_idle! if @atomic
|
|
132
132
|
freeze_page_scripts! if @atomic
|
|
133
|
+
@new_header = @new_header_block.call if @new_header_block
|
|
133
134
|
rows = rows_finder.call(root).map { |row| cells_finder.call(row).to_a }
|
|
134
135
|
rows = normalize(rows)
|
|
135
136
|
|
|
@@ -201,16 +202,30 @@ module Chop
|
|
|
201
202
|
nil
|
|
202
203
|
end
|
|
203
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
|
+
|
|
204
217
|
def wait_for_idle!
|
|
205
218
|
return unless cdp_page
|
|
206
219
|
|
|
207
220
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
221
|
+
idle_samples = 0
|
|
208
222
|
loop do
|
|
209
223
|
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - start > timeout
|
|
210
|
-
pending_frames = session.evaluate_script("document.querySelectorAll(
|
|
224
|
+
pending_frames = session.evaluate_script("document.querySelectorAll(#{PENDING_FRAMES_SELECTOR.inspect}).length")
|
|
211
225
|
pending_network = session.driver.browser.network.pending_connections
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
214
229
|
end
|
|
215
230
|
end
|
|
216
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
|
-
|
|
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
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.
|
|
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-
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|