capybara-simulated 0.7.0 → 0.9.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 +12 -10
- data/lib/capybara/simulated/browser.rb +3077 -245
- data/lib/capybara/simulated/driver.rb +85 -15
- data/lib/capybara/simulated/js/bridge.bundle.js +33716 -19156
- data/lib/capybara/simulated/node.rb +33 -7
- data/lib/capybara/simulated/quickjs_runtime.rb +74 -17
- data/lib/capybara/simulated/runtime_shared.rb +453 -34
- data/lib/capybara/simulated/v8_runtime.rb +134 -10
- data/lib/capybara/simulated/version.rb +1 -1
- metadata +2 -2
|
@@ -122,16 +122,39 @@ module Capybara
|
|
|
122
122
|
self
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
# Capybara's driver `scroll_to` contract (from Element#scroll_to): a target element + align
|
|
126
|
+
# symbol, a position symbol (`:top`/`:bottom`/`:center`), or an `[x, y]` coordinate pair.
|
|
127
|
+
# `self` is the element it was called on — the document root for a session-level scroll (routed
|
|
128
|
+
# via `find('/html')`), else an element. Drives a real scroll offset in the layout engine.
|
|
129
|
+
def scroll_to(target = nil, arg = nil, coords = nil)
|
|
130
|
+
check_stale
|
|
131
|
+
if target
|
|
132
|
+
browser.scroll_to(handle_id, target.handle_id, arg)
|
|
133
|
+
elsif coords
|
|
134
|
+
browser.scroll_to(handle_id, nil, nil, coords[0], coords[1])
|
|
135
|
+
elsif arg
|
|
136
|
+
browser.scroll_to(handle_id, nil, arg)
|
|
137
|
+
end
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Capybara's `Element#scroll_to(:current, offset: [dx, dy])` calls this after the position
|
|
142
|
+
# handling above — a relative scroll from wherever the element is now.
|
|
143
|
+
def scroll_by(dx, dy)
|
|
144
|
+
check_stale
|
|
145
|
+
browser.scroll_by(handle_id, dx, dy)
|
|
146
|
+
self
|
|
147
|
+
end
|
|
126
148
|
|
|
127
149
|
# Capybara's standard rect API. No layout engine — but
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
150
|
+
# The element's coarse border-box (viewport-relative), from the layout engine — backs the
|
|
151
|
+
# spatial selectors (`:above`/`:below`/`:near`) and coordinate drag. Deterministic per layout
|
|
152
|
+
# generation (memoised; only a DOM/style mutation changes it), so Discourse's
|
|
153
|
+
# `wait_for_animation` — which polls `element.rect[:x]` twice and waits for it to stabilise —
|
|
154
|
+
# still settles immediately when nothing is animating.
|
|
132
155
|
def rect
|
|
133
156
|
check_stale
|
|
134
|
-
|
|
157
|
+
browser.rect(handle_id)
|
|
135
158
|
end
|
|
136
159
|
|
|
137
160
|
def send_keys(*keys)
|
|
@@ -195,7 +218,10 @@ module Capybara
|
|
|
195
218
|
def selected? = browser.option_selected?(handle_id)
|
|
196
219
|
def checked? = !!self['checked']
|
|
197
220
|
def readonly? = !!self['readonly']
|
|
198
|
-
def obscured?(*)
|
|
221
|
+
def obscured?(*)
|
|
222
|
+
check_stale
|
|
223
|
+
browser.obscured?(handle_id)
|
|
224
|
+
end
|
|
199
225
|
def synchronize(*) = yield
|
|
200
226
|
def style(names = [])
|
|
201
227
|
check_stale
|
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
|
|
14
14
|
require 'digest'
|
|
15
15
|
require 'quickjs'
|
|
16
|
+
begin
|
|
17
|
+
# Intl moved out of the quickjs gem in 0.19. bridge.js patches Intl.DateTimeFormat, so the
|
|
18
|
+
# companion gem is required for the QuickJS engine — say so plainly rather than letting a bare
|
|
19
|
+
# LoadError name a file the user never asked for.
|
|
20
|
+
require 'quickjs-polyfill-intl/datetimeformat'
|
|
21
|
+
rescue LoadError
|
|
22
|
+
raise LoadError, <<~MSG
|
|
23
|
+
capybara-simulated's QuickJS engine needs the `quickjs-polyfill-intl` gem
|
|
24
|
+
(quickjs 0.19 moved Intl out of the core gem, and the DOM bridge uses
|
|
25
|
+
Intl.DateTimeFormat). Add it next to quickjs:
|
|
26
|
+
|
|
27
|
+
gem 'quickjs', '>= 0.19'
|
|
28
|
+
gem 'quickjs-polyfill-intl'
|
|
29
|
+
|
|
30
|
+
Or use the V8 engine (`gem 'rusty_racer'`), which has ICU built in.
|
|
31
|
+
MSG
|
|
32
|
+
end
|
|
16
33
|
|
|
17
34
|
require_relative 'runtime_shared'
|
|
18
35
|
require_relative 'worker_runtime'
|
|
@@ -150,6 +167,19 @@ module Capybara
|
|
|
150
167
|
normalize(result)
|
|
151
168
|
end
|
|
152
169
|
|
|
170
|
+
# QuickJS has no per-frame realms (iframes share the one VM): `within_frame` falls back to
|
|
171
|
+
# the same realm, and any realm id routes to the single global context.
|
|
172
|
+
def supports_frames? = false
|
|
173
|
+
|
|
174
|
+
def realm_call(_realm_id, name, *args) = call(name, *args)
|
|
175
|
+
|
|
176
|
+
# No per-frame realms, so no realm is ever "alive" as a distinct browsing context —
|
|
177
|
+
# every client id resolves to 'client-window' (the single global). Defined so a
|
|
178
|
+
# caller that gates realm routing on it (deliver_worker_messages) works on both engines.
|
|
179
|
+
def frame_realm_alive?(_realm_id) = false
|
|
180
|
+
|
|
181
|
+
def frame_realm_ids = []
|
|
182
|
+
|
|
153
183
|
# bridge.js owns the virtual clock; we drive it from Ruby because
|
|
154
184
|
# Capybara's polling cadence is wall-clock-anchored.
|
|
155
185
|
def drain_timers(max_ms = nil)
|
|
@@ -216,28 +246,44 @@ module Capybara
|
|
|
216
246
|
# already the inter-test reset point.
|
|
217
247
|
def reset_page = rebuild_ctx
|
|
218
248
|
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
249
|
+
# PERMANENTLY drop this runtime. Distinct from `rebuild_ctx` above, which
|
|
250
|
+
# deliberately does NOT `dispose!` — that runs per visit, on every example,
|
|
251
|
+
# and `dispose!` blocks on the quickjs GC with the GVL held. Here it runs
|
|
252
|
+
# once, when a session is dropped for good, and the cost is the point.
|
|
253
|
+
#
|
|
254
|
+
# This used to be intentionally absent, on the reasoning that Ruby GC's
|
|
255
|
+
# dfree would reach an unreferenced `@vm` on its own. Measured, it does not
|
|
256
|
+
# in time to matter: 30 sessions created and dropped in a loop held 1979 MB
|
|
257
|
+
# with no dispose and 1971 MB with one, because `Browser#dispose` gates on
|
|
258
|
+
# `respond_to?(:dispose)` and so did nothing at all here. The suite's peak
|
|
259
|
+
# RSS is what pays for it.
|
|
260
|
+
#
|
|
261
|
+
# The old note's hazard — a stray post-close `eval`/`call` finding `@vm`
|
|
262
|
+
# nil — is real, so this leaves `@vm` in place and lets the gem's own
|
|
263
|
+
# freed-VM handling answer; `Driver#disposed?` keeps the live-driver walk
|
|
264
|
+
# from stepping a dropped runtime in the first place.
|
|
265
|
+
def dispose
|
|
266
|
+
@vm&.dispose!
|
|
267
|
+
rescue StandardError
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
227
270
|
|
|
228
271
|
# bridge.js patches `Intl.DateTimeFormat`; rusty_racer ships ICU built-in but
|
|
229
272
|
# QuickJS gates it behind a polyfill flag (other surfaces bridge.js touches —
|
|
230
273
|
# URL / TextEncoder / atob/btoa / crypto — already route through Ruby host fns,
|
|
231
274
|
# so POLYFILL_INTL is the only one we strictly need).
|
|
232
275
|
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
|
|
276
|
+
# Since 0.19 the Intl polyfills live in the separate `quickjs-polyfill-intl`
|
|
277
|
+
# gem, registered by name instead of quickjs's old bundled `POLYFILL_INTL`
|
|
278
|
+
# flag. `:polyfill_intl_datetimeformat_all` is the whole DateTimeFormat chain
|
|
279
|
+
# (getcanonicallocales → locale → pluralrules → numberformat → datetimeformat)
|
|
280
|
+
# as ONE registration — the only chain bridge.js needs, and cheaper than
|
|
281
|
+
# requiring the five links separately.
|
|
282
|
+
#
|
|
283
|
+
# PERF (rule 3): the polyfill source is eval'd per VM, and the VM-pool pre-warm
|
|
284
|
+
# used to be GVL-serial, which put every feature on the critical path. quickjs
|
|
285
|
+
# now releases the GVL while loading polyfills, so the pre-warm overlaps.
|
|
286
|
+
INTL_FEATURES = [:polyfill_intl_datetimeformat_all].freeze
|
|
241
287
|
#
|
|
242
288
|
# `max_stack_size: 0` — `JS_SetMaxStackSize` measures C stack
|
|
243
289
|
# delta from runtime construction; Ruby callers reach QuickJS
|
|
@@ -352,11 +398,22 @@ module Capybara
|
|
|
352
398
|
# host fns attached *after* the replay (so snapshot_stubs.js's
|
|
353
399
|
# no-ops don't overwrite real ones), `__csim_isWorker` set, +
|
|
354
400
|
# the per-worker postMessage routed through `post_back`.
|
|
355
|
-
def self.build_worker(browser, post_back)
|
|
401
|
+
def self.build_worker(browser, post_back, broadcast_out = nil, sw_hooks = {})
|
|
356
402
|
vm = Quickjs::VM.new(**VM_OPTIONS)
|
|
357
403
|
bridge_runnable.run(on: vm)
|
|
358
404
|
attach_host_fns(vm, browser)
|
|
359
405
|
vm.define_function('__csim_workerPostMessage') {|data| post_back.call(data); nil }
|
|
406
|
+
# A worker's BroadcastChannel fan-out routes through the thread-safe outbox, not
|
|
407
|
+
# `browser.broadcast_to_windows` (cross-thread inbox mutation). See v8_runtime#build_worker.
|
|
408
|
+
vm.define_function('__csimBroadcast') {|name, data, _rid, origin| broadcast_out&.call(name, data, origin); nil } if broadcast_out
|
|
409
|
+
# Service-worker → main-thread signals via the outbox (see v8_runtime#build_worker).
|
|
410
|
+
vm.define_function('__csim_swPostToClient') {|client_id, data| sw_hooks[:post_to_client]&.call(client_id, data); nil } if sw_hooks[:post_to_client]
|
|
411
|
+
vm.define_function('__csim_swFocusClient') {|client_id| sw_hooks[:focus_client]&.call(client_id); nil } if sw_hooks[:focus_client]
|
|
412
|
+
vm.define_function('__csim_swClaim') { sw_hooks[:claim]&.call; nil } if sw_hooks[:claim]
|
|
413
|
+
vm.define_function('__csim_swFetchRespond') {|fetch_id, resp, realm_id| sw_hooks[:fetch_respond]&.call(fetch_id, resp, realm_id); nil } if sw_hooks[:fetch_respond]
|
|
414
|
+
vm.define_function('__csim_swFetchStream') {|fetch_id, kind, payload, realm_id| sw_hooks[:fetch_stream]&.call(fetch_id, kind, payload, realm_id); nil } if sw_hooks[:fetch_stream]
|
|
415
|
+
vm.define_function('__csim_workerPortPost') {|channel, data| sw_hooks[:port_post]&.call(channel, data); nil } if sw_hooks[:port_post]
|
|
416
|
+
vm.define_function('__csim_workerPortEndpoint') {|channel| sw_hooks[:port_endpoint]&.call(channel); nil } if sw_hooks[:port_endpoint]
|
|
360
417
|
# Override main's __setTimersActive so worker's empty-timer-map
|
|
361
418
|
# flip doesn't race main's `polling?` gate. See v8_runtime's
|
|
362
419
|
# build_worker for the long-form rationale.
|