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
|
@@ -73,7 +73,8 @@ module Capybara
|
|
|
73
73
|
@cookies = {}
|
|
74
74
|
@local_storage = {}
|
|
75
75
|
@browser = build_window_browser
|
|
76
|
-
@
|
|
76
|
+
@browser.window_handle = PRIMARY_HANDLE
|
|
77
|
+
@aux_windows = [] # [{handle:, browser:, name:, opener:}, …]
|
|
77
78
|
@active_handle = nil
|
|
78
79
|
@next_window_seq = 0
|
|
79
80
|
@owner_thread = Thread.current
|
|
@@ -227,6 +228,15 @@ module Capybara
|
|
|
227
228
|
current_browser.find_css(query).map {|id| Node.new(self, id) }
|
|
228
229
|
end
|
|
229
230
|
|
|
231
|
+
# Capybara `within_frame` / `switch_to_frame`. `frame` is the iframe
|
|
232
|
+
# `Capybara::Node::Element` (its `.native` is our driver Node), or the
|
|
233
|
+
# `:parent` / `:top` symbols. The block's finds + actions then route into
|
|
234
|
+
# the frame's own V8 realm via the Browser's `@current_realm_id`.
|
|
235
|
+
def switch_to_frame(frame)
|
|
236
|
+
target = frame.is_a?(Symbol) ? frame : frame.native.handle_id
|
|
237
|
+
current_browser.switch_to_frame(target)
|
|
238
|
+
end
|
|
239
|
+
|
|
230
240
|
# Per-window Browser/VM. `open_aux_window` creates a fresh
|
|
231
241
|
# Browser sharing the Driver's cookie + localStorage jars
|
|
232
242
|
# (origin-shared in real browsers) and visits the target URL;
|
|
@@ -238,30 +248,99 @@ module Capybara
|
|
|
238
248
|
def window_handles
|
|
239
249
|
[PRIMARY_HANDLE] + @aux_windows.map {|w| w[:handle] }
|
|
240
250
|
end
|
|
241
|
-
|
|
251
|
+
|
|
252
|
+
# All window entries (primary + aux) as `{handle:, browser:, name:, opener:}`.
|
|
253
|
+
private def window_entries
|
|
254
|
+
[{handle: PRIMARY_HANDLE, browser: @browser, name: '', opener: nil}] + @aux_windows
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The Browser backing a handle, or nil if the window is closed/unknown.
|
|
258
|
+
def window_browser(handle)
|
|
259
|
+
window_entries.find {|w| w[:handle] == handle }&.fetch(:browser)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Open (or, by `name`, reuse) an auxiliary window. `target="_blank"`
|
|
263
|
+
# clicks and `window.open` both land here. A non-empty `name` that
|
|
264
|
+
# matches an existing window navigates that window instead of opening a
|
|
265
|
+
# new one (HTML window-name targeting); `opener_handle` records the
|
|
266
|
+
# opener so the new window's `window.opener` resolves back to it.
|
|
267
|
+
def open_aux_window(url = nil, name: nil, opener_handle: nil)
|
|
268
|
+
name = name.to_s
|
|
269
|
+
if !name.empty? && (existing = @aux_windows.find {|w| w[:name] == name })
|
|
270
|
+
navigate_window(existing[:browser], url)
|
|
271
|
+
return existing[:handle]
|
|
272
|
+
end
|
|
242
273
|
@next_window_seq += 1
|
|
243
274
|
handle = "csim-window-#{@next_window_seq}"
|
|
244
275
|
aux = build_window_browser
|
|
276
|
+
aux.window_handle = handle
|
|
277
|
+
# Register BEFORE visiting: the opened document's own boot scripts read
|
|
278
|
+
# `window.opener`, which resolves through this entry — so the entry
|
|
279
|
+
# (with its opener) must exist before `visit` runs those scripts.
|
|
280
|
+
@aux_windows << {handle: handle, browser: aux, name: name, opener: opener_handle}
|
|
245
281
|
aux.visit(url) if url && !url.empty?
|
|
246
|
-
@aux_windows << {handle: handle, browser: aux}
|
|
247
282
|
handle
|
|
248
283
|
rescue StandardError => e
|
|
249
|
-
# Aux window URL-load failure (binary content, network error,
|
|
250
|
-
#
|
|
284
|
+
# Aux window URL-load failure (binary content, network error, …)
|
|
285
|
+
# shouldn't tear down the test — the handle is already recorded so
|
|
251
286
|
# `window_opened_by` succeeds; within_window assertions on
|
|
252
|
-
# `current_url` may still pass through whatever `visit`
|
|
253
|
-
#
|
|
287
|
+
# `current_url` may still pass through whatever `visit` managed to set
|
|
288
|
+
# before raising.
|
|
254
289
|
warn "[csim] open_aux_window(#{url.inspect}) raised: #{e.class}: #{e.message[0, 200]}"
|
|
255
|
-
@aux_windows << {handle: handle, browser: aux}
|
|
256
290
|
handle
|
|
257
291
|
end
|
|
258
292
|
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
293
|
+
# ── JS-facing window routing (called via Browser host fns) ──────
|
|
294
|
+
# Each window is a separate Browser/VM, so a cross-window reference is a
|
|
295
|
+
# proxy that forwards here; the Driver routes to the target Browser.
|
|
296
|
+
|
|
297
|
+
# `window.open(url, name)` from the `opener` window's JS. Resolves the URL
|
|
298
|
+
# against the opener's document and records the opener relationship.
|
|
299
|
+
def open_window_from_js(opener_browser, url, name)
|
|
300
|
+
resolved = url.to_s.empty? ? nil : opener_browser.resolve_document_url(url)
|
|
301
|
+
open_aux_window(resolved, name: name, opener_handle: handle_for(opener_browser))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# `targetWindow.postMessage(data, origin)` — queue on the target window's
|
|
305
|
+
# Browser, tagged with the source window's handle.
|
|
306
|
+
def window_post_message(source_browser, target_handle, data, _origin)
|
|
307
|
+
target = window_browser(target_handle) or return
|
|
308
|
+
target.enqueue_window_message(data, _origin, handle_for(source_browser))
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def window_location(handle) = (window_browser(handle)&.current_url).to_s
|
|
312
|
+
def window_set_location(handle, url)
|
|
313
|
+
b = window_browser(handle) or return
|
|
314
|
+
navigate_window(b, b.resolve_document_url(url))
|
|
315
|
+
end
|
|
316
|
+
def window_closed?(handle) = window_browser(handle).nil?
|
|
317
|
+
def opener_handle_of(browser)
|
|
318
|
+
handle = handle_for(browser)
|
|
319
|
+
window_entries.find {|w| w[:handle] == handle }&.fetch(:opener)
|
|
320
|
+
end
|
|
321
|
+
private def handle_for(browser) = browser.window_handle
|
|
322
|
+
|
|
323
|
+
# Navigate an existing window. If it's the window whose JS is currently
|
|
324
|
+
# executing — a self-targeted `window.open(url, ownName)` or
|
|
325
|
+
# `someProxyToSelf.location = …` — DEFER via the location-assign queue:
|
|
326
|
+
# navigating it synchronously (`visit` → `rebuild_ctx`) would dispose the
|
|
327
|
+
# V8 context mid-call and abort the running handler. A non-active window
|
|
328
|
+
# can navigate immediately (its VM isn't on the stack).
|
|
329
|
+
private def navigate_window(browser, url)
|
|
330
|
+
return if url.nil? || url.to_s.empty?
|
|
331
|
+
if browser.equal?(current_browser)
|
|
332
|
+
browser.location_assign(url)
|
|
333
|
+
else
|
|
334
|
+
browser.visit(url)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Capybara `Session#open_new_window(:tab)` entry point — opens at
|
|
339
|
+
# `about:blank` (so `current_url`/title match a real new tab) and the
|
|
340
|
+
# test then `switch_to_window` + `visit`s the real URL. We don't
|
|
341
|
+
# distinguish `:tab` from `:window` (no window-chrome semantics here).
|
|
263
342
|
def open_new_window(_kind = :tab)
|
|
264
|
-
open_aux_window
|
|
343
|
+
open_aux_window('about:blank')
|
|
265
344
|
end
|
|
266
345
|
def window_size(_) = [current_browser.viewport_width, current_browser.viewport_height]
|
|
267
346
|
def close_window(h)
|
|
@@ -9,5 +9,12 @@ module Capybara
|
|
|
9
9
|
# so Capybara's `synchronize` wrapper catches it and reloads the
|
|
10
10
|
# cached element.
|
|
11
11
|
class StaleElement < Capybara::ElementNotFound; end
|
|
12
|
+
|
|
13
|
+
# Raised by `switch_to_frame` when the active JS engine can't give the
|
|
14
|
+
# target `<iframe>` its own browsing context (a real per-frame realm).
|
|
15
|
+
# Only the V8 engine (rusty_racer) builds per-frame realms; under
|
|
16
|
+
# QuickJS the frame stays a same-realm fallback we can't route DOM ops
|
|
17
|
+
# into, so `within_frame` is unsupported there.
|
|
18
|
+
class FrameNotSupported < Capybara::NotSupportedByDriverError; end
|
|
12
19
|
end
|
|
13
20
|
end
|