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 +4 -4
- data/CLAUDE.md +14 -0
- data/README.md +25 -0
- data/lib/chop/diff.rb +17 -3
- 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
|
@@ -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(
|
|
224
|
+
pending_frames = session.evaluate_script("document.querySelectorAll(#{PENDING_FRAMES_SELECTOR.inspect}).length")
|
|
212
225
|
pending_network = session.driver.browser.network.pending_connections
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|