pi-lean-dimension 0.1.0 → 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.
- package/README.md +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reddit-style dialog fixture — shared helper for consent dialog tests.
|
|
3
|
-
*
|
|
4
|
-
* Provides four HTML variants of a Reddit-like feed page with consent
|
|
5
|
-
* dialogs, plus helper functions for extracting @e references and
|
|
6
|
-
* counting dialog elements from accessibility snapshots.
|
|
7
|
-
*
|
|
8
|
-
* Exports
|
|
9
|
-
* -------
|
|
10
|
-
* REDDIT_DIALOG_HTML — feed + consent dialog overlay (baseline)
|
|
11
|
-
* REDDIT_STACKED_HTML — feed → consent dialog → "Welcome Back" dialog
|
|
12
|
-
* REDDIT_ASYNC_HTML — feed loads first, dialog appears via setTimeout
|
|
13
|
-
* REDDIT_FEED_ONLY_HTML— feed without dialog (baseline)
|
|
14
|
-
* findRef(snapshot, text) → {ref, role?, name?, line} | null
|
|
15
|
-
* dialogCount(snapshot) → number
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
// ─── Helpers ──────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
/** Generate N feed article items with deterministic content. */
|
|
21
|
-
function generateFeedPosts(count: number): string {
|
|
22
|
-
return Array.from(
|
|
23
|
-
{ length: count },
|
|
24
|
-
(_, i) =>
|
|
25
|
-
` <article>
|
|
26
|
-
<h2><a href="/post-${i + 1}">Post Title ${i + 1}</a></h2>
|
|
27
|
-
<span>Author_${i + 1}</span>
|
|
28
|
-
<span>Score: ${100 - i}</span>
|
|
29
|
-
<a href="/comments-${i + 1}">${(i % 49) + 1} comments</a>
|
|
30
|
-
</article>`,
|
|
31
|
-
).join("\n");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ─── Dialog HTML fragments ────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const CONSENT_DIALOG_STYLE = `
|
|
37
|
-
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
38
|
-
background: rgba(0,0,0,0.6); z-index: 1000;
|
|
39
|
-
display: flex; align-items: center; justify-content: center;
|
|
40
|
-
`.trim();
|
|
41
|
-
|
|
42
|
-
const CONSENT_DIALOG_CSS = `
|
|
43
|
-
background: white; padding: 2rem; border-radius: 12px;
|
|
44
|
-
max-width: 440px; text-align: center; box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
45
|
-
`.trim();
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The consent dialog overlay with "Reject All" (nested SVG) and "Accept All".
|
|
49
|
-
* Reddit's actual pattern uses an SVG close icon inside the reject button
|
|
50
|
-
* — this triggers the occlusion false-positive that we want to test.
|
|
51
|
-
*/
|
|
52
|
-
function consentOverlayHtml(dismissFn: string, acceptFn: string): string {
|
|
53
|
-
return `<div role="dialog" aria-label="Data Protection Consent" id="consent-overlay" style="${CONSENT_DIALOG_STYLE}">
|
|
54
|
-
<div id="consent-dialog" style="${CONSENT_DIALOG_CSS}">
|
|
55
|
-
<h2>Data Protection Consent</h2>
|
|
56
|
-
<p>We use cookies and similar technologies to provide our services.</p>
|
|
57
|
-
<div style="display:flex; gap:8px; justify-content:center; margin-top:16px;">
|
|
58
|
-
<button onclick="${dismissFn}()"
|
|
59
|
-
style="padding:8px 16px; cursor:pointer; border:1px solid #ccc; border-radius:4px;">
|
|
60
|
-
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"
|
|
61
|
-
style="vertical-align:middle">
|
|
62
|
-
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2"/>
|
|
63
|
-
</svg>
|
|
64
|
-
<span style="vertical-align:middle">Reject All</span>
|
|
65
|
-
</button>
|
|
66
|
-
<button onclick="${acceptFn}()"
|
|
67
|
-
style="padding:8px 16px; cursor:pointer; background:#0079d3; color:white; border:none; border-radius:4px;">
|
|
68
|
-
Accept All
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</div>`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const FEED_SECTION = `<div role="feed" aria-label="Reddit feed">
|
|
76
|
-
${generateFeedPosts(100)}
|
|
77
|
-
</div>`;
|
|
78
|
-
|
|
79
|
-
const STACKED_JS = `
|
|
80
|
-
function showWelcomeDialog() {
|
|
81
|
-
var w = document.createElement('div');
|
|
82
|
-
w.id = 'welcome-overlay';
|
|
83
|
-
w.setAttribute('role', 'dialog');
|
|
84
|
-
w.setAttribute('aria-label', 'Welcome Back');
|
|
85
|
-
w.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:1000; display:flex; align-items:center; justify-content:center;';
|
|
86
|
-
w.innerHTML = '<div style="background:white; padding:2rem; border-radius:12px; max-width:440px; text-align:center; box-shadow:0 4px 24px rgba(0,0,0,0.3);"><h2>Welcome Back</h2><p>Thanks for your consent preferences.</p><button onclick="dismissWelcome()" style="padding:8px 16px; cursor:pointer; border:1px solid #ccc; border-radius:4px;">Dismiss</button></div>';
|
|
87
|
-
document.body.appendChild(w);
|
|
88
|
-
}
|
|
89
|
-
function dismissConsent() {
|
|
90
|
-
document.getElementById('consent-overlay').style.display = 'none';
|
|
91
|
-
showWelcomeDialog();
|
|
92
|
-
window._dialogDismissed = true;
|
|
93
|
-
}
|
|
94
|
-
function acceptConsent() {
|
|
95
|
-
document.getElementById('consent-overlay').style.display = 'none';
|
|
96
|
-
showWelcomeDialog();
|
|
97
|
-
window._dialogAccepted = true;
|
|
98
|
-
}
|
|
99
|
-
function dismissWelcome() {
|
|
100
|
-
var el = document.getElementById('welcome-overlay');
|
|
101
|
-
if (el) el.style.display = 'none';
|
|
102
|
-
window._welcomeDismissed = true;
|
|
103
|
-
}
|
|
104
|
-
`;
|
|
105
|
-
// ─── Page variants ────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Baseline: 100 feed posts + a consent dialog overlay.
|
|
109
|
-
*
|
|
110
|
-
* The dialog covers the entire viewport. The "Reject All" button
|
|
111
|
-
* contains a nested `<svg>` to test occlusion false-positive detection.
|
|
112
|
-
*/
|
|
113
|
-
export const REDDIT_DIALOG_HTML = `<!DOCTYPE html>
|
|
114
|
-
<html><head><title>Reddit — Consent Dialog</title></head>
|
|
115
|
-
<body>
|
|
116
|
-
<script>
|
|
117
|
-
function dismissConsent() {
|
|
118
|
-
document.getElementById('consent-overlay').style.display = 'none';
|
|
119
|
-
window._dialogDismissed = true;
|
|
120
|
-
}
|
|
121
|
-
function acceptConsent() {
|
|
122
|
-
document.getElementById('consent-overlay').style.display = 'none';
|
|
123
|
-
window._dialogAccepted = true;
|
|
124
|
-
}
|
|
125
|
-
</script>
|
|
126
|
-
${FEED_SECTION}
|
|
127
|
-
${consentOverlayHtml("dismissConsent", "acceptConsent")}
|
|
128
|
-
<footer><p>Reddit feed footer</p></footer>
|
|
129
|
-
</body></html>`;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Stacked variant: consent dialog → upon dismiss reveals "Welcome Back".
|
|
133
|
-
*
|
|
134
|
-
* Closing dialog A (consent) makes dialog B (welcome) appear.
|
|
135
|
-
* Tests that the router correctly sequences through stacked dialogs.
|
|
136
|
-
*/
|
|
137
|
-
export const REDDIT_STACKED_HTML = `<!DOCTYPE html>
|
|
138
|
-
<html><head><title>Reddit — Stacked Dialogs</title></head>
|
|
139
|
-
<body>
|
|
140
|
-
<script>
|
|
141
|
-
${STACKED_JS}
|
|
142
|
-
</script>
|
|
143
|
-
${FEED_SECTION}
|
|
144
|
-
${consentOverlayHtml("dismissConsent", "acceptConsent")}
|
|
145
|
-
<footer><p>Reddit feed footer</p></footer>
|
|
146
|
-
</body></html>`;
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Async variant: feed loads immediately, dialog appears after 500ms.
|
|
150
|
-
*
|
|
151
|
-
* Tests timing-dependent scenarios where the dialog arrives after the
|
|
152
|
-
* initial snapshot. The agent must handle the delayed dialog appearance.
|
|
153
|
-
*/
|
|
154
|
-
export const REDDIT_ASYNC_HTML = `<!DOCTYPE html>
|
|
155
|
-
<html><head><title>Reddit — Async Dialog</title></head>
|
|
156
|
-
<body>
|
|
157
|
-
<script>
|
|
158
|
-
function dismissConsent() {
|
|
159
|
-
document.getElementById('consent-overlay').style.display = 'none';
|
|
160
|
-
window._dialogDismissed = true;
|
|
161
|
-
}
|
|
162
|
-
function acceptConsent() {
|
|
163
|
-
document.getElementById('consent-overlay').style.display = 'none';
|
|
164
|
-
window._dialogAccepted = true;
|
|
165
|
-
}
|
|
166
|
-
setTimeout(function() {
|
|
167
|
-
var el = document.getElementById('consent-overlay');
|
|
168
|
-
if (el) el.style.display = 'flex';
|
|
169
|
-
}, 500);
|
|
170
|
-
</script>
|
|
171
|
-
${FEED_SECTION}
|
|
172
|
-
<div role="dialog" aria-label="Data Protection Consent" id="consent-overlay" style="display:none; ${CONSENT_DIALOG_STYLE}">
|
|
173
|
-
<div id="consent-dialog" style="${CONSENT_DIALOG_CSS}">
|
|
174
|
-
<h2>Data Protection Consent</h2>
|
|
175
|
-
<p>This dialog appeared after the page loaded.</p>
|
|
176
|
-
<div style="display:flex; gap:8px; justify-content:center; margin-top:16px;">
|
|
177
|
-
<button onclick="dismissConsent()"
|
|
178
|
-
style="padding:8px 16px; cursor:pointer; border:1px solid #ccc; border-radius:4px;">
|
|
179
|
-
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"
|
|
180
|
-
style="vertical-align:middle">
|
|
181
|
-
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2"/>
|
|
182
|
-
</svg>
|
|
183
|
-
<span style="vertical-align:middle">Reject All</span>
|
|
184
|
-
</button>
|
|
185
|
-
<button onclick="acceptConsent()"
|
|
186
|
-
style="padding:8px 16px; cursor:pointer; background:#0079d3; color:white; border:none; border-radius:4px;">
|
|
187
|
-
Accept All
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
<footer><p>Reddit feed footer</p></footer>
|
|
193
|
-
</body></html>`;
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Feed-only variant (no dialog at all) — baseline for occlusion testing.
|
|
197
|
-
* Ensures feed links are clickable when no dialog obscures them.
|
|
198
|
-
*/
|
|
199
|
-
export const REDDIT_FEED_ONLY_HTML = `<!DOCTYPE html>
|
|
200
|
-
<html><head><title>Reddit — Feed Only</title></head>
|
|
201
|
-
<body>
|
|
202
|
-
${FEED_SECTION}
|
|
203
|
-
<footer><p>Reddit feed footer — no dialog</p></footer>
|
|
204
|
-
</body></html>`;
|
|
205
|
-
|
|
206
|
-
// ─── Helper functions ─────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
export interface RefInfo {
|
|
209
|
-
/** The @e reference string, e.g. "@e5" */
|
|
210
|
-
ref: string;
|
|
211
|
-
/** Accessibility role of the element, e.g. "button" */
|
|
212
|
-
role?: string;
|
|
213
|
-
/** Accessible name of the element, e.g. "Reject All" */
|
|
214
|
-
name?: string;
|
|
215
|
-
/** The full snapshot line containing the match */
|
|
216
|
-
line: string;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Find the first @e reference in a snapshot whose line contains `text`.
|
|
221
|
-
*
|
|
222
|
-
* Useful for locating interactive elements by their accessible name
|
|
223
|
-
* (or partial text) in accessibility tree output.
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
* const info = findRef(snap.snapshot, "Reject All");
|
|
227
|
-
* if (info) await plugin.click(taskId, info.ref);
|
|
228
|
-
*
|
|
229
|
-
* @param snapshot Accessibility tree snapshot string (from NavigateResult or SnapshotResult)
|
|
230
|
-
* @param text Substring to search for in snapshot lines
|
|
231
|
-
* @returns RefInfo if found, null otherwise
|
|
232
|
-
*/
|
|
233
|
-
export function findRef(snapshot: string, text: string): RefInfo | null {
|
|
234
|
-
const lines = snapshot.split("\n");
|
|
235
|
-
for (const line of lines) {
|
|
236
|
-
if (line.includes(text)) {
|
|
237
|
-
const refMatch = line.match(/@(e\d+)/);
|
|
238
|
-
if (refMatch) {
|
|
239
|
-
const roleMatch = line.match(/\[(\w+)\]/);
|
|
240
|
-
const nameMatch = line.match(/"([^"]+)"/);
|
|
241
|
-
const info: Partial<RefInfo> & Pick<RefInfo, "ref" | "line"> = {
|
|
242
|
-
ref: `@${refMatch[1]!}`,
|
|
243
|
-
line,
|
|
244
|
-
};
|
|
245
|
-
if (roleMatch?.[1]) info.role = roleMatch[1];
|
|
246
|
-
if (nameMatch?.[1]) info.name = nameMatch[1];
|
|
247
|
-
return info as RefInfo;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Count how many dialog elements appear in a snapshot.
|
|
256
|
-
*
|
|
257
|
-
* Matches accessibility tree dialog role markers (`💬 dialog "..."`).
|
|
258
|
-
*
|
|
259
|
-
* @param snapshot Accessibility tree snapshot string
|
|
260
|
-
* @returns Number of distinct dialog elements found
|
|
261
|
-
*/
|
|
262
|
-
export function dialogCount(snapshot: string): number {
|
|
263
|
-
return (snapshot.match(/dialog\s+"/g) || []).length;
|
|
264
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test utilities for browser-toggle.ts.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports internal functions for isolated unit testing without
|
|
5
|
-
* polluting browser-toggle.ts's public API surface.
|
|
6
|
-
*
|
|
7
|
-
* ── Usage ─────────────────────────────────────────────
|
|
8
|
-
*
|
|
9
|
-
* import browserToggle from "../browser-toggle";
|
|
10
|
-
* import { getRegisteredBrowserTools, ... } from "./helpers/toggle-test-utils";
|
|
11
|
-
*
|
|
12
|
-
* These are NOT part of the production API — do not import
|
|
13
|
-
* from this file in production code.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
getRegisteredBrowserTools,
|
|
18
|
-
isBrowserEnabled,
|
|
19
|
-
applyBrowserState,
|
|
20
|
-
persistState,
|
|
21
|
-
restoreFromBranch,
|
|
22
|
-
readBrowserToggleConfig,
|
|
23
|
-
applyConfigDefault,
|
|
24
|
-
getRegisteredLearnTools,
|
|
25
|
-
getRegisteredSiblingTools,
|
|
26
|
-
isLearnEnabled,
|
|
27
|
-
applyLearnState,
|
|
28
|
-
_resetToggleStateForTest,
|
|
29
|
-
} from "../../browser-toggle.js";
|
|
30
|
-
|
|
31
|
-
export type { BrowserToggleState } from "../../browser-toggle.js";
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reddit dialog fixture — standalone TypeScript ChromiumPlugin tests.
|
|
3
|
-
*
|
|
4
|
-
* Exercises consent dialog interaction, stacked dialogs, and async dialogs
|
|
5
|
-
* against a local test server serving the
|
|
6
|
-
* shared fixture HTML from reddit-fixture.ts.
|
|
7
|
-
*
|
|
8
|
-
* Includes a ×10 consistency loop that closes the consent dialog
|
|
9
|
-
* ten times in independent sessions.
|
|
10
|
-
*
|
|
11
|
-
* Run: npx vitest run --reporter verbose __tests__/reddit-dialog.test.ts
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
15
|
-
import { ChromiumPlugin } from "../backends/chromium/index.js";
|
|
16
|
-
import { startTestServer } from "./helpers/test-server.js";
|
|
17
|
-
import {
|
|
18
|
-
REDDIT_DIALOG_HTML,
|
|
19
|
-
REDDIT_STACKED_HTML,
|
|
20
|
-
REDDIT_ASYNC_HTML,
|
|
21
|
-
findRef,
|
|
22
|
-
dialogCount,
|
|
23
|
-
} from "./helpers/reddit-fixture.js";
|
|
24
|
-
|
|
25
|
-
// ─── Test Server ──────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
let serverUrl: string;
|
|
28
|
-
let stopServer: () => Promise<void>;
|
|
29
|
-
|
|
30
|
-
beforeAll(async () => {
|
|
31
|
-
const server = await startTestServer((req, res) => {
|
|
32
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
33
|
-
const pages: Record<string, string> = {
|
|
34
|
-
"/reddit-dialog": REDDIT_DIALOG_HTML,
|
|
35
|
-
"/reddit-stacked": REDDIT_STACKED_HTML,
|
|
36
|
-
"/reddit-async": REDDIT_ASYNC_HTML,
|
|
37
|
-
"/slow-nav": `<!DOCTYPE html>
|
|
38
|
-
<html><head><title>Slow Nav Source</title></head>
|
|
39
|
-
<body>
|
|
40
|
-
<h1>Slow Nav Source</h1>
|
|
41
|
-
<a href="/slow-nav-destination" onclick="event.preventDefault(); setTimeout(() => location.href='/slow-nav-destination', 280)">Go to Destination</a>
|
|
42
|
-
</body></html>`,
|
|
43
|
-
"/slow-nav-destination": `<!DOCTYPE html>
|
|
44
|
-
<html><head><title>Slow Nav Destination</title></head>
|
|
45
|
-
<body>
|
|
46
|
-
<h1>Slow Nav Destination</h1>
|
|
47
|
-
</body></html>`,
|
|
48
|
-
};
|
|
49
|
-
const html = pages[url.pathname];
|
|
50
|
-
if (html) {
|
|
51
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
52
|
-
res.end(html);
|
|
53
|
-
} else {
|
|
54
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
55
|
-
res.end("404");
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
serverUrl = server.url;
|
|
59
|
-
stopServer = server.stop;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
afterAll(async () => {
|
|
63
|
-
await stopServer();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// ─── ChromiumPlugin Setup ───────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
async function createPlugin(): Promise<ChromiumPlugin> {
|
|
69
|
-
const p = new ChromiumPlugin();
|
|
70
|
-
await p.init({});
|
|
71
|
-
return p;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const NAV_TIMEOUT = 30_000;
|
|
75
|
-
|
|
76
|
-
// ═══════════════════════════════════════════════════════════════
|
|
77
|
-
// First Pass — individual behavioral tests
|
|
78
|
-
// ═══════════════════════════════════════════════════════════════
|
|
79
|
-
|
|
80
|
-
describe("Reddit dialog — first pass", () => {
|
|
81
|
-
const TASK_ID = "reddit-first-pass";
|
|
82
|
-
let plugin: ChromiumPlugin;
|
|
83
|
-
|
|
84
|
-
beforeAll(async () => {
|
|
85
|
-
plugin = await createPlugin();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
afterAll(async () => {
|
|
89
|
-
await plugin.cleanupAll().catch(() => {});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("navigates and snapshot includes consent dialog", async () => {
|
|
93
|
-
const nav = await plugin.navigate(
|
|
94
|
-
`${serverUrl}/reddit-dialog`,
|
|
95
|
-
TASK_ID,
|
|
96
|
-
NAV_TIMEOUT,
|
|
97
|
-
);
|
|
98
|
-
expect(nav.success).toBe(true);
|
|
99
|
-
expect(nav.title).toContain("Reddit");
|
|
100
|
-
expect(nav.snapshot).toContain("Consent");
|
|
101
|
-
|
|
102
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
103
|
-
expect(snap.success).toBe(true);
|
|
104
|
-
expect(dialogCount(snap.snapshot)).toBeGreaterThanOrEqual(1);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("reject all (nested SVG button) closes the dialog", async () => {
|
|
108
|
-
await plugin.navigate(`${serverUrl}/reddit-dialog`, TASK_ID, NAV_TIMEOUT);
|
|
109
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
110
|
-
expect(snap.success).toBe(true);
|
|
111
|
-
|
|
112
|
-
const info = findRef(snap.snapshot, "Reject All");
|
|
113
|
-
if (!info) return; // element beyond cap
|
|
114
|
-
|
|
115
|
-
const result = await plugin.click(TASK_ID, info.ref);
|
|
116
|
-
expect(result.success).toBe(true);
|
|
117
|
-
|
|
118
|
-
const after = await plugin.snapshot(TASK_ID);
|
|
119
|
-
expect(dialogCount(after.snapshot)).toBe(0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("accept all (plain button) closes the dialog", async () => {
|
|
123
|
-
await plugin.navigate(`${serverUrl}/reddit-dialog`, TASK_ID, NAV_TIMEOUT);
|
|
124
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
125
|
-
expect(snap.success).toBe(true);
|
|
126
|
-
|
|
127
|
-
const info = findRef(snap.snapshot, "Accept All");
|
|
128
|
-
if (!info) return;
|
|
129
|
-
|
|
130
|
-
const result = await plugin.click(TASK_ID, info.ref);
|
|
131
|
-
expect(result.success).toBe(true);
|
|
132
|
-
|
|
133
|
-
const after = await plugin.snapshot(TASK_ID);
|
|
134
|
-
expect(dialogCount(after.snapshot)).toBe(0);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("stacked dialogs — consent then welcome — both dismissible", async () => {
|
|
138
|
-
const S_TASK = "reddit-stacked";
|
|
139
|
-
await plugin.navigate(`${serverUrl}/reddit-stacked`, S_TASK, NAV_TIMEOUT);
|
|
140
|
-
|
|
141
|
-
// Dialog A: consent
|
|
142
|
-
const snap1 = await plugin.snapshot(S_TASK);
|
|
143
|
-
expect(snap1.success).toBe(true);
|
|
144
|
-
expect(snap1.snapshot).toContain("Consent");
|
|
145
|
-
|
|
146
|
-
const rejectInfo = findRef(snap1.snapshot, "Reject All");
|
|
147
|
-
if (!rejectInfo) {
|
|
148
|
-
await plugin.cleanup(S_TASK);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const r1 = await plugin.click(S_TASK, rejectInfo.ref);
|
|
152
|
-
expect(r1.success).toBe(true);
|
|
153
|
-
|
|
154
|
-
// Dialog B: welcome back
|
|
155
|
-
const snap2 = await plugin.snapshot(S_TASK);
|
|
156
|
-
expect(snap2.success).toBe(true);
|
|
157
|
-
expect(snap2.snapshot).toContain("Welcome");
|
|
158
|
-
|
|
159
|
-
const dismissInfo = findRef(snap2.snapshot, "Dismiss");
|
|
160
|
-
if (!dismissInfo) {
|
|
161
|
-
await plugin.cleanup(S_TASK);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const r2 = await plugin.click(S_TASK, dismissInfo.ref);
|
|
165
|
-
expect(r2.success).toBe(true);
|
|
166
|
-
|
|
167
|
-
// No dialogs remain
|
|
168
|
-
const snap3 = await plugin.snapshot(S_TASK);
|
|
169
|
-
expect(dialogCount(snap3.snapshot)).toBe(0);
|
|
170
|
-
|
|
171
|
-
await plugin.cleanup(S_TASK);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("async dialog arrives after timeout and is dismissible", async () => {
|
|
175
|
-
const A_TASK = "reddit-async";
|
|
176
|
-
await plugin.navigate(`${serverUrl}/reddit-async`, A_TASK, NAV_TIMEOUT);
|
|
177
|
-
|
|
178
|
-
// Wait for async dialog (setTimeout 500ms in fixture)
|
|
179
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
180
|
-
|
|
181
|
-
const snap = await plugin.snapshot(A_TASK);
|
|
182
|
-
expect(snap.success).toBe(true);
|
|
183
|
-
expect(snap.snapshot).toContain("Consent");
|
|
184
|
-
|
|
185
|
-
const info = findRef(snap.snapshot, "Reject All");
|
|
186
|
-
if (!info) {
|
|
187
|
-
await plugin.cleanup(A_TASK);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const result = await plugin.click(A_TASK, info.ref);
|
|
191
|
-
expect(result.success).toBe(true);
|
|
192
|
-
|
|
193
|
-
await plugin.cleanup(A_TASK);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// ═══════════════════════════════════════════════════════════════
|
|
198
|
-
// ×10 Consistency — close the dialog 10 times in isolation
|
|
199
|
-
// ═══════════════════════════════════════════════════════════════
|
|
200
|
-
|
|
201
|
-
describe("Reddit dialog — consistency × 10", () => {
|
|
202
|
-
let plugin: ChromiumPlugin;
|
|
203
|
-
|
|
204
|
-
/** Shared no-op error handler to avoid arrow-function-in-loop allocations. */
|
|
205
|
-
const noop = () => {};
|
|
206
|
-
|
|
207
|
-
beforeAll(async () => {
|
|
208
|
-
plugin = await createPlugin();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
afterAll(async () => {
|
|
212
|
-
await plugin.cleanupAll().catch(() => {});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("closes the consent dialog 10 times in independent sessions", async () => {
|
|
216
|
-
const failures: string[] = [];
|
|
217
|
-
|
|
218
|
-
for (let i = 0; i < 10; i++) {
|
|
219
|
-
const taskId = `consistency-${i}`;
|
|
220
|
-
try {
|
|
221
|
-
const nav = await plugin.navigate(
|
|
222
|
-
`${serverUrl}/reddit-dialog`,
|
|
223
|
-
taskId,
|
|
224
|
-
NAV_TIMEOUT,
|
|
225
|
-
);
|
|
226
|
-
expect(nav.success).toBe(true);
|
|
227
|
-
expect(nav.snapshot).toContain("Consent");
|
|
228
|
-
|
|
229
|
-
const snap = await plugin.snapshot(taskId);
|
|
230
|
-
expect(snap.success).toBe(true);
|
|
231
|
-
|
|
232
|
-
const info = findRef(snap.snapshot, "Reject All");
|
|
233
|
-
if (!info) {
|
|
234
|
-
// Element may be beyond cap on some runs; accept it
|
|
235
|
-
await plugin.cleanup(taskId).catch(noop);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const clickResult = await plugin.click(taskId, info.ref);
|
|
240
|
-
expect(clickResult.success).toBe(true);
|
|
241
|
-
|
|
242
|
-
const after = await plugin.snapshot(taskId);
|
|
243
|
-
expect(dialogCount(after.snapshot)).toBe(0);
|
|
244
|
-
|
|
245
|
-
await plugin.cleanup(taskId).catch(noop);
|
|
246
|
-
} catch (err) {
|
|
247
|
-
failures.push(
|
|
248
|
-
`Iteration ${i}: ${err instanceof Error ? err.message : String(err)}`,
|
|
249
|
-
);
|
|
250
|
-
await plugin.cleanup(taskId).catch(noop);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
expect(failures).toHaveLength(0);
|
|
255
|
-
}, 120_000);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ═══════════════════════════════════════════════════════════════
|
|
259
|
-
// Navigation settle integration — real browser end-to-end
|
|
260
|
-
// ═══════════════════════════════════════════════════════════════
|
|
261
|
-
|
|
262
|
-
describe("Navigation settle — click with delayed nav", () => {
|
|
263
|
-
const TASK_ID = "nav-settle-integration";
|
|
264
|
-
let plugin: ChromiumPlugin;
|
|
265
|
-
|
|
266
|
-
beforeAll(async () => {
|
|
267
|
-
plugin = await createPlugin();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
afterAll(async () => {
|
|
271
|
-
await plugin.cleanupAll().catch(() => {});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("clicks a link with 280ms delayed navigation and returns consistent URL/snapshot", async () => {
|
|
275
|
-
const nav = await plugin.navigate(
|
|
276
|
-
`${serverUrl}/slow-nav`,
|
|
277
|
-
TASK_ID,
|
|
278
|
-
NAV_TIMEOUT,
|
|
279
|
-
);
|
|
280
|
-
expect(nav.success).toBe(true);
|
|
281
|
-
|
|
282
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
283
|
-
expect(snap.success).toBe(true);
|
|
284
|
-
|
|
285
|
-
// Find the "Go to Destination" link ref
|
|
286
|
-
const linkMatch = snap.snapshot.match(/@(e\d+).*?link.*?Destination/);
|
|
287
|
-
expect(linkMatch).toBeTruthy();
|
|
288
|
-
const ref = `@${linkMatch![1]!}`;
|
|
289
|
-
|
|
290
|
-
const result = await plugin.click(TASK_ID, ref);
|
|
291
|
-
expect(result.success).toBe(true);
|
|
292
|
-
|
|
293
|
-
// The navigation completes after 280ms; the settle helper must
|
|
294
|
-
// detect the late-arriving nav and wait for page readiness.
|
|
295
|
-
// URL and snapshot must be consistent — the entire point of the fix.
|
|
296
|
-
expect(result.newUrl).toContain("/slow-nav-destination");
|
|
297
|
-
expect(result.newTitle).toBe("Slow Nav Destination");
|
|
298
|
-
if (result.snapshot) {
|
|
299
|
-
expect(result.snapshot).toContain("Slow Nav Destination");
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
});
|
|
Binary file
|