capybara-simulated 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +75 -9
- data/lib/capybara/simulated/browser.rb +410 -72
- data/lib/capybara/simulated/driver.rb +92 -13
- data/lib/capybara/simulated/errors.rb +7 -0
- data/lib/capybara/simulated/js/bridge.bundle.js +402 -51
- data/lib/capybara/simulated/node.rb +19 -3
- data/lib/capybara/simulated/runtime_shared.rb +10 -0
- data/lib/capybara/simulated/v8_runtime.rb +64 -9
- data/lib/capybara/simulated/version.rb +1 -1
- metadata +1 -1
|
@@ -17,9 +17,13 @@ module Capybara
|
|
|
17
17
|
@browser = driver.current_browser
|
|
18
18
|
@initial_node = @browser.lookup_node(handle)
|
|
19
19
|
@context_gen = @browser.context_gen
|
|
20
|
+
# The frame realm this handle belongs to (nil = main document). Handle
|
|
21
|
+
# integers are per-realm, so a main-document node and a frame node can
|
|
22
|
+
# share an id — `==` includes the realm to keep them distinct.
|
|
23
|
+
@realm_id = @browser.current_realm_id
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
attr_reader :handle_id, :context_gen
|
|
26
|
+
attr_reader :handle_id, :context_gen, :realm_id
|
|
23
27
|
|
|
24
28
|
# Tick the virtual clock unconditionally on the text path. Unlike
|
|
25
29
|
# `Node#[]` (attribute reads), the text readers are NOT preceded by
|
|
@@ -211,13 +215,25 @@ module Capybara
|
|
|
211
215
|
def ==(other)
|
|
212
216
|
other.is_a?(Node) &&
|
|
213
217
|
other.handle_id == @handle_id &&
|
|
214
|
-
other.context_gen == @context_gen
|
|
218
|
+
other.context_gen == @context_gen &&
|
|
219
|
+
other.realm_id == @realm_id
|
|
215
220
|
end
|
|
216
221
|
|
|
217
222
|
private
|
|
218
223
|
|
|
219
224
|
def browser = @browser
|
|
220
|
-
def check_stale
|
|
225
|
+
def check_stale
|
|
226
|
+
# Handle integers are per-realm. If the browser is no longer switched
|
|
227
|
+
# to this node's frame (block exited, or a different frame entered),
|
|
228
|
+
# routing its handle through `dom_call` would hit the wrong realm's
|
|
229
|
+
# registry — so treat a cross-realm access as stale, matching how real
|
|
230
|
+
# browsers invalidate frame element references after `switch_to_frame`.
|
|
231
|
+
if @realm_id != browser.current_realm_id
|
|
232
|
+
raise Capybara::Simulated::StaleElement,
|
|
233
|
+
'element belongs to a different browsing context than the active frame'
|
|
234
|
+
end
|
|
235
|
+
browser.check_stale(handle_id, @initial_node, @context_gen)
|
|
236
|
+
end
|
|
221
237
|
end
|
|
222
238
|
end
|
|
223
239
|
end
|
|
@@ -68,6 +68,16 @@ module Capybara
|
|
|
68
68
|
'__csim_eventSourceClose' => ->(b, *a) { b.event_source_close(a[0]); nil },
|
|
69
69
|
'__csim_rackFetchAsync' => ->(b, *a) { b.rack_fetch_async(a[0], a[1], a[2], a[3]) },
|
|
70
70
|
'__csim_rackFetchAsyncAbort' => ->(b, *a) { b.rack_fetch_async_abort(a[0]); nil },
|
|
71
|
+
# Cross-window references (window.open / opener / postMessage). Each
|
|
72
|
+
# window is a separate VM, so these forward to the Driver to route to
|
|
73
|
+
# the target window's Browser.
|
|
74
|
+
'__csimWindowOpen' => ->(b, *a) { b.open_child_window(a[0], a[1]) },
|
|
75
|
+
'__csimWindowPostMessage' => ->(b, *a) { b.post_message_to_window(a[0], a[1], a[2]); nil },
|
|
76
|
+
'__csimWindowLocation' => ->(b, *a) { b.window_location_of(a[0]) },
|
|
77
|
+
'__csimWindowSetLocation' => ->(b, *a) { b.set_window_location(a[0], a[1]); nil },
|
|
78
|
+
'__csimWindowClosed' => ->(b, *a) { b.window_closed?(a[0]) },
|
|
79
|
+
'__csimWindowClose' => ->(b, *a) { b.close_child_window(a[0]); nil },
|
|
80
|
+
'__csimWindowOpener' => ->(b, *_) { b.opener_handle },
|
|
71
81
|
'__csim_workerSpawn' => ->(b, *a) { b.worker_spawn(a[0]) },
|
|
72
82
|
'__csim_workerPostToWorker' => ->(b, *a) { b.worker_post_to_worker(a[0], a[1]); nil },
|
|
73
83
|
'__csim_workerTerminate' => ->(b, *a) { b.worker_terminate(a[0]); nil },
|
|
@@ -26,6 +26,13 @@ begin
|
|
|
26
26
|
# media-optimization-worker hands a 317 MB raw RGBA frame from an
|
|
27
27
|
# 8900×8900 fixture through the transfer-buffer path). Match
|
|
28
28
|
# Discourse's own testem flag of 4 GB so the test fits.
|
|
29
|
+
#
|
|
30
|
+
# rusty_racer >= 0.1.4 installs a near-heap-limit callback on every isolate,
|
|
31
|
+
# so EXCEEDING this cap raises a catchable `RustyRacer::V8OutOfMemoryError`
|
|
32
|
+
# (and the isolate recovers) instead of V8 aborting the whole process with a
|
|
33
|
+
# fatal "Reached heap limit". So this value doubles as the memory backstop: a
|
|
34
|
+
# runaway script fails one example, it doesn't kill the run. No per-isolate
|
|
35
|
+
# `memory_limit` needed — the default protection catches at the cap.
|
|
29
36
|
max_old_mb = (ENV['CSIM_V8_MAX_OLD_SPACE_MB'] || '4096').to_i
|
|
30
37
|
RustyRacer::Platform.set_flags!('max-old-space-size': max_old_mb) if max_old_mb > 0
|
|
31
38
|
# `CSIM_V8_PROF=1` turns on V8's tick-sampling profiler. Output
|
|
@@ -331,6 +338,32 @@ module Capybara
|
|
|
331
338
|
result
|
|
332
339
|
end
|
|
333
340
|
|
|
341
|
+
# Route a host-fn call into a specific frame realm's context — or the
|
|
342
|
+
# main context when `realm_id` is nil/0. Each frame realm is a full
|
|
343
|
+
# bridge with its OWN handle registry + `document`, so a node / query
|
|
344
|
+
# op on a frame node (a `within_frame` body) must execute in that
|
|
345
|
+
# realm; running it in the main context would dereference the handle
|
|
346
|
+
# against the wrong registry. Callers (`Browser#dom_call`) gate on
|
|
347
|
+
# `frame_realm_alive?` first, so a disposed realm surfaces as a stale
|
|
348
|
+
# element rather than silently mis-resolving against the main registry.
|
|
349
|
+
def realm_call(realm_id, name, *args)
|
|
350
|
+
return call(name, *args) if realm_id.nil? || realm_id.zero?
|
|
351
|
+
fr = frame_realms[realm_id]
|
|
352
|
+
return call(name, *args) unless fr
|
|
353
|
+
result = fr.call(name, *args)
|
|
354
|
+
ScriptCache.warm_pending!
|
|
355
|
+
result
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Is `realm_id` a live frame realm? A frame removed / re-navigated
|
|
359
|
+
# mid-block disposes its realm (`__csim_disposeFrameRealm`) while the
|
|
360
|
+
# Browser's `@current_realm_id` may still point at it; the Browser uses
|
|
361
|
+
# this to raise a stale-element instead of running a frame handle op
|
|
362
|
+
# against the main registry.
|
|
363
|
+
def frame_realm_alive?(realm_id)
|
|
364
|
+
!(realm_id.nil? || realm_id.zero?) && frame_realms.key?(realm_id)
|
|
365
|
+
end
|
|
366
|
+
|
|
334
367
|
# bridge.js owns the virtual clock; Ruby still drives it because
|
|
335
368
|
# Capybara's polling cadence is wall-clock-anchored. Use `call`
|
|
336
369
|
# (function reference) rather than `eval` (string compile) — the
|
|
@@ -370,6 +403,21 @@ module Capybara
|
|
|
370
403
|
@frame_realms.clear
|
|
371
404
|
end
|
|
372
405
|
|
|
406
|
+
# Frame-scoped navigation: tear down the realm `old_id` and build a fresh
|
|
407
|
+
# one for the same `<iframe>` from the just-fetched document, returning the
|
|
408
|
+
# new realm's context id. A new context (not an in-place document reset) is
|
|
409
|
+
# the right model — it drops the prior frame document's timers / listeners
|
|
410
|
+
# / module state, exactly like the main page's per-visit rebuild. `parent_id`
|
|
411
|
+
# keeps the new realm's `parent`/`top` wired to the owning realm. The
|
|
412
|
+
# Browser then re-points the iframe element at the new id (`__csimRebindFrameRealm`).
|
|
413
|
+
def reload_frame_realm(old_id, parent_id, url, body, content_type)
|
|
414
|
+
if (fr = frame_realms.delete(old_id))
|
|
415
|
+
@realm_module_handles&.delete(old_id)
|
|
416
|
+
fr.dispose rescue nil
|
|
417
|
+
end
|
|
418
|
+
create_frame_realm(ctx, url, body, content_type, parent_id)
|
|
419
|
+
end
|
|
420
|
+
|
|
373
421
|
# One native microtask checkpoint — a checkpoint runs the queue until
|
|
374
422
|
# empty, and rusty already performs one at the end of every top-level
|
|
375
423
|
# eval/call (V8's default kAuto policy), so a single explicit checkpoint
|
|
@@ -536,15 +584,16 @@ module Capybara
|
|
|
536
584
|
attach_frame_realm_loader(c)
|
|
537
585
|
end
|
|
538
586
|
|
|
539
|
-
# The bridge calls `__csim_createFrameRealm(url, body, contentType)`
|
|
540
|
-
# `iframe.contentWindow`'s getter) to spin up a real per-iframe realm
|
|
587
|
+
# The bridge calls `__csim_createFrameRealm(url, body, contentType, parentId)`
|
|
588
|
+
# (from `iframe.contentWindow`'s getter) to spin up a real per-iframe realm
|
|
589
|
+
# whose `parent`/`top` point at the realm `parentId` identifies. This
|
|
541
590
|
# runs re-entrantly inside the main ctx's eval — rusty services nested
|
|
542
591
|
# requests while a host callback is in flight. Returns the realm's context
|
|
543
592
|
# id (or nil on failure — then the bridge keeps its same-realm fallback).
|
|
544
593
|
# The bridge maps `iframe.contentWindow` to `RustyRacer.contextGlobal(id)`.
|
|
545
594
|
def attach_frame_realm_loader(c)
|
|
546
|
-
c.attach('__csim_createFrameRealm', ->(url, body, content_type) {
|
|
547
|
-
RuntimeShared.safe_call { create_frame_realm(c, url, body, content_type) }
|
|
595
|
+
c.attach('__csim_createFrameRealm', ->(url, body, content_type, parent_id = 0) {
|
|
596
|
+
RuntimeShared.safe_call { create_frame_realm(c, url, body, content_type, parent_id) }
|
|
548
597
|
})
|
|
549
598
|
# Re-navigating an iframe (src/srcdoc reassigned) builds a fresh realm;
|
|
550
599
|
# the bridge calls this to tear down the superseded one so it doesn't
|
|
@@ -564,7 +613,7 @@ module Capybara
|
|
|
564
613
|
# state, point it at its own URL with the top frame as parent/top, then
|
|
565
614
|
# load its document (running its scripts in the realm). Tracked for
|
|
566
615
|
# event-loop draining + teardown.
|
|
567
|
-
def create_frame_realm(parent_ctx, url, body, content_type)
|
|
616
|
+
def create_frame_realm(parent_ctx, url, body, content_type, parent_id = 0)
|
|
568
617
|
realm = parent_ctx.create_context
|
|
569
618
|
# Re-evaling the snapshot source would redefine snapshot globals (e.g.
|
|
570
619
|
# the `scrollX` accessor) and throw — re-entrantly. Only eval the
|
|
@@ -580,12 +629,18 @@ module Capybara
|
|
|
580
629
|
attach_run_script_with_cache(realm)
|
|
581
630
|
attach_realm_esm_entry(realm)
|
|
582
631
|
reseed_realm_js(realm)
|
|
583
|
-
# Wire parent/top to the
|
|
584
|
-
#
|
|
632
|
+
# Wire `parent` / `top` to the realm that owns this iframe (its context
|
|
633
|
+
# id passed from `__csimFrameWindow`), BEFORE the frame's scripts run —
|
|
634
|
+
# `top` propagates up the chain (the main realm's `top` is itself). A
|
|
635
|
+
# nested frame thus reaches its TRUE parent, not unconditionally the
|
|
636
|
+
# main frame. `parent_id` is an integer the marshaller carries verbatim.
|
|
585
637
|
realm.eval(<<~JS)
|
|
586
638
|
if (globalThis.#{HOST_NAMESPACE_NAME} && typeof globalThis.#{HOST_NAMESPACE_NAME}.contextGlobal === 'function') {
|
|
587
|
-
var
|
|
588
|
-
|
|
639
|
+
var __parentWin = globalThis.#{HOST_NAMESPACE_NAME}.contextGlobal(#{parent_id.to_i});
|
|
640
|
+
if (__parentWin) {
|
|
641
|
+
globalThis.parent = __parentWin;
|
|
642
|
+
globalThis.top = __parentWin.top || __parentWin;
|
|
643
|
+
}
|
|
589
644
|
}
|
|
590
645
|
JS
|
|
591
646
|
# Pass the URL + document body as call ARGUMENTS, not interpolated into
|