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.
@@ -73,7 +73,8 @@ module Capybara
73
73
  @cookies = {}
74
74
  @local_storage = {}
75
75
  @browser = build_window_browser
76
- @aux_windows = [] # [{handle:, browser:}, …]
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
- def open_aux_window(url = nil)
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
- # …) shouldn't tear down the test — record the handle so
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
- # managed to set before raising.
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
- # Capybara `Session#open_new_window(:tab)` entry point visits
260
- # `about:blank` so the test can `switch_to_window` then `visit`
261
- # the real URL. We don't distinguish `:tab` from `:window` (no
262
- # window-chrome semantics in this driver).
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