pi-lean-dimension 0.1.0 → 0.2.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.
Files changed (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. 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
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. 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
  75. 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
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. 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
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. 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
- });