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
|
@@ -23,16 +23,11 @@ import {
|
|
|
23
23
|
beforeEach,
|
|
24
24
|
afterEach,
|
|
25
25
|
} from "vitest";
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
BrowserPlugin,
|
|
28
|
+
PluginCapabilities,
|
|
29
|
+
} from "../../core/plugin-api.js";
|
|
27
30
|
import { startTestServer, type TestServer } from "./test-server.js";
|
|
28
|
-
import {
|
|
29
|
-
REDDIT_DIALOG_HTML,
|
|
30
|
-
REDDIT_STACKED_HTML,
|
|
31
|
-
REDDIT_ASYNC_HTML,
|
|
32
|
-
REDDIT_FEED_ONLY_HTML,
|
|
33
|
-
findRef,
|
|
34
|
-
dialogCount,
|
|
35
|
-
} from "./reddit-fixture.js";
|
|
36
31
|
|
|
37
32
|
// ─── Options ──────────────────────────────────────────────────────
|
|
38
33
|
|
|
@@ -60,6 +55,23 @@ export interface ContractTestOptions {
|
|
|
60
55
|
* Default: false.
|
|
61
56
|
*/
|
|
62
57
|
navigationSettle?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Vitest test timeout (ms) for every `it()` in this suite.
|
|
61
|
+
* When set, each test gets this timeout instead of vitest's default
|
|
62
|
+
* (15s). Useful for real-browser plugins (e.g. Camoufox) where
|
|
63
|
+
* the first-navigate bridge launch can exceed the default.
|
|
64
|
+
* Default: undefined (vitest default: 15_000).
|
|
65
|
+
*/
|
|
66
|
+
testTimeout?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set true to run the cache-survives-eval-failure parity test.
|
|
70
|
+
* Used by stealth backends (Camoufox) where eval can fail on
|
|
71
|
+
* JS-active pages but the cache path must still work.
|
|
72
|
+
* Default: false.
|
|
73
|
+
*/
|
|
74
|
+
runCacheSurvivesEvalFailure?: boolean;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
// ─── HTML Fixtures ────────────────────────────────────────────────
|
|
@@ -95,15 +107,6 @@ const INTERACTIVE_HTML = `<!DOCTYPE html>
|
|
|
95
107
|
</script>
|
|
96
108
|
</body></html>`;
|
|
97
109
|
|
|
98
|
-
/** Page with images. */
|
|
99
|
-
const IMAGES_HTML = `<!DOCTYPE html>
|
|
100
|
-
<html><head><title>Contract Test — Images</title></head>
|
|
101
|
-
<body>
|
|
102
|
-
<h1>Image Page</h1>
|
|
103
|
-
<img src="/img/logo.png" alt="Logo" width="200" height="50" />
|
|
104
|
-
<img src="/img/photo.jpg" alt="Photo" width="640" height="480" />
|
|
105
|
-
</body></html>`;
|
|
106
|
-
|
|
107
110
|
/** Long page that requires scrolling. */
|
|
108
111
|
const SCROLL_HTML = `<!DOCTYPE html>
|
|
109
112
|
<html><head><title>Contract Test — Scroll</title></head>
|
|
@@ -183,17 +186,10 @@ const NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
|
183
186
|
<html><head><title>Not Found</title></head>
|
|
184
187
|
<body><h1>404 — Page Not Found</h1></body></html>`;
|
|
185
188
|
|
|
186
|
-
/** 1x1 transparent PNG, pre-decoded for image serving. */
|
|
187
|
-
const PIXEL_PNG_BUFFER = Buffer.from(
|
|
188
|
-
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
189
|
-
"base64",
|
|
190
|
-
);
|
|
191
|
-
|
|
192
189
|
// ─── Test Server Router ──────────────────────────────────────────
|
|
193
190
|
|
|
194
191
|
/**
|
|
195
192
|
* Build an HTTP request handler that serves the contract test fixtures.
|
|
196
|
-
* Image paths return a tiny PNG; HTML paths return the fixture.
|
|
197
193
|
*/
|
|
198
194
|
function contractTestHandler(
|
|
199
195
|
req: import("node:http").IncomingMessage,
|
|
@@ -201,21 +197,10 @@ function contractTestHandler(
|
|
|
201
197
|
): void {
|
|
202
198
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
203
199
|
|
|
204
|
-
// Image endpoint
|
|
205
|
-
if (url.pathname.startsWith("/img/")) {
|
|
206
|
-
res.writeHead(200, {
|
|
207
|
-
"Content-Type": "image/png",
|
|
208
|
-
"Content-Length": PIXEL_PNG_BUFFER.length,
|
|
209
|
-
});
|
|
210
|
-
res.end(PIXEL_PNG_BUFFER);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
200
|
// HTML pages
|
|
215
201
|
const pages: Record<string, string> = {
|
|
216
202
|
"/simple": SIMPLE_HTML,
|
|
217
203
|
"/interactive": INTERACTIVE_HTML,
|
|
218
|
-
"/images": IMAGES_HTML,
|
|
219
204
|
"/scroll": SCROLL_HTML,
|
|
220
205
|
"/console": CONSOLE_HTML,
|
|
221
206
|
"/page-a": PAGE_A_HTML,
|
|
@@ -223,10 +208,6 @@ function contractTestHandler(
|
|
|
223
208
|
"/duplicates": DUPLICATE_HTML,
|
|
224
209
|
"/slow-nav": SLOW_NAV_HTML,
|
|
225
210
|
"/slow-nav-destination": SLOW_NAV_TARGET_HTML,
|
|
226
|
-
"/reddit-dialog": REDDIT_DIALOG_HTML,
|
|
227
|
-
"/reddit-stacked": REDDIT_STACKED_HTML,
|
|
228
|
-
"/reddit-async": REDDIT_ASYNC_HTML,
|
|
229
|
-
"/reddit-feed": REDDIT_FEED_ONLY_HTML,
|
|
230
211
|
};
|
|
231
212
|
|
|
232
213
|
const html = pages[url.pathname];
|
|
@@ -268,9 +249,34 @@ export function runContractTests(
|
|
|
268
249
|
realBrowser = false,
|
|
269
250
|
navigateTimeout = 15_000,
|
|
270
251
|
navigationSettle = false,
|
|
252
|
+
testTimeout,
|
|
253
|
+
runCacheSurvivesEvalFailure = false,
|
|
271
254
|
} = options;
|
|
272
255
|
const TASK_ID = "contract-test";
|
|
273
256
|
|
|
257
|
+
// Scoped test creator: when testTimeout is set, every `it()` call in this
|
|
258
|
+
// suite uses that timeout, so slow real-browser plugins (e.g. Camoufox)
|
|
259
|
+
// don't hit the default 15s vitest timeout on first-navigate bridge launch.
|
|
260
|
+
//
|
|
261
|
+
// Typed as simple callables (not vitest's TestAPI / AfterEachAPI) because
|
|
262
|
+
// arrow functions lack the chainable .only/.skip/.runIf properties that
|
|
263
|
+
// the vitest types include. Chainable variants are never needed here —
|
|
264
|
+
// the one .runIf call in this file uses the original `it` directly.
|
|
265
|
+
const _it = testTimeout
|
|
266
|
+
? (name: string, fn: (...args: unknown[]) => unknown): void => {
|
|
267
|
+
it(name, fn, testTimeout);
|
|
268
|
+
}
|
|
269
|
+
: it;
|
|
270
|
+
|
|
271
|
+
// Scoped afterEach: when testTimeout is set, apply it to the cleanup
|
|
272
|
+
// hook too — plugin.cleanupAll() may exceed the default 10s hook timeout
|
|
273
|
+
// for real-browser plugins with expensive teardown.
|
|
274
|
+
const _afterEach = testTimeout
|
|
275
|
+
? (fn: (...args: unknown[]) => unknown): void => {
|
|
276
|
+
afterEach(fn, testTimeout);
|
|
277
|
+
}
|
|
278
|
+
: afterEach;
|
|
279
|
+
|
|
274
280
|
// ═══════════════════════════════════════════════════════════
|
|
275
281
|
// Structural tests — work with any BrowserPlugin (including MockPlugin)
|
|
276
282
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -282,26 +288,22 @@ export function runContractTests(
|
|
|
282
288
|
plugin = await createPlugin();
|
|
283
289
|
});
|
|
284
290
|
|
|
285
|
-
|
|
291
|
+
_afterEach(async () => {
|
|
286
292
|
await plugin.cleanupAll().catch(() => {});
|
|
287
293
|
});
|
|
288
294
|
|
|
289
295
|
// ─── Identity & Capabilities ───────────────────────────
|
|
290
296
|
|
|
291
297
|
describe("identity", () => {
|
|
292
|
-
|
|
298
|
+
_it("has a non-empty name", () => {
|
|
293
299
|
expect(plugin.name).toBeTruthy();
|
|
294
300
|
expect(typeof plugin.name).toBe("string");
|
|
295
301
|
});
|
|
296
302
|
|
|
297
|
-
|
|
303
|
+
_it("has complete capabilities", () => {
|
|
298
304
|
const caps: PluginCapabilities = plugin.capabilities;
|
|
299
305
|
expect(typeof caps.supportsFullPageScreenshot).toBe("boolean");
|
|
300
|
-
expect(typeof caps.supportsConsoleCapture).toBe("boolean");
|
|
301
306
|
expect(typeof caps.supportsJavaScriptEvaluate).toBe("boolean");
|
|
302
|
-
expect(typeof caps.supportsBotDetection).toBe("boolean");
|
|
303
|
-
expect(typeof caps.supportsDialogAutoDismissal).toBe("boolean");
|
|
304
|
-
expect(typeof caps.supportsAbortSignal).toBe("boolean");
|
|
305
307
|
expect(typeof caps.engine).toBe("string");
|
|
306
308
|
});
|
|
307
309
|
});
|
|
@@ -309,7 +311,7 @@ export function runContractTests(
|
|
|
309
311
|
// ─── Navigate ─────────────────────────────────────────
|
|
310
312
|
|
|
311
313
|
describe("navigate()", () => {
|
|
312
|
-
|
|
314
|
+
_it("returns a NavigateResult with required fields", async () => {
|
|
313
315
|
const result = await plugin.navigate(
|
|
314
316
|
"https://example.com/",
|
|
315
317
|
TASK_ID,
|
|
@@ -332,17 +334,20 @@ export function runContractTests(
|
|
|
332
334
|
}
|
|
333
335
|
});
|
|
334
336
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
_it(
|
|
338
|
+
"returns a well-formed result for any URL (success or error)",
|
|
339
|
+
async () => {
|
|
340
|
+
const result = await plugin.navigate("not-a-url", TASK_ID, 30_000);
|
|
337
341
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
expect(result).toBeDefined();
|
|
343
|
+
expect(typeof result.success).toBe("boolean");
|
|
344
|
+
if (!result.success) {
|
|
345
|
+
expect(result.error).toBeTruthy();
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
);
|
|
344
349
|
|
|
345
|
-
|
|
350
|
+
_it("does not set botDetected for normal pages", async () => {
|
|
346
351
|
const result = await plugin.navigate(
|
|
347
352
|
"https://example.com/",
|
|
348
353
|
TASK_ID,
|
|
@@ -358,7 +363,7 @@ export function runContractTests(
|
|
|
358
363
|
// ─── Snapshot ─────────────────────────────────────────
|
|
359
364
|
|
|
360
365
|
describe("snapshot()", () => {
|
|
361
|
-
|
|
366
|
+
_it("returns a SnapshotResult with required fields", async () => {
|
|
362
367
|
// Ensure a session exists first
|
|
363
368
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
364
369
|
|
|
@@ -381,7 +386,7 @@ export function runContractTests(
|
|
|
381
386
|
// ─── Click ────────────────────────────────────────────
|
|
382
387
|
|
|
383
388
|
describe("click()", () => {
|
|
384
|
-
|
|
389
|
+
_it("returns an InteractionResult with required fields", async () => {
|
|
385
390
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
386
391
|
|
|
387
392
|
// Use a ref that might exist; structural test just checks shape
|
|
@@ -407,7 +412,7 @@ export function runContractTests(
|
|
|
407
412
|
// ─── Type ────────────────────────────────────────────
|
|
408
413
|
|
|
409
414
|
describe("type()", () => {
|
|
410
|
-
|
|
415
|
+
_it("returns an InteractionResult with required fields", async () => {
|
|
411
416
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
412
417
|
|
|
413
418
|
const result = await plugin.type(TASK_ID, "@e1", "hello");
|
|
@@ -431,7 +436,7 @@ export function runContractTests(
|
|
|
431
436
|
// ─── Scroll ──────────────────────────────────────────
|
|
432
437
|
|
|
433
438
|
describe("scroll()", () => {
|
|
434
|
-
|
|
439
|
+
_it("returns an InteractionResult with required fields", async () => {
|
|
435
440
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
436
441
|
|
|
437
442
|
const result = await plugin.scroll(TASK_ID, "down");
|
|
@@ -448,7 +453,7 @@ export function runContractTests(
|
|
|
448
453
|
}
|
|
449
454
|
});
|
|
450
455
|
|
|
451
|
-
|
|
456
|
+
_it("accepts 'up' direction", async () => {
|
|
452
457
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
453
458
|
|
|
454
459
|
const result = await plugin.scroll(TASK_ID, "up");
|
|
@@ -461,7 +466,7 @@ export function runContractTests(
|
|
|
461
466
|
// ─── GoBack ──────────────────────────────────────────
|
|
462
467
|
|
|
463
468
|
describe("goBack()", () => {
|
|
464
|
-
|
|
469
|
+
_it("returns an InteractionResult with required fields", async () => {
|
|
465
470
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
466
471
|
|
|
467
472
|
const result = await plugin.goBack(TASK_ID);
|
|
@@ -489,7 +494,7 @@ export function runContractTests(
|
|
|
489
494
|
// ─── Press ───────────────────────────────────────────
|
|
490
495
|
|
|
491
496
|
describe("press()", () => {
|
|
492
|
-
|
|
497
|
+
_it("returns an InteractionResult with required fields", async () => {
|
|
493
498
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
494
499
|
|
|
495
500
|
const result = await plugin.press(TASK_ID, "Enter");
|
|
@@ -510,7 +515,7 @@ export function runContractTests(
|
|
|
510
515
|
// ─── Screenshot ──────────────────────────────────────
|
|
511
516
|
|
|
512
517
|
describe("screenshot()", () => {
|
|
513
|
-
|
|
518
|
+
_it("returns a ScreenshotResult with required fields", async () => {
|
|
514
519
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
515
520
|
|
|
516
521
|
const result = await plugin.screenshot(TASK_ID);
|
|
@@ -528,7 +533,7 @@ export function runContractTests(
|
|
|
528
533
|
}
|
|
529
534
|
});
|
|
530
535
|
|
|
531
|
-
|
|
536
|
+
_it("accepts fullPage option when supported", async () => {
|
|
532
537
|
if (!plugin.capabilities.supportsFullPageScreenshot) return;
|
|
533
538
|
|
|
534
539
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
@@ -545,7 +550,7 @@ export function runContractTests(
|
|
|
545
550
|
// ─── Console ─────────────────────────────────────────
|
|
546
551
|
|
|
547
552
|
describe("getConsoleMessages()", () => {
|
|
548
|
-
|
|
553
|
+
_it("returns a ConsoleMessagesResult with required fields", async () => {
|
|
549
554
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
550
555
|
|
|
551
556
|
const result = await plugin.getConsoleMessages(TASK_ID);
|
|
@@ -568,7 +573,7 @@ export function runContractTests(
|
|
|
568
573
|
// ─── ClearConsole ────────────────────────────────────
|
|
569
574
|
|
|
570
575
|
describe("clearConsole()", () => {
|
|
571
|
-
|
|
576
|
+
_it("does not throw", async () => {
|
|
572
577
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
573
578
|
|
|
574
579
|
// Should resolve without error
|
|
@@ -579,7 +584,7 @@ export function runContractTests(
|
|
|
579
584
|
// ─── Evaluate ────────────────────────────────────────
|
|
580
585
|
|
|
581
586
|
describe("evaluate()", () => {
|
|
582
|
-
|
|
587
|
+
_it("returns an EvaluateResult with required fields", async () => {
|
|
583
588
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
584
589
|
|
|
585
590
|
if (!plugin.capabilities.supportsJavaScriptEvaluate) {
|
|
@@ -606,7 +611,7 @@ export function runContractTests(
|
|
|
606
611
|
// ─── Cookies & storage state ─────────────────────────
|
|
607
612
|
|
|
608
613
|
describe("getCookies()", () => {
|
|
609
|
-
|
|
614
|
+
_it("returns a CookieResult with required fields", async () => {
|
|
610
615
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
611
616
|
|
|
612
617
|
const result = await plugin.getCookies(TASK_ID);
|
|
@@ -625,7 +630,7 @@ export function runContractTests(
|
|
|
625
630
|
}
|
|
626
631
|
});
|
|
627
632
|
|
|
628
|
-
|
|
633
|
+
_it("accepts optional urls filter", async () => {
|
|
629
634
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
630
635
|
|
|
631
636
|
const result = await plugin.getCookies(TASK_ID, [
|
|
@@ -639,7 +644,7 @@ export function runContractTests(
|
|
|
639
644
|
});
|
|
640
645
|
|
|
641
646
|
describe("addCookies()", () => {
|
|
642
|
-
|
|
647
|
+
_it("returns a ResultBase with required fields", async () => {
|
|
643
648
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
644
649
|
|
|
645
650
|
const result = await plugin.addCookies(TASK_ID, [
|
|
@@ -658,7 +663,7 @@ export function runContractTests(
|
|
|
658
663
|
}
|
|
659
664
|
});
|
|
660
665
|
|
|
661
|
-
|
|
666
|
+
_it("rejects cookies without required fields", async () => {
|
|
662
667
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
663
668
|
|
|
664
669
|
// Missing domain and path — may fail at Playwright level
|
|
@@ -673,7 +678,7 @@ export function runContractTests(
|
|
|
673
678
|
});
|
|
674
679
|
|
|
675
680
|
describe("clearCookies()", () => {
|
|
676
|
-
|
|
681
|
+
_it("returns a ResultBase with required fields", async () => {
|
|
677
682
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
678
683
|
|
|
679
684
|
const result = await plugin.clearCookies(TASK_ID);
|
|
@@ -682,7 +687,7 @@ export function runContractTests(
|
|
|
682
687
|
expect(typeof result.success).toBe("boolean");
|
|
683
688
|
});
|
|
684
689
|
|
|
685
|
-
|
|
690
|
+
_it("accepts optional filter params", async () => {
|
|
686
691
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
687
692
|
|
|
688
693
|
const result = await plugin.clearCookies(TASK_ID, {
|
|
@@ -695,7 +700,7 @@ export function runContractTests(
|
|
|
695
700
|
});
|
|
696
701
|
|
|
697
702
|
describe("getStorageState()", () => {
|
|
698
|
-
|
|
703
|
+
_it("returns a StorageStateResult with required fields", async () => {
|
|
699
704
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
700
705
|
|
|
701
706
|
const result = await plugin.getStorageState(TASK_ID);
|
|
@@ -723,7 +728,7 @@ export function runContractTests(
|
|
|
723
728
|
// ─── Cleanup ────────────────────────────────────────
|
|
724
729
|
|
|
725
730
|
describe("cleanup()", () => {
|
|
726
|
-
|
|
731
|
+
_it("does not throw for an active session", async () => {
|
|
727
732
|
await plugin.navigate("https://example.com/", TASK_ID, 30_000);
|
|
728
733
|
|
|
729
734
|
await expect(plugin.cleanup(TASK_ID)).resolves.toBeUndefined();
|
|
@@ -733,7 +738,7 @@ export function runContractTests(
|
|
|
733
738
|
// ─── CleanupAll ──────────────────────────────────────
|
|
734
739
|
|
|
735
740
|
describe("cleanupAll()", () => {
|
|
736
|
-
|
|
741
|
+
_it("does not throw", async () => {
|
|
737
742
|
await expect(plugin.cleanupAll()).resolves.toBeUndefined();
|
|
738
743
|
});
|
|
739
744
|
});
|
|
@@ -761,14 +766,14 @@ export function runContractTests(
|
|
|
761
766
|
plugin = await createPlugin();
|
|
762
767
|
});
|
|
763
768
|
|
|
764
|
-
|
|
769
|
+
_afterEach(async () => {
|
|
765
770
|
await plugin.cleanupAll().catch(() => {});
|
|
766
771
|
});
|
|
767
772
|
|
|
768
773
|
// ─── Navigate ─────────────────────────────────────────
|
|
769
774
|
|
|
770
775
|
describe("navigate() — real pages", () => {
|
|
771
|
-
|
|
776
|
+
_it("loads a page and returns correct title", async () => {
|
|
772
777
|
const result = await plugin.navigate(
|
|
773
778
|
`${server.url}/simple`,
|
|
774
779
|
TASK_ID,
|
|
@@ -780,20 +785,23 @@ export function runContractTests(
|
|
|
780
785
|
expect(result.url).toContain("/simple");
|
|
781
786
|
});
|
|
782
787
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
_it(
|
|
789
|
+
"returns a snapshot with @e refs for interactive elements",
|
|
790
|
+
async () => {
|
|
791
|
+
const result = await plugin.navigate(
|
|
792
|
+
`${server.url}/interactive`,
|
|
793
|
+
TASK_ID,
|
|
794
|
+
navigateTimeout,
|
|
795
|
+
);
|
|
789
796
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
797
|
+
expect(result.success).toBe(true);
|
|
798
|
+
expect(result.snapshot).toBeTruthy();
|
|
799
|
+
// Interactive page should have at least some elements
|
|
800
|
+
expect(result.elementCount).toBeGreaterThan(0);
|
|
801
|
+
},
|
|
802
|
+
);
|
|
795
803
|
|
|
796
|
-
|
|
804
|
+
_it("follows redirects", async () => {
|
|
797
805
|
const result = await plugin.navigate(
|
|
798
806
|
server.url,
|
|
799
807
|
TASK_ID,
|
|
@@ -804,7 +812,7 @@ export function runContractTests(
|
|
|
804
812
|
expect(result.url).toContain("/simple");
|
|
805
813
|
});
|
|
806
814
|
|
|
807
|
-
|
|
815
|
+
_it("handles 404 pages", async () => {
|
|
808
816
|
const result = await plugin.navigate(
|
|
809
817
|
`${server.url}/nonexistent`,
|
|
810
818
|
TASK_ID,
|
|
@@ -820,64 +828,42 @@ export function runContractTests(
|
|
|
820
828
|
// ─── Click ────────────────────────────────────────────
|
|
821
829
|
|
|
822
830
|
describe("click() — real interaction", () => {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
TASK_ID,
|
|
827
|
-
navigateTimeout,
|
|
828
|
-
);
|
|
829
|
-
|
|
830
|
-
// Find the link @e ref from the snapshot
|
|
831
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
832
|
-
expect(snap.success).toBe(true);
|
|
833
|
-
|
|
834
|
-
// Extract a link ref from the snapshot
|
|
835
|
-
const linkMatch = snap.snapshot.match(/@e(\d+)/);
|
|
836
|
-
expect(linkMatch).toBeTruthy();
|
|
837
|
-
|
|
838
|
-
const ref = `@e${linkMatch![1]}`;
|
|
839
|
-
const result = await plugin.click(TASK_ID, ref);
|
|
840
|
-
|
|
841
|
-
expect(result.success).toBe(true);
|
|
842
|
-
// After clicking a link, the page may have changed
|
|
843
|
-
if (result.snapshot) {
|
|
844
|
-
expect(typeof result.snapshot).toBe("string");
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it("clicks duplicate-named links without strict-mode violation", async () => {
|
|
849
|
-
await plugin.navigate(
|
|
850
|
-
`${server.url}/duplicates`,
|
|
851
|
-
TASK_ID,
|
|
852
|
-
navigateTimeout,
|
|
853
|
-
);
|
|
854
|
-
|
|
855
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
856
|
-
expect(snap.success).toBe(true);
|
|
857
|
-
|
|
858
|
-
// Extract all link @e refs from the snapshot
|
|
859
|
-
const linkRefs: string[] = [];
|
|
860
|
-
const linkMatches = snap.snapshot.matchAll(/@(e\d+)\s.*?link/g);
|
|
861
|
-
for (const m of linkMatches) {
|
|
862
|
-
linkRefs.push(`@${m[1]!}`);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
expect(linkRefs.length).toBeGreaterThanOrEqual(3);
|
|
866
|
-
|
|
867
|
-
// Click each duplicate link in sequence — should not throw strict mode
|
|
868
|
-
// Each link with the same name "Same Link" must be clickable via its @e ref
|
|
869
|
-
for (const ref of linkRefs) {
|
|
870
|
-
const result = await plugin.click(TASK_ID, ref);
|
|
871
|
-
expect(result.success).toBe(true);
|
|
872
|
-
|
|
873
|
-
// Navigate back to duplicates page for next click
|
|
831
|
+
_it(
|
|
832
|
+
"clicks duplicate-named links without strict-mode violation",
|
|
833
|
+
async () => {
|
|
874
834
|
await plugin.navigate(
|
|
875
835
|
`${server.url}/duplicates`,
|
|
876
836
|
TASK_ID,
|
|
877
837
|
navigateTimeout,
|
|
878
838
|
);
|
|
879
|
-
|
|
880
|
-
|
|
839
|
+
|
|
840
|
+
const snap = await plugin.snapshot(TASK_ID);
|
|
841
|
+
expect(snap.success).toBe(true);
|
|
842
|
+
|
|
843
|
+
// Extract all link @e refs from the snapshot
|
|
844
|
+
const linkRefs: string[] = [];
|
|
845
|
+
const linkMatches = snap.snapshot.matchAll(/@(e\d+)\s.*?link/g);
|
|
846
|
+
for (const m of linkMatches) {
|
|
847
|
+
linkRefs.push(`@${m[1]!}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
expect(linkRefs.length).toBeGreaterThanOrEqual(3);
|
|
851
|
+
|
|
852
|
+
// Click each duplicate link in sequence — should not throw strict mode
|
|
853
|
+
// Each link with the same name "Same Link" must be clickable via its @e ref
|
|
854
|
+
for (const ref of linkRefs) {
|
|
855
|
+
const result = await plugin.click(TASK_ID, ref);
|
|
856
|
+
expect(result.success).toBe(true);
|
|
857
|
+
|
|
858
|
+
// Navigate back to duplicates page for next click
|
|
859
|
+
await plugin.navigate(
|
|
860
|
+
`${server.url}/duplicates`,
|
|
861
|
+
TASK_ID,
|
|
862
|
+
navigateTimeout,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
);
|
|
881
867
|
});
|
|
882
868
|
|
|
883
869
|
it.runIf(navigationSettle)(
|
|
@@ -911,37 +897,10 @@ export function runContractTests(
|
|
|
911
897
|
},
|
|
912
898
|
);
|
|
913
899
|
|
|
914
|
-
// ─── Type ────────────────────────────────────────────
|
|
915
|
-
|
|
916
|
-
describe("type() — real interaction", () => {
|
|
917
|
-
it("types into an input field", async () => {
|
|
918
|
-
await plugin.navigate(
|
|
919
|
-
`${server.url}/interactive`,
|
|
920
|
-
TASK_ID,
|
|
921
|
-
navigateTimeout,
|
|
922
|
-
);
|
|
923
|
-
|
|
924
|
-
// Find an input ref
|
|
925
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
926
|
-
expect(snap.success).toBe(true);
|
|
927
|
-
|
|
928
|
-
// Look for textbox in the snapshot (input fields are role=textbox)
|
|
929
|
-
const inputMatch = snap.snapshot.match(/textbox[^\n]*@e(\d+)/);
|
|
930
|
-
if (!inputMatch) {
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const ref = `@e${inputMatch[1]}`;
|
|
935
|
-
const result = await plugin.type(TASK_ID, ref, "hello world");
|
|
936
|
-
|
|
937
|
-
expect(result.success).toBe(true);
|
|
938
|
-
});
|
|
939
|
-
});
|
|
940
|
-
|
|
941
900
|
// ─── Scroll ──────────────────────────────────────────
|
|
942
901
|
|
|
943
902
|
describe("scroll() — real interaction", () => {
|
|
944
|
-
|
|
903
|
+
_it("scrolls down on a long page", async () => {
|
|
945
904
|
await plugin.navigate(`${server.url}/scroll`, TASK_ID, navigateTimeout);
|
|
946
905
|
|
|
947
906
|
const result = await plugin.scroll(TASK_ID, "down");
|
|
@@ -949,7 +908,7 @@ export function runContractTests(
|
|
|
949
908
|
expect(result.success).toBe(true);
|
|
950
909
|
});
|
|
951
910
|
|
|
952
|
-
|
|
911
|
+
_it("scrolls back up", async () => {
|
|
953
912
|
await plugin.navigate(`${server.url}/scroll`, TASK_ID, navigateTimeout);
|
|
954
913
|
|
|
955
914
|
await plugin.scroll(TASK_ID, "down");
|
|
@@ -957,12 +916,35 @@ export function runContractTests(
|
|
|
957
916
|
|
|
958
917
|
expect(result.success).toBe(true);
|
|
959
918
|
});
|
|
919
|
+
|
|
920
|
+
_it("scrolls to the bottom after repeated scrolls", async () => {
|
|
921
|
+
await plugin.navigate(`${server.url}/scroll`, TASK_ID, navigateTimeout);
|
|
922
|
+
|
|
923
|
+
let bottomReached = false;
|
|
924
|
+
const MAX_SCROLLS = 100;
|
|
925
|
+
for (let i = 0; i < MAX_SCROLLS; i++) {
|
|
926
|
+
const scroll = await plugin.scroll(TASK_ID, "down");
|
|
927
|
+
expect(scroll.success).toBe(true);
|
|
928
|
+
|
|
929
|
+
const evalResult = await plugin.evaluate(
|
|
930
|
+
TASK_ID,
|
|
931
|
+
"document.getElementById('bottom-marker').getBoundingClientRect().top < window.innerHeight",
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
if (evalResult.success && evalResult.result === true) {
|
|
935
|
+
bottomReached = true;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
expect(bottomReached).toBe(true);
|
|
941
|
+
});
|
|
960
942
|
});
|
|
961
943
|
|
|
962
944
|
// ─── GoBack ──────────────────────────────────────────
|
|
963
945
|
|
|
964
946
|
describe("goBack() — real navigation", () => {
|
|
965
|
-
|
|
947
|
+
_it("navigates back to the previous page", async () => {
|
|
966
948
|
// Navigate to page A
|
|
967
949
|
await plugin.navigate(`${server.url}/page-a`, TASK_ID, navigateTimeout);
|
|
968
950
|
|
|
@@ -993,7 +975,7 @@ export function runContractTests(
|
|
|
993
975
|
// ─── Press ───────────────────────────────────────────
|
|
994
976
|
|
|
995
977
|
describe("press() — real interaction", () => {
|
|
996
|
-
|
|
978
|
+
_it("presses Enter key", async () => {
|
|
997
979
|
await plugin.navigate(
|
|
998
980
|
`${server.url}/interactive`,
|
|
999
981
|
TASK_ID,
|
|
@@ -1009,7 +991,7 @@ export function runContractTests(
|
|
|
1009
991
|
// ─── Screenshot ──────────────────────────────────────
|
|
1010
992
|
|
|
1011
993
|
describe("screenshot() — real capture", () => {
|
|
1012
|
-
|
|
994
|
+
_it("returns a valid JPEG data URI", async () => {
|
|
1013
995
|
await plugin.navigate(`${server.url}/simple`, TASK_ID, navigateTimeout);
|
|
1014
996
|
|
|
1015
997
|
const result = await plugin.screenshot(TASK_ID);
|
|
@@ -1027,9 +1009,7 @@ export function runContractTests(
|
|
|
1027
1009
|
// ─── Console ─────────────────────────────────────────
|
|
1028
1010
|
|
|
1029
1011
|
describe("console — real capture", () => {
|
|
1030
|
-
|
|
1031
|
-
if (!plugin.capabilities.supportsConsoleCapture) return;
|
|
1032
|
-
|
|
1012
|
+
_it("captures console messages from the page", async () => {
|
|
1033
1013
|
await plugin.navigate(
|
|
1034
1014
|
`${server.url}/console`,
|
|
1035
1015
|
TASK_ID,
|
|
@@ -1041,13 +1021,13 @@ export function runContractTests(
|
|
|
1041
1021
|
expect(result.success).toBe(true);
|
|
1042
1022
|
expect(result.messages.length).toBeGreaterThan(0);
|
|
1043
1023
|
|
|
1044
|
-
const texts = result.messages.map((m) => m.text);
|
|
1045
|
-
expect(
|
|
1024
|
+
const texts = result.messages.map((m: { text: string }) => m.text);
|
|
1025
|
+
expect(
|
|
1026
|
+
texts.some((t: string) => t.includes("hello from console")),
|
|
1027
|
+
).toBe(true);
|
|
1046
1028
|
});
|
|
1047
1029
|
|
|
1048
|
-
|
|
1049
|
-
if (!plugin.capabilities.supportsConsoleCapture) return;
|
|
1050
|
-
|
|
1030
|
+
_it("clears console messages", async () => {
|
|
1051
1031
|
await plugin.navigate(
|
|
1052
1032
|
`${server.url}/console`,
|
|
1053
1033
|
TASK_ID,
|
|
@@ -1065,7 +1045,7 @@ export function runContractTests(
|
|
|
1065
1045
|
// ─── Evaluate ────────────────────────────────────────
|
|
1066
1046
|
|
|
1067
1047
|
describe("evaluate() — real execution", () => {
|
|
1068
|
-
|
|
1048
|
+
_it("evaluates a JavaScript expression", async () => {
|
|
1069
1049
|
if (!plugin.capabilities.supportsJavaScriptEvaluate) return;
|
|
1070
1050
|
|
|
1071
1051
|
await plugin.navigate(`${server.url}/simple`, TASK_ID, navigateTimeout);
|
|
@@ -1076,7 +1056,7 @@ export function runContractTests(
|
|
|
1076
1056
|
expect(result.result).toBe("Contract Test — Simple");
|
|
1077
1057
|
});
|
|
1078
1058
|
|
|
1079
|
-
|
|
1059
|
+
_it("evaluates arithmetic", async () => {
|
|
1080
1060
|
if (!plugin.capabilities.supportsJavaScriptEvaluate) return;
|
|
1081
1061
|
|
|
1082
1062
|
await plugin.navigate(`${server.url}/simple`, TASK_ID, navigateTimeout);
|
|
@@ -1087,7 +1067,7 @@ export function runContractTests(
|
|
|
1087
1067
|
expect(result.result).toBe(5);
|
|
1088
1068
|
});
|
|
1089
1069
|
|
|
1090
|
-
|
|
1070
|
+
_it("returns error for invalid expressions", async () => {
|
|
1091
1071
|
if (!plugin.capabilities.supportsJavaScriptEvaluate) return;
|
|
1092
1072
|
|
|
1093
1073
|
await plugin.navigate(`${server.url}/simple`, TASK_ID, navigateTimeout);
|
|
@@ -1105,7 +1085,7 @@ export function runContractTests(
|
|
|
1105
1085
|
// ─── Lifecycle ───────────────────────────────────────
|
|
1106
1086
|
|
|
1107
1087
|
describe("lifecycle — real sessions", () => {
|
|
1108
|
-
|
|
1088
|
+
_it("creates and cleans up a session", async () => {
|
|
1109
1089
|
await plugin.navigate(`${server.url}/simple`, TASK_ID, navigateTimeout);
|
|
1110
1090
|
|
|
1111
1091
|
// Session should be working
|
|
@@ -1120,7 +1100,7 @@ export function runContractTests(
|
|
|
1120
1100
|
expect(afterCleanup.success).toBe(false);
|
|
1121
1101
|
});
|
|
1122
1102
|
|
|
1123
|
-
|
|
1103
|
+
_it("isolates sessions by taskId", async () => {
|
|
1124
1104
|
const TASK_A = "task-a";
|
|
1125
1105
|
const TASK_B = "task-b";
|
|
1126
1106
|
|
|
@@ -1148,143 +1128,43 @@ export function runContractTests(
|
|
|
1148
1128
|
});
|
|
1149
1129
|
});
|
|
1150
1130
|
|
|
1151
|
-
// ───
|
|
1152
|
-
|
|
1153
|
-
describe("
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
expect(dialogCount(snap.snapshot)).toBeGreaterThanOrEqual(1);
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
it("'Reject All' (nested SVG) is clickable", async () => {
|
|
1171
|
-
await plugin.navigate(
|
|
1172
|
-
`${server.url}/reddit-dialog`,
|
|
1173
|
-
TASK_ID,
|
|
1174
|
-
navigateTimeout,
|
|
1175
|
-
);
|
|
1176
|
-
|
|
1177
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
1178
|
-
expect(snap.success).toBe(true);
|
|
1179
|
-
|
|
1180
|
-
const info = findRef(snap.snapshot, "Reject All");
|
|
1181
|
-
if (!info) {
|
|
1182
|
-
// If the element is beyond the cap, skip
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
const result = await plugin.click(TASK_ID, info.ref);
|
|
1187
|
-
expect(result.success).toBe(true);
|
|
1188
|
-
|
|
1189
|
-
const after = await plugin.snapshot(TASK_ID);
|
|
1190
|
-
expect(after.success).toBe(true);
|
|
1191
|
-
expect(dialogCount(after.snapshot)).toBe(0);
|
|
1192
|
-
});
|
|
1131
|
+
// ─── Cache survives eval failure (parity invariant) ─
|
|
1132
|
+
|
|
1133
|
+
describe("cache survives eval failure", () => {
|
|
1134
|
+
// ponytail: This test asserts a structural invariant — the cache
|
|
1135
|
+
// is not invalidated by an eval call, even one that fails. It does
|
|
1136
|
+
// NOT force an actual eval failure (which is engine-dependent and
|
|
1137
|
+
// not reproducible in CI without a challenge page). A passing eval
|
|
1138
|
+
// call must not invalidate the cache either — the invariant is that
|
|
1139
|
+
// cache-query and eval are independent paths.
|
|
1140
|
+
//
|
|
1141
|
+
// Full regression guard for the eval-failure class: if navigate
|
|
1142
|
+
// populated an N-element a11y tree, the cache-query path works even
|
|
1143
|
+
// when the eval path fails (e.g. on Camoufox challenge pages where
|
|
1144
|
+
// the main-world context is destroyed).
|
|
1145
|
+
_it("cache query works after evaluate call", async () => {
|
|
1146
|
+
if (!runCacheSurvivesEvalFailure) return;
|
|
1193
1147
|
|
|
1194
|
-
it("'Accept All' (plain button) is clickable", async () => {
|
|
1195
1148
|
await plugin.navigate(
|
|
1196
|
-
`${server.url}/
|
|
1149
|
+
`${server.url}/interactive`,
|
|
1197
1150
|
TASK_ID,
|
|
1198
1151
|
navigateTimeout,
|
|
1199
1152
|
);
|
|
1200
1153
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const info = findRef(snap.snapshot, "Accept All");
|
|
1205
|
-
if (!info) {
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
const result = await plugin.click(TASK_ID, info.ref);
|
|
1210
|
-
expect(result.success).toBe(true);
|
|
1211
|
-
|
|
1212
|
-
const after = await plugin.snapshot(TASK_ID);
|
|
1213
|
-
expect(after.success).toBe(true);
|
|
1214
|
-
expect(dialogCount(after.snapshot)).toBe(0);
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
it("stacked dialogs close in sequence", async () => {
|
|
1218
|
-
const STACKED_TASK = "stacked-test";
|
|
1219
|
-
|
|
1220
|
-
await plugin.navigate(
|
|
1221
|
-
`${server.url}/reddit-stacked`,
|
|
1222
|
-
STACKED_TASK,
|
|
1223
|
-
navigateTimeout,
|
|
1224
|
-
);
|
|
1225
|
-
|
|
1226
|
-
// First snapshot — consent dialog should be visible
|
|
1227
|
-
const snap1 = await plugin.snapshot(STACKED_TASK);
|
|
1154
|
+
// Snapshot → a11y tree populated
|
|
1155
|
+
const snap1 = await plugin.snapshot(TASK_ID);
|
|
1228
1156
|
expect(snap1.success).toBe(true);
|
|
1229
|
-
expect(
|
|
1157
|
+
expect(snap1.elementCount).toBeGreaterThan(0);
|
|
1230
1158
|
|
|
1231
|
-
//
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
await plugin.cleanup(STACKED_TASK);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
const result1 = await plugin.click(STACKED_TASK, info1.ref);
|
|
1238
|
-
expect(result1.success).toBe(true);
|
|
1159
|
+
// Evaluate (may succeed or fail — not forced). The
|
|
1160
|
+
// invariant: an eval call does not invalidate the cache.
|
|
1161
|
+
await plugin.evaluate(TASK_ID, "document.title");
|
|
1239
1162
|
|
|
1240
|
-
//
|
|
1241
|
-
const snap2 = await plugin.snapshot(
|
|
1163
|
+
// Re-run snapshot — must still succeed with same content
|
|
1164
|
+
const snap2 = await plugin.snapshot(TASK_ID);
|
|
1242
1165
|
expect(snap2.success).toBe(true);
|
|
1243
|
-
expect(snap2.
|
|
1244
|
-
|
|
1245
|
-
// Click "Dismiss" to close the welcome dialog
|
|
1246
|
-
const info2 = findRef(snap2.snapshot, "Dismiss");
|
|
1247
|
-
if (!info2) {
|
|
1248
|
-
await plugin.cleanup(STACKED_TASK);
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
const result2 = await plugin.click(STACKED_TASK, info2.ref);
|
|
1252
|
-
expect(result2.success).toBe(true);
|
|
1253
|
-
|
|
1254
|
-
// Third snapshot — no dialogs should remain
|
|
1255
|
-
const snap3 = await plugin.snapshot(STACKED_TASK);
|
|
1256
|
-
expect(snap3.success).toBe(true);
|
|
1257
|
-
expect(dialogCount(snap3.snapshot)).toBe(0);
|
|
1258
|
-
|
|
1259
|
-
await plugin.cleanup(STACKED_TASK);
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
it("async dialog eventually appears", async () => {
|
|
1263
|
-
const ASYNC_TASK = "async-test";
|
|
1264
|
-
|
|
1265
|
-
await plugin.navigate(
|
|
1266
|
-
`${server.url}/reddit-async`,
|
|
1267
|
-
ASYNC_TASK,
|
|
1268
|
-
navigateTimeout,
|
|
1269
|
-
);
|
|
1270
|
-
|
|
1271
|
-
// Wait for the async dialog to appear (setTimeout 500ms)
|
|
1272
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1273
|
-
|
|
1274
|
-
const snap = await plugin.snapshot(ASYNC_TASK);
|
|
1275
|
-
expect(snap.success).toBe(true);
|
|
1276
|
-
expect(snap.snapshot).toContain("Consent");
|
|
1277
|
-
|
|
1278
|
-
const info = findRef(snap.snapshot, "Reject All");
|
|
1279
|
-
if (!info) {
|
|
1280
|
-
await plugin.cleanup(ASYNC_TASK);
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
const result = await plugin.click(ASYNC_TASK, info.ref);
|
|
1285
|
-
expect(result.success).toBe(true);
|
|
1286
|
-
|
|
1287
|
-
await plugin.cleanup(ASYNC_TASK);
|
|
1166
|
+
expect(snap2.elementCount).toBeGreaterThan(0);
|
|
1167
|
+
expect(snap2.snapshot).toBe(snap1.snapshot);
|
|
1288
1168
|
});
|
|
1289
1169
|
});
|
|
1290
1170
|
});
|