capybara-lightpanda 0.14.2 → 0.15.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.
@@ -20,10 +20,11 @@ module Capybara
20
20
  # `Capybara::Lightpanda.configure { |c| c.port = 9222 }` when external
21
21
  # tooling needs a known address.
22
22
  DEFAULT_PORT = 0
23
- # Mirrors Lightpanda's own `Viewport.default` (1920x1080) rather than
24
- # Cuprite's 1024x768. window_size is applied for real now (see below), so
25
- # a 1024x768 default would silently shrink the viewport of every existing
26
- # suite and flip `@media` branches under them. Matching the browser's
23
+ # Mirrors Lightpanda's own `Viewport.default` (1920x1080), not the
24
+ # 1024x768 some drivers default to. window_size is applied for real now
25
+ # (see below), so a 1024x768 default would silently shrink the viewport
26
+ # of every existing suite and flip `@media` branches under them.
27
+ # Matching the browser's
27
28
  # native default keeps `window_size` truthful AND leaves default
28
29
  # behavior byte-identical to before it was wired up.
29
30
  DEFAULT_WINDOW_SIZE = [1920, 1080].freeze
@@ -36,16 +37,17 @@ module Capybara
36
37
  # no rendering engine, so getBoundingClientRect stays synthetic and
37
38
  # nothing reflows. Sizing down will not make an off-viewport element
38
39
  # report as obscured.
39
- # headless is accepted for Cuprite drop-in compatibility but inert —
40
+ # headless is accepted so suites migrating from other drivers keep
41
+ # working, but is inert —
40
42
  # headless is the only mode Lightpanda runs in.
41
- # save_path: directory for downloaded files (Cuprite parity). nil falls
43
+ # save_path: directory for downloaded files. nil falls
42
44
  # back to Capybara.save_path at create_page time; downloads stay off when
43
45
  # both are nil (Browser#create_page only opts in when a path exists).
44
46
  # raise_on_unhandled_modal: a JS dialog that opens with no
45
47
  # accept_modal/dismiss_modal pre-arm in flight is resolved by Lightpanda's
46
48
  # silent default (confirm → cancel, prompt → null, alert → dismissed), so
47
49
  # a runaway confirm cancels the action and the spec can still pass. false
48
- # (default, Cuprite parity) warns on stderr; true raises
50
+ # (default) warns on stderr; true raises
49
51
  # UnhandledModalError from the action that opened it.
50
52
  # load_images: adds `image` to the `--load-resources` value so `<img>`
51
53
  # elements are fetched over the network (upstream #3230, build >= 8834,
@@ -264,7 +264,48 @@ module Capybara
264
264
  # end in Network.loadingFailed, which Network consumes); #3529 (9877)
265
265
  # SameSite=None without Secure refused (Cookies#set reports it);
266
266
  # #3702 (9968) click no longer focuses (CLICK_JS focuses itself).
267
- MINIMUM_NIGHTLY_BUILD = Gem::Version.new("9994")
267
+ #
268
+ # 2026-10-16 bump 9994 -> 10641 (= release 1.0.1, the new lockstep cap).
269
+ # Retired or ungated by it:
270
+ # #3958 (10613, ours) Window and Document carry the GlobalEventHandlers
271
+ # `on*` attributes, so javascripts/event_handlers.js is gone — jQuery
272
+ # 1.x sees `'onchange' in window` natively.
273
+ # #3940 (10536) a link to a non-fetchable scheme (mailto:, tel:) is a
274
+ # no-op that keeps the document, so CLICK_JS no longer cancels the
275
+ # click's default from a window listener.
276
+ # #3755 / #3849 (10049 / 10310, ours / upstream) exceptions from timers,
277
+ # rAF, inline handlers and microtasks reach `window.onerror`, so
278
+ # Browser#page_errors sees them; #3757 (10052) hides a closed
279
+ # [popover]; #3759 (10057) routes send_keys into an iframe; #3874
280
+ # (10366) Ctrl+A selects a field — upstream_fixes_test.rb runs them
281
+ # un-gated.
282
+ # #3763 (10239, ours) history traverses pushState entries in place;
283
+ # #3831 (10255) `window` serializes as CyclicObject, not a node;
284
+ # #3843 (10261) a failed navigation commits an error document — their
285
+ # tests drop the nightly_build branches.
286
+ # #3952 (10575) typed text fires `change` when committed by blur/Enter.
287
+ # A `nightly_build` gate is nil on a release binary, so every such test
288
+ # skipped on 1.0.1 although it carried the fix: ungate, don't re-gate.
289
+ #
290
+ # 2026-10-23 bump 10641 -> 10965 (= release 1.0.2, the new lockstep cap;
291
+ # CSS nesting #3962 at 10976 and blank-page timers #4148 at 11012 stay
292
+ # above it). Rides along:
293
+ # #4096 (10904) Runtime.exceptionThrown for unhandled rejections, and
294
+ # `unhandledrejection` no longer fires for a rejection handled in
295
+ # the same tick — errors.js stops reporting those false positives;
296
+ # page_errors_test.rb drops its nightly_build gate. With #4115
297
+ # (10910, an iframe's exception carries its context id) this
298
+ # unblocks the B19 swap off errors.js.
299
+ # #3997 (10685) the scheduler rework: a MessageChannel task loop no
300
+ # longer delays a pending fetch tenfold (#4042), which held
301
+ # wait_for_idle.
302
+ # #3658 (10657) client rects follow the scroll and
303
+ # DOM.scrollIntoViewIfNeeded is real — HTML5_DRAG_JS centers the
304
+ # drop target itself because scrollIntoView ignores `block`.
305
+ MINIMUM_NIGHTLY_BUILD = Gem::Version.new("10965")
306
+
307
+ # Lines of browser output #output keeps (ferrum keeps the same 100).
308
+ OUTPUT_LINES = 100
268
309
 
269
310
  # Second, equivalent floor for the *release* channel.
270
311
  #
@@ -273,16 +314,16 @@ module Capybara
273
314
  # bare "0.3.6" carrying no commit counter at all, and MINIMUM_NIGHTLY_BUILD
274
315
  # has nothing to compare against. Releases are cut from the same trunk, so
275
316
  # a release is acceptable exactly when its own commit count clears the
276
- # nightly floor. 1.0.0 (2026-10-02) is build 9994 — exactly the nightly
317
+ # nightly floor. 1.0.2 (2026-10-23) is build 10965 — exactly the nightly
277
318
  # floor, which is no accident: the floor was raised *to* the release so
278
- # both channels gate the same commit. (0.4.1 = 9463 and 0.4.0 = 9058
279
- # predate fixes the floor exists for.)
319
+ # both channels gate the same commit. (1.0.1 = 10641 predates fixes the
320
+ # floor exists for.)
280
321
  #
281
322
  # INVARIANT: every MINIMUM_NIGHTLY_BUILD bump must also move this to the
282
323
  # first release containing that build (`git rev-list --count <tag>` in the
283
324
  # browser repo tells you). Leaving it behind would let the release channel
284
325
  # silently accept a binary the nightly channel rejects.
285
- MINIMUM_RELEASE = Gem::Version.new("1.0.0")
326
+ MINIMUM_RELEASE = Gem::Version.new("1.0.2")
286
327
 
287
328
  class << self
288
329
  # `lightpanda version` prints one of two shapes, and the gem supports
@@ -348,6 +389,17 @@ module Capybara
348
389
  @stderr_r = nil
349
390
  @stderr_w = nil
350
391
  @finalizer_registered = false
392
+ @output = []
393
+ @output_partial = {}
394
+ @output_mutex = Mutex.new
395
+ end
396
+
397
+ # The last OUTPUT_LINES lines Lightpanda wrote to stdout/stderr, oldest
398
+ # first, across restarts of this Process. Kept for diagnostics: a crash
399
+ # or panic only ever shows up there, and from build 10783 so do page
400
+ # errors (`page_error` scope, WARN). Mirrors ferrum's Process::Output.
401
+ def output
402
+ @output_mutex.synchronize { @output.dup }
351
403
  end
352
404
 
353
405
  def start
@@ -416,7 +468,7 @@ module Capybara
416
468
  next unless ready
417
469
 
418
470
  ready[0].each do |io|
419
- io.read_nonblock(4096)
471
+ record_output(io, io.read_nonblock(4096))
420
472
  rescue IO::WaitReadable
421
473
  # No data
422
474
  rescue EOFError
@@ -430,6 +482,18 @@ module Capybara
430
482
  end
431
483
  end
432
484
 
485
+ # Split per pipe so a line cut across two reads is kept whole, and a
486
+ # stdout fragment never glues onto a stderr one.
487
+ def record_output(io, chunk)
488
+ text = chunk.dup.force_encoding(Encoding::UTF_8).scrub
489
+ @output_mutex.synchronize do
490
+ *lines, rest = "#{@output_partial[io]}#{text}".split("\n", -1)
491
+ @output_partial[io] = rest
492
+ @output.concat(lines)
493
+ @output.shift(@output.size - OUTPUT_LINES) if @output.size > OUTPUT_LINES
494
+ end
495
+ end
496
+
433
497
  def spawn_process(binary_path)
434
498
  args = build_args
435
499
 
@@ -499,6 +563,7 @@ module Capybara
499
563
  ready[0].each do |io|
500
564
  chunk = io.read_nonblock(1024)
501
565
  output << chunk
566
+ record_output(io, chunk)
502
567
 
503
568
  if (match = output.match(READY_PATTERN))
504
569
  @ws_url = "ws://#{match[1]}/"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Lightpanda
5
- VERSION = "0.14.2"
5
+ VERSION = "0.15.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-lightpanda
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.2
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Navid Emad
@@ -91,7 +91,6 @@ files:
91
91
  - lib/capybara/lightpanda/headers.rb
92
92
  - lib/capybara/lightpanda/javascripts/attach.js
93
93
  - lib/capybara/lightpanda/javascripts/banner.js
94
- - lib/capybara/lightpanda/javascripts/errors.js
95
94
  - lib/capybara/lightpanda/javascripts/predicates.js
96
95
  - lib/capybara/lightpanda/javascripts/turbo.js
97
96
  - lib/capybara/lightpanda/keyboard.rb
@@ -1,74 +0,0 @@
1
- // --- Uncaught page errors ---
2
- // Lightpanda implements no `Runtime.exceptionThrown` (upstream gap, wishlist
3
- // B19), so an uncaught exception in page JS reaches no CDP client at all. It is
4
- // invisible to console_logs, which only ever sees explicit console.* calls, so
5
- // a handler that dies on a TypeError leaves no trace and the failure surfaces
6
- // far away as ElementNotFound on whatever it was supposed to produce. That is
7
- // the exact shape of the solidus taxon-tree failure in real-apps causes.yml.
8
- //
9
- // These listeners are passive — no preventDefault, so nothing the page would
10
- // otherwise see is swallowed, and nothing that would have propagated stops
11
- // propagating. They re-emit through the same console.debug sentinel channel
12
- // turbo.js uses, and console.rb routes them into Browser#page_errors rather
13
- // than #console_logs. Keeping the two apart is deliberate: console_logs stays
14
- // "the console.* calls the page made", which is what Chrome models (an
15
- // exception is exceptionThrown, not consoleAPICalled) and what Playwright and
16
- // Puppeteer expose (pageerror, separate from console). Folding exceptions into
17
- // console_logs would also break every suite already asserting that buffer is
18
- // free of errors.
19
- //
20
- // Delete this file once upstream emits Runtime.exceptionThrown and the floor
21
- // covers it: subscribe Browser::Console to that event and feed the same buffer.
22
- function _reportPageError(fields) {
23
- try {
24
- console.debug('__lightpanda_page_error_' + JSON.stringify(fields));
25
- } catch (e) {
26
- // A field that won't serialize (a getter that throws, a cyclic `reason`)
27
- // must not turn one page error into two, so report the kind and move on.
28
- try {
29
- console.debug('__lightpanda_page_error_' + JSON.stringify({
30
- kind: fields && fields.kind ? fields.kind : 'error',
31
- message: '(page error could not be serialized)'
32
- }));
33
- } catch (e2) {}
34
- }
35
- }
36
-
37
- window.addEventListener('error', function (e) {
38
- // A failed <img>/<script>/<link> fetch fires `error` too, but on the element
39
- // and without bubbling, so it shouldn't reach this listener. Guard anyway
40
- // (Lightpanda's dispatch need not match Chrome's exactly) and identify those
41
- // by their missing string `message` — a resource failure is a network fact
42
- // and Network#traffic already carries it.
43
- if (!e || typeof e.message !== 'string') return;
44
-
45
- _reportPageError({
46
- kind: 'error',
47
- message: e.message,
48
- url: typeof e.filename === 'string' ? e.filename : '',
49
- line: typeof e.lineno === 'number' ? e.lineno : null,
50
- column: typeof e.colno === 'number' ? e.colno : null,
51
- // Cap the stack: it rides a console argument over the CDP WebSocket, and a
52
- // deep recursion stack can be megabytes.
53
- stack: e.error && typeof e.error.stack === 'string' ? e.error.stack.slice(0, 4000) : null
54
- });
55
- });
56
-
57
- window.addEventListener('unhandledrejection', function (e) {
58
- var reason = e ? e.reason : null;
59
- var message;
60
- try {
61
- message = reason && reason.message ? String(reason.message) : String(reason);
62
- } catch (err) {
63
- message = '(rejection reason could not be stringified)';
64
- }
65
-
66
- _reportPageError({
67
- kind: 'unhandledrejection',
68
- message: message,
69
- url: '',
70
- line: null,
71
- column: null,
72
- stack: reason && typeof reason.stack === 'string' ? reason.stack.slice(0, 4000) : null
73
- });
74
- });