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.
data/vendor/js/entry.mjs CHANGED
@@ -1,8 +1,23 @@
1
- // Bundle entry: brings happy-dom and an XPath engine into one IIFE that
2
- // exposes them on a single `__csim_bundle` global. happy-dom does not
3
- // implement document.evaluate, so we layer fontoxpath on top.
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 {evaluateXPathToNodes} from 'fontoxpath';
6
+ import wgxpath from 'wicked-good-xpath';
7
7
 
8
- export {Window, URL, URLSearchParams, evaluateXPathToNodes};
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
- evaluateXPathToNodes} = globalThis.__csim_bundle;
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 nodes = evaluateXPathToNodes(xpath, root, xpathDomFacade, null, {
1261
- // happy-dom places elements in the XHTML namespace.
1262
- namespaceResolver: () => 'http://www.w3.org/1999/xhtml'
1263
- });
1264
- return nodes.map(track);
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
- if (!visibleAncestorChainOk(el)) return '';
1311
- return String(visibleTextOf(el));
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) { drainTimers(ms); return true; },
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-simulated
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima