capybara-simulated 0.0.7 → 0.1.1
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 +303 -158
- data/lib/capybara/simulated/asset_cache.rb +232 -0
- data/lib/capybara/simulated/browser.rb +3409 -845
- data/lib/capybara/simulated/driver.rb +341 -134
- data/lib/capybara/simulated/errors.rb +9 -5
- data/lib/capybara/simulated/js/bridge.bundle.js +19738 -0
- data/lib/capybara/simulated/js/snapshot_stubs.js +110 -0
- data/lib/capybara/simulated/node.rb +151 -163
- data/lib/capybara/simulated/quickjs_runtime.rb +424 -0
- data/lib/capybara/simulated/runtime_shared.rb +183 -0
- data/lib/capybara/simulated/script_cache.rb +168 -0
- data/lib/capybara/simulated/sourcemap.rb +119 -0
- data/lib/capybara/simulated/stack_resolver.rb +97 -0
- data/lib/capybara/simulated/trace.rb +111 -0
- data/lib/capybara/simulated/v8_runtime.rb +987 -0
- data/lib/capybara/simulated/version.rb +3 -1
- data/lib/capybara/simulated/webauthn_state.rb +367 -0
- data/lib/capybara/simulated/whitespace_normalizer.rb +45 -0
- data/lib/capybara/simulated/worker_runtime.rb +30 -0
- data/lib/capybara/simulated.rb +31 -4
- data/lib/capybara-simulated.rb +2 -0
- data/vendor/js/vendor.bundle.js +13 -0
- metadata +24 -32
- data/vendor/esbuild-wasm/LICENSE.md +0 -21
- data/vendor/esbuild-wasm/bin/esbuild +0 -91
- data/vendor/esbuild-wasm/esbuild.wasm +0 -0
- data/vendor/esbuild-wasm/lib/main.js +0 -2337
- data/vendor/esbuild-wasm/wasm_exec.js +0 -575
- data/vendor/esbuild-wasm/wasm_exec_node.js +0 -40
- data/vendor/js/bundle-modules.mjs +0 -168
- data/vendor/js/csim.bundle.js +0 -91560
- data/vendor/js/entry.mjs +0 -23
- data/vendor/js/prelude.js +0 -190
- data/vendor/js/runtime.js +0 -2208
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Stub host fns so bridge.js can be baked into a Snapshot (V8) /
|
|
2
|
+
// compiled to bytecode (QuickJS) without the real Ruby-side hosts
|
|
3
|
+
// being attached yet. Real implementations land at Context build
|
|
4
|
+
// time via the engine's attach API. Only the host fns bridge.js
|
|
5
|
+
// actually invokes appear here — if you add a new host fn, add the
|
|
6
|
+
// matching stub here too.
|
|
7
|
+
Object.defineProperty(globalThis, Symbol.toStringTag, { value: 'Window' });
|
|
8
|
+
|
|
9
|
+
// The vendored whatwg-url engine captures `SharedArrayBuffer` and a
|
|
10
|
+
// `TextEncoder`/`TextDecoder` at module-load time = snapshot-BUILD time here —
|
|
11
|
+
// and the V8 snapshot context exposes neither (SharedArrayBuffer is
|
|
12
|
+
// Spectre-disabled; our real TextEncoder lives in bridge's encoding.js, which
|
|
13
|
+
// loads AFTER the vendor bundle). Provide them before the vendor bundle so
|
|
14
|
+
// whatwg-url bakes cleanly. SharedArrayBuffer is only touched by whatwg-url's
|
|
15
|
+
// (unused-for-URL) WebIDL buffer-type guard, so aliasing it to ArrayBuffer is
|
|
16
|
+
// fine. The TextEncoder/TextDecoder MUST be real UTF-8 — whatwg-url captures
|
|
17
|
+
// them in a module closure and uses them for percent-encoding at runtime (the
|
|
18
|
+
// bridge's full encoding.js later overrides `globalThis.TextEncoder` for app
|
|
19
|
+
// code, but whatwg-url keeps the instance it captured here).
|
|
20
|
+
globalThis.SharedArrayBuffer = globalThis.SharedArrayBuffer || globalThis.ArrayBuffer;
|
|
21
|
+
if (typeof globalThis.TextEncoder === 'undefined') {
|
|
22
|
+
globalThis.TextEncoder = class TextEncoder {
|
|
23
|
+
get encoding() { return 'utf-8'; }
|
|
24
|
+
encode(str) {
|
|
25
|
+
str = String(str === undefined ? '' : str);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (let i = 0; i < str.length; i++) {
|
|
28
|
+
let c = str.codePointAt(i);
|
|
29
|
+
if (c > 0xffff) i++; // surrogate pair — skip the low half
|
|
30
|
+
else if (c >= 0xd800 && c <= 0xdfff) c = 0xfffd; // unpaired surrogate → U+FFFD (WHATWG)
|
|
31
|
+
if (c < 0x80) out.push(c);
|
|
32
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
33
|
+
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
34
|
+
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
35
|
+
}
|
|
36
|
+
return new Uint8Array(out);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (typeof globalThis.TextDecoder === 'undefined') {
|
|
41
|
+
globalThis.TextDecoder = class TextDecoder {
|
|
42
|
+
constructor() {}
|
|
43
|
+
get encoding() { return 'utf-8'; }
|
|
44
|
+
decode(input) {
|
|
45
|
+
if (!input) return '';
|
|
46
|
+
const b = input instanceof Uint8Array ? input : new Uint8Array(input.buffer || input);
|
|
47
|
+
let s = '';
|
|
48
|
+
for (let i = 0; i < b.length;) {
|
|
49
|
+
let c = b[i++];
|
|
50
|
+
if (c >= 0xf0) c = ((c & 0x07) << 18) | ((b[i++] & 0x3f) << 12) | ((b[i++] & 0x3f) << 6) | (b[i++] & 0x3f);
|
|
51
|
+
else if (c >= 0xe0) c = ((c & 0x0f) << 12) | ((b[i++] & 0x3f) << 6) | (b[i++] & 0x3f);
|
|
52
|
+
else if (c >= 0xc0) c = ((c & 0x1f) << 6) | (b[i++] & 0x3f);
|
|
53
|
+
s += String.fromCodePoint(c);
|
|
54
|
+
}
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
globalThis.__csim_parseUrl = function (input, base) {
|
|
60
|
+
return {
|
|
61
|
+
href: 'http://placeholder/', protocol: 'http:',
|
|
62
|
+
username: '', password: '', host: 'placeholder',
|
|
63
|
+
hostname: 'placeholder', port: '', pathname: '/',
|
|
64
|
+
search: '', hash: '', origin: 'http://placeholder'
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
globalThis.__csim_randomUUID = function () { return '00000000-0000-0000-0000-000000000000'; };
|
|
68
|
+
globalThis.__csim_randomBytes = function (n) { return new Array(n).fill(0); };
|
|
69
|
+
globalThis.__csim_atob = function (s) { return ''; };
|
|
70
|
+
globalThis.__csim_btoa = function (s) { return ''; };
|
|
71
|
+
globalThis.__csim_utf8Encode = function (s) { return []; };
|
|
72
|
+
globalThis.__csim_utf8Decode = function (a) { return ''; };
|
|
73
|
+
globalThis.__rackFetch = function () { return null; };
|
|
74
|
+
globalThis.__locationAssign = function () { return null; };
|
|
75
|
+
globalThis.__locationReload = function () { return null; };
|
|
76
|
+
globalThis.__setTimersActive = function () { return null; };
|
|
77
|
+
globalThis.__setCurrentUrl = function () { return null; };
|
|
78
|
+
globalThis.__pushHistoryEntry = function () { return null; };
|
|
79
|
+
globalThis.__historyLength = function () { return 1; };
|
|
80
|
+
globalThis.__csimReadFilePick = function () { return null; };
|
|
81
|
+
globalThis.__getDocumentCookie = function () { return ''; };
|
|
82
|
+
globalThis.__setDocumentCookie = function () { return null; };
|
|
83
|
+
globalThis.__csim_storageGet = function () { return null; };
|
|
84
|
+
globalThis.__csim_storageSet = function () { return null; };
|
|
85
|
+
globalThis.__csim_storageRemove = function () { return null; };
|
|
86
|
+
globalThis.__csim_storageClear = function () { return null; };
|
|
87
|
+
globalThis.__csim_storageKey = function () { return null; };
|
|
88
|
+
globalThis.__csim_storageLength = function () { return 0; };
|
|
89
|
+
// Geolocation override state; JSON string ('null' = no override configured).
|
|
90
|
+
globalThis.__csimGeolocationState = function () { return 'null'; };
|
|
91
|
+
globalThis.__modalDialog = function () { return null; };
|
|
92
|
+
globalThis.__csim_pushImportmap = function () { return null; };
|
|
93
|
+
globalThis.__csim_logConsole = function () { return null; };
|
|
94
|
+
globalThis.__csim_eventSourceOpen = function () { return 0; };
|
|
95
|
+
globalThis.__csim_eventSourceClose = function () { return null; };
|
|
96
|
+
globalThis.__csim_workerSpawn = function () { return 0; };
|
|
97
|
+
globalThis.__csim_workerPostToWorker= function () { return null; };
|
|
98
|
+
globalThis.__csim_workerTerminate = function () { return null; };
|
|
99
|
+
globalThis.__csim_workerPostMessage = function () { return null; };
|
|
100
|
+
globalThis.__csim_decodeImage = function () { return null; };
|
|
101
|
+
globalThis.__csim_blobRegister = function () { return null; };
|
|
102
|
+
globalThis.__csim_blobResolve = function () { return null; };
|
|
103
|
+
globalThis.__csim_blobUnregister = function () { return null; };
|
|
104
|
+
// Default classic-script runner: indirect-eval at global scope. Both
|
|
105
|
+
// runtimes override this with a bytecode-caching host fn at attach
|
|
106
|
+
// time. `//# sourceURL=…` labels eval'd content so stack traces
|
|
107
|
+
// report the script URL instead of `<anonymous>`.
|
|
108
|
+
globalThis.__csim_runScript = function (label, body) {
|
|
109
|
+
(0, eval)(body + '\n//# sourceURL=' + (label || 'csim-eval'));
|
|
110
|
+
};
|
|
@@ -1,235 +1,223 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'capybara/node/base'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
require_relative 'whitespace_normalizer'
|
|
3
6
|
|
|
4
7
|
module Capybara
|
|
5
8
|
module Simulated
|
|
6
9
|
class Node < Capybara::Driver::Node
|
|
7
|
-
include
|
|
8
|
-
|
|
9
|
-
def
|
|
10
|
-
|
|
10
|
+
include WhitespaceNormalizer
|
|
11
|
+
|
|
12
|
+
def initialize(driver, handle)
|
|
13
|
+
super(driver, self)
|
|
14
|
+
@handle_id = handle
|
|
15
|
+
# Pin the Browser at construction so nodes from one window
|
|
16
|
+
# stay valid even after `switch_to_window` flips to another.
|
|
17
|
+
@browser = driver.current_browser
|
|
18
|
+
@initial_node = @browser.lookup_node(handle)
|
|
19
|
+
@context_gen = @browser.context_gen
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :handle_id, :context_gen
|
|
23
|
+
|
|
24
|
+
# Tick the virtual clock unconditionally on the text path. Unlike
|
|
25
|
+
# `Node#[]` (attribute reads), the text readers are NOT preceded by
|
|
26
|
+
# a `find_css` that ticks under the wall throttle: `have_text` /
|
|
27
|
+
# `assert_text` polls `.text` directly against an already-found,
|
|
28
|
+
# cached scope node, so the find loop short-circuits without
|
|
29
|
+
# re-ticking. Gating these behind `timer_wait_elapsed?` therefore
|
|
30
|
+
# stalls a pure text-poll loop's virtual clock and scheduled
|
|
31
|
+
# `setTimeout`s never fire (smoke_spec virtual-clock contract).
|
|
32
|
+
# They run ~once per poll-scope (not once per matched result like
|
|
33
|
+
# the attribute filters the audit targeted), so they are not the
|
|
34
|
+
# O(N) hot path and are safe to tick every call.
|
|
11
35
|
def all_text
|
|
36
|
+
browser.tick_real_time
|
|
37
|
+
check_stale
|
|
12
38
|
normalize_spacing(browser.all_text(handle_id))
|
|
13
39
|
end
|
|
14
40
|
|
|
15
41
|
def visible_text
|
|
42
|
+
browser.tick_real_time
|
|
43
|
+
check_stale
|
|
16
44
|
normalize_visible_spacing(browser.visible_text(handle_id))
|
|
17
45
|
end
|
|
18
46
|
|
|
19
|
-
def [](name)
|
|
20
|
-
case name.to_s
|
|
21
|
-
when 'value' then value
|
|
22
|
-
when 'checked' then checked? ? 'true' : nil
|
|
23
|
-
when 'selected' then selected? ? 'true' : nil
|
|
24
|
-
else browser.attr(handle_id, name)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
47
|
def value
|
|
48
|
+
check_stale
|
|
29
49
|
browser.value(handle_id)
|
|
30
50
|
end
|
|
31
51
|
|
|
32
|
-
def set(value, **_opts)
|
|
33
|
-
return if disabled?
|
|
34
|
-
# readonly is meaningless on checkbox/radio; the HTML spec ignores
|
|
35
|
-
# it for those types, and Capybara's specs explicitly assert that.
|
|
36
|
-
type = (self[:type] || tag_name || '').to_s.downcase
|
|
37
|
-
return if readonly? && type != 'checkbox' && type != 'radio'
|
|
38
|
-
browser.set_value(handle_id, value)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def select_option
|
|
42
|
-
return if disabled?
|
|
43
|
-
browser.select_option(handle_id)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def unselect_option
|
|
47
|
-
raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node_multiple?
|
|
48
|
-
browser.unselect_option(handle_id)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def tag_name
|
|
52
|
-
browser.tag_name(handle_id)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
52
|
def visible?
|
|
53
|
+
check_stale
|
|
56
54
|
browser.visible?(handle_id)
|
|
57
55
|
end
|
|
58
56
|
|
|
59
|
-
def
|
|
60
|
-
false
|
|
61
|
-
end
|
|
57
|
+
def tag_name = browser.tag_name(handle_id)
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
# Convenience accessors mirroring real browser nodes — Discourse
|
|
60
|
+
# tests reach for `.native.inner_html` (e.g. reviewables XSS
|
|
61
|
+
# checks); without this method `.native` (= self) raised
|
|
62
|
+
# NoMethodError.
|
|
63
|
+
def inner_html
|
|
64
|
+
check_stale
|
|
65
|
+
browser.inner_html(handle_id)
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
def
|
|
68
|
-
|
|
68
|
+
def outer_html
|
|
69
|
+
check_stale
|
|
70
|
+
browser.outer_html(handle_id)
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
def [](name)
|
|
74
|
+
# Tick the virtual clock so Capybara's helpers that poll an
|
|
75
|
+
# attribute in a tight `sleep(0.1) until` loop (e.g. Avo's
|
|
76
|
+
# `wait_for_body_class_missing`) actually see the JS chain
|
|
77
|
+
# progress. Without this the body class never transitions
|
|
78
|
+
# because we only advance time during `find`, and the helper
|
|
79
|
+
# caches the body node before its poll loop.
|
|
80
|
+
#
|
|
81
|
+
# Gated behind the same wall-clock throttle `find_css` uses
|
|
82
|
+
# (`timer_wait_elapsed?`): on framework-runloop pages that keep
|
|
83
|
+
# `@timers_active` permanently true, an ungated tick here would
|
|
84
|
+
# drain timers on every per-result attribute filter / poll
|
|
85
|
+
# iteration. The throttle still ticks the first time and once
|
|
86
|
+
# per ~50 ms window thereafter, so poll loops that wait for a
|
|
87
|
+
# timer to fire between reads still make progress.
|
|
88
|
+
#
|
|
89
|
+
# `tick_real_time` also drains the Worker / EventSource / hijacked
|
|
90
|
+
# -fetch outboxes, so an attribute poll whose value is delivered
|
|
91
|
+
# only by one of those channels (with no active timer) would
|
|
92
|
+
# otherwise never see it. `async_io_pending?` is an O(1) gate
|
|
93
|
+
# (three empty? checks) that lets those cases drain without paying
|
|
94
|
+
# an unconditional tick on timer-driven runloop pages.
|
|
95
|
+
browser.tick_real_time if browser.timer_wait_elapsed? || browser.async_io_pending?
|
|
96
|
+
check_stale
|
|
97
|
+
browser.attr(handle_id, name.to_s)
|
|
81
98
|
end
|
|
82
99
|
|
|
83
100
|
def click(keys = [], **opts)
|
|
84
|
-
|
|
101
|
+
check_stale
|
|
102
|
+
browser.click(handle_id, keys, **opts)
|
|
85
103
|
end
|
|
86
104
|
|
|
87
105
|
def right_click(keys = [], **opts)
|
|
88
|
-
|
|
106
|
+
check_stale
|
|
107
|
+
browser.right_click(handle_id, keys, **opts)
|
|
89
108
|
end
|
|
90
109
|
|
|
91
110
|
def double_click(keys = [], **opts)
|
|
92
|
-
|
|
111
|
+
check_stale
|
|
112
|
+
browser.double_click(handle_id, keys, **opts)
|
|
93
113
|
end
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# x/y from Capybara are *offsets* — from top-left when
|
|
98
|
-
# Capybara.w3c_click_offset is false, from center when true. The
|
|
99
|
-
# runtime reads `simRect` to translate these into absolute
|
|
100
|
-
# clientX/clientY before dispatching the mouse events. Defaults
|
|
101
|
-
# (no x/y) target the element's center.
|
|
102
|
-
def click_options(keys, opts)
|
|
103
|
-
out = modifier_options(keys)
|
|
104
|
-
if opts.key?(:x) || opts.key?(:y)
|
|
105
|
-
out['offsetX'] = opts[:x].to_f if opts.key?(:x)
|
|
106
|
-
out['offsetY'] = opts[:y].to_f if opts.key?(:y)
|
|
107
|
-
out['w3cOffset'] = !!Capybara.w3c_click_offset
|
|
108
|
-
end
|
|
109
|
-
out['delay'] = opts[:delay].to_f if opts.key?(:delay) && opts[:delay].to_f.positive?
|
|
110
|
-
out
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def modifier_options(keys)
|
|
114
|
-
opts = {}
|
|
115
|
-
Array(keys).each do |k|
|
|
116
|
-
case k
|
|
117
|
-
when :shift then opts['shiftKey'] = true
|
|
118
|
-
when :ctrl, :control then opts['ctrlKey'] = true
|
|
119
|
-
when :alt then opts['altKey'] = true
|
|
120
|
-
when :meta, :command then opts['metaKey'] = true
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
opts
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
public
|
|
127
|
-
|
|
128
|
-
def hover
|
|
115
|
+
def hover(**_opts)
|
|
116
|
+
check_stale
|
|
129
117
|
browser.hover(handle_id)
|
|
118
|
+
self
|
|
130
119
|
end
|
|
131
120
|
|
|
132
|
-
def
|
|
133
|
-
|
|
121
|
+
def scroll_to(*, **) ; self ; end
|
|
122
|
+
|
|
123
|
+
# Capybara's standard rect API. No layout engine — but
|
|
124
|
+
# Discourse's `wait_for_animation` helper polls `element.rect[:x]`
|
|
125
|
+
# twice and waits for the values to stabilise, which they
|
|
126
|
+
# immediately do here (constant zeros) since we never animate.
|
|
127
|
+
# That unblocks every test guarded by an animation settle.
|
|
128
|
+
def rect
|
|
129
|
+
check_stale
|
|
130
|
+
{x: 0, y: 0, width: 0, height: 0, top: 0, left: 0, bottom: 0, right: 0}
|
|
134
131
|
end
|
|
135
132
|
|
|
136
133
|
def send_keys(*keys)
|
|
134
|
+
check_stale
|
|
137
135
|
browser.send_keys(handle_id, keys)
|
|
136
|
+
true
|
|
138
137
|
end
|
|
139
138
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
browser.
|
|
143
|
-
|
|
144
|
-
browser.trigger(other.handle_id, 'drop')
|
|
145
|
-
browser.trigger(handle_id, 'dragend')
|
|
139
|
+
def trigger(event)
|
|
140
|
+
check_stale
|
|
141
|
+
browser.dispatch_event(handle_id, event.to_s)
|
|
142
|
+
true
|
|
146
143
|
end
|
|
147
|
-
|
|
148
144
|
def drop(*args)
|
|
149
|
-
|
|
150
|
-
browser.drop(handle_id,
|
|
145
|
+
check_stale
|
|
146
|
+
browser.drop(handle_id, args)
|
|
147
|
+
true
|
|
151
148
|
end
|
|
152
149
|
|
|
153
|
-
def
|
|
154
|
-
|
|
150
|
+
def drag_to(target_node, **opts)
|
|
151
|
+
check_stale
|
|
152
|
+
target_node.check_stale if target_node.respond_to?(:check_stale)
|
|
153
|
+
target_handle = target_node.respond_to?(:handle_id) ? target_node.handle_id : target_node.native
|
|
154
|
+
browser.drag_to(handle_id, target_handle, **opts)
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
def set(value, **_)
|
|
158
|
+
check_stale
|
|
159
|
+
browser.set_value_with_events(handle_id, value)
|
|
160
|
+
end
|
|
155
161
|
|
|
156
|
-
def
|
|
157
|
-
|
|
162
|
+
def select_option
|
|
163
|
+
check_stale
|
|
164
|
+
browser.select_option(handle_id)
|
|
158
165
|
end
|
|
159
166
|
|
|
160
|
-
def
|
|
161
|
-
|
|
167
|
+
def unselect_option
|
|
168
|
+
check_stale
|
|
169
|
+
browser.unselect_option(handle_id)
|
|
162
170
|
end
|
|
163
171
|
|
|
164
|
-
def
|
|
165
|
-
|
|
172
|
+
def submit(*_)
|
|
173
|
+
check_stale
|
|
174
|
+
browser.submit_form(handle_id)
|
|
166
175
|
end
|
|
167
176
|
|
|
168
|
-
def
|
|
169
|
-
browser.
|
|
177
|
+
def find_xpath(query)
|
|
178
|
+
browser.find_xpath(query, handle_id).map {|id| self.class.new(driver, id) }
|
|
170
179
|
end
|
|
171
180
|
|
|
172
|
-
def
|
|
173
|
-
browser.
|
|
181
|
+
def find_css(query)
|
|
182
|
+
browser.find_css(query, handle_id).map {|id| self.class.new(driver, id) }
|
|
174
183
|
end
|
|
175
184
|
|
|
176
185
|
def shadow_root
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
check_stale
|
|
187
|
+
h = browser.shadow_root_handle(handle_id)
|
|
188
|
+
h && self.class.new(driver, h)
|
|
189
|
+
end
|
|
190
|
+
def disabled? = browser.disabled?(handle_id)
|
|
191
|
+
def selected? = browser.option_selected?(handle_id)
|
|
192
|
+
def checked? = !!self['checked']
|
|
193
|
+
def readonly? = !!self['readonly']
|
|
194
|
+
def obscured?(*) = !visible?
|
|
195
|
+
def synchronize(*) = yield
|
|
196
|
+
def style(names = [])
|
|
197
|
+
check_stale
|
|
198
|
+
browser.computed_style(handle_id, Array(names))
|
|
179
199
|
end
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
def path
|
|
201
|
+
check_stale
|
|
202
|
+
browser.node_path(handle_id)
|
|
183
203
|
end
|
|
184
204
|
|
|
205
|
+
# Both handle_id and context_gen are part of identity: after a
|
|
206
|
+
# cross-page reload the new element can land on the same handle
|
|
207
|
+
# id (JS-side counter resets per ctx) but a different generation.
|
|
208
|
+
# Capybara's synchronize uses `old_base == @base` to decide
|
|
209
|
+
# whether reload made progress, so id-only equality would mark
|
|
210
|
+
# a successful reload as a no-op and raise the original error.
|
|
185
211
|
def ==(other)
|
|
186
|
-
other.is_a?(Node) &&
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def inspect
|
|
190
|
-
%(#<Capybara::Simulated::Node tag="#{tag_name}" id=#{handle_id}>)
|
|
191
|
-
rescue StandardError
|
|
192
|
-
super
|
|
212
|
+
other.is_a?(Node) &&
|
|
213
|
+
other.handle_id == @handle_id &&
|
|
214
|
+
other.context_gen == @context_gen
|
|
193
215
|
end
|
|
194
216
|
|
|
195
217
|
private
|
|
196
218
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
when Hash
|
|
200
|
-
arg.map {|type, data|
|
|
201
|
-
{'kind' => 'string', 'type' => type.to_s, 'data' => data.to_s}
|
|
202
|
-
}
|
|
203
|
-
when String
|
|
204
|
-
[{'kind' => 'file', 'name' => File.basename(arg), 'type' => '', 'contents' => safely_read(arg)}]
|
|
205
|
-
else
|
|
206
|
-
[]
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def safely_read(path)
|
|
211
|
-
File.binread(path)
|
|
212
|
-
rescue StandardError
|
|
213
|
-
''
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def browser = driver.browser
|
|
217
|
-
|
|
218
|
-
def select_node_multiple?
|
|
219
|
-
browser.evaluate_script(<<~JS)
|
|
220
|
-
(() => {
|
|
221
|
-
const el = (#{select_lookup_js})
|
|
222
|
-
const sel = el.closest('select');
|
|
223
|
-
return !!(sel && sel.multiple);
|
|
224
|
-
})()
|
|
225
|
-
JS
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def select_lookup_js
|
|
229
|
-
# The JS-side handles map is private; use evaluate via a tiny accessor.
|
|
230
|
-
# Cheaper: ask browser directly through path/tag inspection.
|
|
231
|
-
"document.evaluate(#{path.inspect}, document, null, 9, null).singleNodeValue"
|
|
232
|
-
end
|
|
219
|
+
def browser = @browser
|
|
220
|
+
def check_stale = browser.check_stale(handle_id, @initial_node, @context_gen)
|
|
233
221
|
end
|
|
234
222
|
end
|
|
235
223
|
end
|