capybara-simulated 0.0.5 → 0.0.7
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 +25 -10
- data/lib/capybara/simulated/version.rb +1 -1
- data/vendor/js/csim.bundle.js +4198 -13653
- data/vendor/js/entry.mjs +20 -5
- data/vendor/js/prelude.js +4 -0
- data/vendor/js/runtime.js +53 -121
- metadata +1 -1
data/vendor/js/entry.mjs
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
// Bundle entry: brings happy-dom
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Bundle entry: brings happy-dom + wgxpath into one IIFE that exposes them
|
|
2
|
+
// on a single `__csim_bundle` global. happy-dom does not implement
|
|
3
|
+
// `document.evaluate`, so wgxpath layers it onto `Document.prototype`.
|
|
4
4
|
import {Window} from 'happy-dom';
|
|
5
5
|
import {URL, URLSearchParams} from 'whatwg-url';
|
|
6
|
-
import
|
|
6
|
+
import wgxpath from 'wicked-good-xpath';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// happy-dom 20's `document` is an `HTMLDocument` instance and the proto
|
|
9
|
+
// chain doesn't include `Document.prototype` (where wgxpath plants
|
|
10
|
+
// `evaluate` / `createExpression` / `createNSResolver`). Mirror the three
|
|
11
|
+
// methods onto `HTMLDocument.prototype` so `document.evaluate` works.
|
|
12
|
+
function installXPath(win) {
|
|
13
|
+
wgxpath.install(win, /* opt_force */ true);
|
|
14
|
+
if (win.HTMLDocument && win.Document) {
|
|
15
|
+
for (const m of ['evaluate', 'createExpression', 'createNSResolver']) {
|
|
16
|
+
if (win.Document.prototype[m] && !win.HTMLDocument.prototype[m]) {
|
|
17
|
+
win.HTMLDocument.prototype[m] = win.Document.prototype[m];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {Window, URL, URLSearchParams, installXPath};
|
data/vendor/js/prelude.js
CHANGED
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
globalThis.__csim_runTimers = function (ms) {
|
|
132
132
|
const advance = (typeof ms === 'number') ? Math.max(0, ms) : Infinity;
|
|
133
133
|
_virtualClock += advance;
|
|
134
|
+
let everFired = false;
|
|
134
135
|
while (true) {
|
|
135
136
|
let fired = false;
|
|
136
137
|
const due = [];
|
|
@@ -141,10 +142,13 @@
|
|
|
141
142
|
_timers.delete(id);
|
|
142
143
|
try { t.fn(); } catch (_) {}
|
|
143
144
|
fired = true;
|
|
145
|
+
everFired = true;
|
|
144
146
|
}
|
|
145
147
|
if (!fired) break;
|
|
146
148
|
}
|
|
149
|
+
return everFired;
|
|
147
150
|
};
|
|
151
|
+
globalThis.__csim_pendingTimerCount = function () { return _timers.size; };
|
|
148
152
|
globalThis.__csim_clearTimers = function () {
|
|
149
153
|
_timers.clear();
|
|
150
154
|
_virtualClock = 0;
|
data/vendor/js/runtime.js
CHANGED
|
@@ -5,12 +5,25 @@
|
|
|
5
5
|
|
|
6
6
|
(function () {
|
|
7
7
|
const {Window, URL: WhatwgURL, URLSearchParams: WhatwgURLSearchParams,
|
|
8
|
-
|
|
8
|
+
installXPath} = globalThis.__csim_bundle;
|
|
9
9
|
if (!globalThis.URL) globalThis.URL = WhatwgURL;
|
|
10
10
|
if (!globalThis.URLSearchParams) globalThis.URLSearchParams = WhatwgURLSearchParams;
|
|
11
11
|
|
|
12
12
|
const handles = new Map();
|
|
13
13
|
let nextHandleId = 0;
|
|
14
|
+
|
|
15
|
+
// Capybara's `have_text` matchers and several internal checks call into
|
|
16
|
+
// `visibleText` repeatedly against the same handle while polling. The
|
|
17
|
+
// text-collection walks happy-dom's textContent / styles each call, so
|
|
18
|
+
// re-running it 1000+ times per spec adds up. Cache by handle and drop
|
|
19
|
+
// the whole cache the moment any mutating runtime method runs (or
|
|
20
|
+
// `drainTimers` advances the virtual clock — Stimulus / Turbo timers
|
|
21
|
+
// may have rewritten the page). The cache is intentionally narrow:
|
|
22
|
+
// we measured a broader MutationObserver-driven cache as a wash, since
|
|
23
|
+
// the dispatch overhead ate the cache wins; pinning a bare `Map.clear()`
|
|
24
|
+
// to known-mutating call sites is much cheaper.
|
|
25
|
+
const visibleTextCache = new Map();
|
|
26
|
+
function clearReadCache() { if (visibleTextCache.size) visibleTextCache.clear(); }
|
|
14
27
|
let currentWindow = null;
|
|
15
28
|
let currentDocument = null;
|
|
16
29
|
let activeHandleId = null;
|
|
@@ -47,6 +60,7 @@
|
|
|
47
60
|
catch (_) {}
|
|
48
61
|
}
|
|
49
62
|
handles.clear();
|
|
63
|
+
visibleTextCache.clear();
|
|
50
64
|
nextHandleId = 0;
|
|
51
65
|
activeHandleId = null;
|
|
52
66
|
modalQueue.length = 0;
|
|
@@ -170,6 +184,7 @@
|
|
|
170
184
|
installValidationMessages(win);
|
|
171
185
|
installMutationObserverPin(win);
|
|
172
186
|
installAttrNodeValue(win);
|
|
187
|
+
installXPath(win);
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
// happy-dom 20's `Attr` class implements `value` / `textContent` / `name`
|
|
@@ -889,8 +904,9 @@
|
|
|
889
904
|
|
|
890
905
|
function drainTimers(ms) {
|
|
891
906
|
if (typeof globalThis.__csim_runTimers === 'function') {
|
|
892
|
-
try { globalThis.__csim_runTimers(ms); } catch (_) {}
|
|
907
|
+
try { return !!globalThis.__csim_runTimers(ms); } catch (_) {}
|
|
893
908
|
}
|
|
909
|
+
return false;
|
|
894
910
|
}
|
|
895
911
|
|
|
896
912
|
// happy-dom has no layout engine, so getBoundingClientRect always returns
|
|
@@ -963,7 +979,6 @@
|
|
|
963
979
|
patchWindowGlobals(win);
|
|
964
980
|
installFetchShim(win);
|
|
965
981
|
installCustomElementUpgrade(win);
|
|
966
|
-
installDocumentEvaluate(win.document);
|
|
967
982
|
installHistoryTracking(win);
|
|
968
983
|
currentWindow = win;
|
|
969
984
|
currentDocument = win.document;
|
|
@@ -1257,11 +1272,14 @@
|
|
|
1257
1272
|
const fast = tryFastXPath(xpath, root);
|
|
1258
1273
|
if (fast) return fast.map(track);
|
|
1259
1274
|
try {
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1275
|
+
const result = currentDocument.evaluate(
|
|
1276
|
+
xpath, root, null,
|
|
1277
|
+
/* UNORDERED_NODE_ITERATOR_TYPE */ 4, null
|
|
1278
|
+
);
|
|
1279
|
+
const ids = [];
|
|
1280
|
+
let n;
|
|
1281
|
+
while ((n = result.iterateNext())) ids.push(track(n));
|
|
1282
|
+
return ids;
|
|
1265
1283
|
} catch (e) {
|
|
1266
1284
|
return findViaXPathFallback(xpath, root);
|
|
1267
1285
|
}
|
|
@@ -1305,10 +1323,13 @@
|
|
|
1305
1323
|
},
|
|
1306
1324
|
allText(id) { return String(lookup(id).textContent || ''); },
|
|
1307
1325
|
visibleText(id) {
|
|
1326
|
+
const cached = visibleTextCache.get(id);
|
|
1327
|
+
if (cached !== undefined) return cached;
|
|
1308
1328
|
const el = lookup(id);
|
|
1309
1329
|
// If any ancestor of `el` is hidden, the element renders nothing.
|
|
1310
|
-
|
|
1311
|
-
|
|
1330
|
+
const txt = visibleAncestorChainOk(el) ? String(visibleTextOf(el)) : '';
|
|
1331
|
+
visibleTextCache.set(id, txt);
|
|
1332
|
+
return txt;
|
|
1312
1333
|
},
|
|
1313
1334
|
path(id) { return String(buildXPath(lookup(id))); },
|
|
1314
1335
|
rect(id) {
|
|
@@ -1347,6 +1368,7 @@
|
|
|
1347
1368
|
},
|
|
1348
1369
|
|
|
1349
1370
|
setValue(id, value) {
|
|
1371
|
+
clearReadCache();
|
|
1350
1372
|
const el = lookup(id);
|
|
1351
1373
|
const tag = (el.tagName || '').toUpperCase();
|
|
1352
1374
|
const type = (el.type || (el.getAttribute && el.getAttribute('type')) || '').toLowerCase();
|
|
@@ -1454,6 +1476,7 @@
|
|
|
1454
1476
|
},
|
|
1455
1477
|
|
|
1456
1478
|
selectOption(id) {
|
|
1479
|
+
clearReadCache();
|
|
1457
1480
|
const opt = lookup(id);
|
|
1458
1481
|
const select = opt.parentNode && (opt.parentNode.tagName === 'SELECT'
|
|
1459
1482
|
? opt.parentNode
|
|
@@ -1472,6 +1495,7 @@
|
|
|
1472
1495
|
return true;
|
|
1473
1496
|
},
|
|
1474
1497
|
unselectOption(id) {
|
|
1498
|
+
clearReadCache();
|
|
1475
1499
|
const opt = lookup(id);
|
|
1476
1500
|
const select = opt.closest && opt.closest('select');
|
|
1477
1501
|
if (!select || !isMultipleSelect(select)) return false;
|
|
@@ -1518,8 +1542,8 @@
|
|
|
1518
1542
|
boolPropOrAttr(lookup(id), 'readonly'); },
|
|
1519
1543
|
multiple(id) { return boolPropOrAttr(lookup(id), 'multiple'); },
|
|
1520
1544
|
|
|
1521
|
-
focus(id) { activeHandleId = id; lookup(id).dispatchEvent(makeEvent('focus', {bubbles: false})); return true; },
|
|
1522
|
-
blur(id) { lookup(id).dispatchEvent(makeEvent('blur', {bubbles: false})); if (activeHandleId === id) activeHandleId = null; return true; },
|
|
1545
|
+
focus(id) { clearReadCache(); activeHandleId = id; lookup(id).dispatchEvent(makeEvent('focus', {bubbles: false})); return true; },
|
|
1546
|
+
blur(id) { clearReadCache(); lookup(id).dispatchEvent(makeEvent('blur', {bubbles: false})); if (activeHandleId === id) activeHandleId = null; return true; },
|
|
1523
1547
|
activeElement() {
|
|
1524
1548
|
// Honour the JS-side `document.activeElement` first — happy-dom
|
|
1525
1549
|
// updates it when scripts call `focus()` directly, which the
|
|
@@ -1531,14 +1555,22 @@
|
|
|
1531
1555
|
},
|
|
1532
1556
|
|
|
1533
1557
|
hover(id) {
|
|
1558
|
+
clearReadCache();
|
|
1534
1559
|
const el = lookup(id);
|
|
1535
1560
|
el.dispatchEvent(makeEvent('mouseover'));
|
|
1536
1561
|
el.dispatchEvent(makeEvent('mouseenter', {bubbles: false}));
|
|
1537
1562
|
return true;
|
|
1538
1563
|
},
|
|
1539
|
-
trigger(id, name) { lookup(id).dispatchEvent(makeEvent(name)); return true; },
|
|
1540
|
-
|
|
1541
|
-
drainTimers(ms) {
|
|
1564
|
+
trigger(id, name) { clearReadCache(); lookup(id).dispatchEvent(makeEvent(name)); return true; },
|
|
1565
|
+
|
|
1566
|
+
drainTimers(ms) {
|
|
1567
|
+
// Only clear the read cache if a timer actually fired — most
|
|
1568
|
+
// `advance_virtual_clock` calls from Ruby have nothing to do but
|
|
1569
|
+
// tick the wall clock. Without this the visibleText cache is
|
|
1570
|
+
// invalidated on every cross-bridge call and never hits.
|
|
1571
|
+
if (drainTimers(ms)) clearReadCache();
|
|
1572
|
+
return true;
|
|
1573
|
+
},
|
|
1542
1574
|
|
|
1543
1575
|
consumeHistoryPushed() {
|
|
1544
1576
|
const v = _historyPushed;
|
|
@@ -1561,6 +1593,7 @@
|
|
|
1561
1593
|
},
|
|
1562
1594
|
|
|
1563
1595
|
click(id, button, modifiers, skipDown) {
|
|
1596
|
+
clearReadCache();
|
|
1564
1597
|
const el = lookup(id);
|
|
1565
1598
|
activeHandleId = id;
|
|
1566
1599
|
const m = Object.assign({button: button || 0}, modifiers || {});
|
|
@@ -1707,6 +1740,7 @@
|
|
|
1707
1740
|
},
|
|
1708
1741
|
|
|
1709
1742
|
doubleClick(id, modifiers) {
|
|
1743
|
+
clearReadCache();
|
|
1710
1744
|
const el = lookup(id);
|
|
1711
1745
|
const m = Object.assign({button: 0}, modifiers || {});
|
|
1712
1746
|
applyClickOffset(el, m);
|
|
@@ -1720,6 +1754,7 @@
|
|
|
1720
1754
|
return {action: 'none'};
|
|
1721
1755
|
},
|
|
1722
1756
|
rightClick(id, modifiers, skipDown) {
|
|
1757
|
+
clearReadCache();
|
|
1723
1758
|
const el = lookup(id);
|
|
1724
1759
|
const m = Object.assign({button: 2}, modifiers || {});
|
|
1725
1760
|
applyClickOffset(el, m);
|
|
@@ -1735,6 +1770,7 @@
|
|
|
1735
1770
|
// exposes `DragEvent` as a plain `Event`, so we paste `dataTransfer`
|
|
1736
1771
|
// onto each event before dispatch.
|
|
1737
1772
|
drop(id, items) {
|
|
1773
|
+
clearReadCache();
|
|
1738
1774
|
const el = lookup(id);
|
|
1739
1775
|
const win = currentWindow;
|
|
1740
1776
|
const dt = new win.DataTransfer();
|
|
@@ -1761,9 +1797,10 @@
|
|
|
1761
1797
|
return true;
|
|
1762
1798
|
},
|
|
1763
1799
|
|
|
1764
|
-
submit(id) { return submitDescriptor(lookup(id), null); },
|
|
1800
|
+
submit(id) { clearReadCache(); return submitDescriptor(lookup(id), null); },
|
|
1765
1801
|
|
|
1766
1802
|
sendKeys(id, keys) {
|
|
1803
|
+
clearReadCache();
|
|
1767
1804
|
const el = lookup(id);
|
|
1768
1805
|
const editable = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
|
|
1769
1806
|
let formToSubmit = null;
|
|
@@ -1955,111 +1992,6 @@
|
|
|
1955
1992
|
wrap('replaceState');
|
|
1956
1993
|
}
|
|
1957
1994
|
|
|
1958
|
-
// happy-dom does not implement `document.evaluate`. Capybara internals
|
|
1959
|
-
// and user scripts both reach for it occasionally. Polyfill it on top of
|
|
1960
|
-
// fontoxpath so callers see W3C XPathResult semantics.
|
|
1961
|
-
function installDocumentEvaluate(doc) {
|
|
1962
|
-
if (typeof doc.evaluate === 'function') return;
|
|
1963
|
-
const ANY_TYPE = 0;
|
|
1964
|
-
const NUMBER_TYPE = 1;
|
|
1965
|
-
const STRING_TYPE = 2;
|
|
1966
|
-
const BOOLEAN_TYPE = 3;
|
|
1967
|
-
const UNORDERED_NODE_ITERATOR_TYPE = 4;
|
|
1968
|
-
const ORDERED_NODE_ITERATOR_TYPE = 5;
|
|
1969
|
-
const UNORDERED_NODE_SNAPSHOT_TYPE = 6;
|
|
1970
|
-
const ORDERED_NODE_SNAPSHOT_TYPE = 7;
|
|
1971
|
-
const ANY_UNORDERED_NODE_TYPE = 8;
|
|
1972
|
-
const FIRST_ORDERED_NODE_TYPE = 9;
|
|
1973
|
-
|
|
1974
|
-
function makeResult(type, items) {
|
|
1975
|
-
return {
|
|
1976
|
-
resultType: type,
|
|
1977
|
-
snapshotLength: items.length,
|
|
1978
|
-
snapshotItem: (i) => items[i] || null,
|
|
1979
|
-
get singleNodeValue() { return items[0] || null; },
|
|
1980
|
-
iterateNext: (() => { let i = 0; return () => items[i++] || null; })()
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
doc.evaluate = function (xpath, contextNode, _resolver, type, _result) {
|
|
1985
|
-
const root = contextNode || doc.documentElement || doc;
|
|
1986
|
-
let nodes = [];
|
|
1987
|
-
try {
|
|
1988
|
-
nodes = evaluateXPathToNodes(xpath, root, xpathDomFacade, null, {
|
|
1989
|
-
namespaceResolver: () => 'http://www.w3.org/1999/xhtml'
|
|
1990
|
-
});
|
|
1991
|
-
} catch (_) {
|
|
1992
|
-
nodes = [];
|
|
1993
|
-
}
|
|
1994
|
-
switch (type) {
|
|
1995
|
-
case FIRST_ORDERED_NODE_TYPE:
|
|
1996
|
-
case ANY_UNORDERED_NODE_TYPE:
|
|
1997
|
-
return makeResult(type, nodes.slice(0, 1));
|
|
1998
|
-
case NUMBER_TYPE:
|
|
1999
|
-
case STRING_TYPE:
|
|
2000
|
-
case BOOLEAN_TYPE:
|
|
2001
|
-
// We do not support primitive XPath results; callers fall back.
|
|
2002
|
-
return makeResult(type, []);
|
|
2003
|
-
default:
|
|
2004
|
-
return makeResult(type || ORDERED_NODE_SNAPSHOT_TYPE, nodes);
|
|
2005
|
-
}
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// Custom DOM facade for fontoxpath that uses childNodes-based traversal
|
|
2010
|
-
// so happy-dom quirks around sibling links don't break root-anchored
|
|
2011
|
-
// queries. Mirrors the helper we built for linkedom.
|
|
2012
|
-
// fontoxpath calls into this facade tens of thousands of times per
|
|
2013
|
-
// traversal, so each method needs to delegate to happy-dom's native
|
|
2014
|
-
// pointers rather than reconstructing them. Earlier revisions of
|
|
2015
|
-
// getNextSibling / getPreviousSibling rebuilt childNodes via
|
|
2016
|
-
// Array.from(parent.childNodes) and then indexOf'd the node — quadratic
|
|
2017
|
-
// in tree size, and the dominant cost on any non-trivial page.
|
|
2018
|
-
const xpathDomFacade = {
|
|
2019
|
-
getAllAttributes(node) {
|
|
2020
|
-
if (!node || node.nodeType !== 1 || !node.attributes) return [];
|
|
2021
|
-
return Array.from(node.attributes);
|
|
2022
|
-
},
|
|
2023
|
-
getAttribute(node, name) {
|
|
2024
|
-
return node && node.getAttribute ? node.getAttribute(name) : null;
|
|
2025
|
-
},
|
|
2026
|
-
getChildNodes(node) {
|
|
2027
|
-
if (!node) return [];
|
|
2028
|
-
if (node.childNodes) return Array.from(node.childNodes);
|
|
2029
|
-
if (node.nodeType === 9 && node.documentElement) return [node.documentElement];
|
|
2030
|
-
return [];
|
|
2031
|
-
},
|
|
2032
|
-
getData(node) {
|
|
2033
|
-
if (!node) return '';
|
|
2034
|
-
if (node.nodeType === 2) return node.value || '';
|
|
2035
|
-
return node.data || '';
|
|
2036
|
-
},
|
|
2037
|
-
// happy-dom 20 has a quirk on `<template>`: `template.childNodes` is
|
|
2038
|
-
// empty (the parsed content lives in `template.content`) but
|
|
2039
|
-
// `template.firstChild` / `template.lastChild` still return the first
|
|
2040
|
-
// text node of `template.content`. Walking into that ghost child
|
|
2041
|
-
// makes fontoxpath traverse content as if it were a regular descendant,
|
|
2042
|
-
// which scrambles every predicate downstream. Verify against
|
|
2043
|
-
// childNodes.length before trusting firstChild / lastChild.
|
|
2044
|
-
getFirstChild(node) {
|
|
2045
|
-
const fc = node?.firstChild ?? null;
|
|
2046
|
-
if (!fc) return null;
|
|
2047
|
-
const cs = node.childNodes;
|
|
2048
|
-
if (cs && cs.length === 0) return null;
|
|
2049
|
-
return fc;
|
|
2050
|
-
},
|
|
2051
|
-
getLastChild(node) {
|
|
2052
|
-
const lc = node?.lastChild ?? null;
|
|
2053
|
-
if (!lc) return null;
|
|
2054
|
-
const cs = node.childNodes;
|
|
2055
|
-
if (cs && cs.length === 0) return null;
|
|
2056
|
-
return lc;
|
|
2057
|
-
},
|
|
2058
|
-
getNextSibling(node) { return node?.nextSibling ?? null; },
|
|
2059
|
-
getPreviousSibling(node) { return node?.previousSibling ?? null; },
|
|
2060
|
-
getParentNode(node) { return node?.parentNode ?? null; }
|
|
2061
|
-
};
|
|
2062
|
-
|
|
2063
1995
|
// Capybara's xpath gem produces a small set of stable XPath shapes for
|
|
2064
1996
|
// its built-in selectors (link, button, link_or_button, select, option,
|
|
2065
1997
|
// field, fillable_field, …). Even after the facade fix fontoxpath spends
|