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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Availability probe + path resolver for shipped Python bridge backends.
|
|
3
|
+
*
|
|
4
|
+
* Returns resolved paths and a `describeIfAvailable` gate so every
|
|
5
|
+
* Python backend test file collapses to ~15 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { describe } from "vitest";
|
|
12
|
+
|
|
13
|
+
/** Optional engine-binary check for Python backends. */
|
|
14
|
+
export interface PyBackendHarnessOptions {
|
|
15
|
+
/** When "firefox", also probes that the Firefox browser binary exists. */
|
|
16
|
+
engine?: "chromium" | "firefox";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Result of {@link createPyBackendTestHarness}. */
|
|
20
|
+
export interface PyBackendTestHarnessResult {
|
|
21
|
+
/** Resolved path to `<bridgeName>/bridge.py`. */
|
|
22
|
+
bridgeScript: string;
|
|
23
|
+
/** Resolved path to the Python venv interpreter. */
|
|
24
|
+
pythonPath: string;
|
|
25
|
+
/** True when all prerequisites are met (python + bridge + optional engine). */
|
|
26
|
+
prerequisitesMet: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* `describe` when prerequisites are met, `describe.skip` otherwise.
|
|
29
|
+
* Drop-in for the `describeIfAvailable` gate pattern.
|
|
30
|
+
*/
|
|
31
|
+
describeIfAvailable: typeof describe | typeof describe.skip;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Probe a shipped Python bridge backend and return resolved paths +
|
|
36
|
+
* a ready-to-use `describeIfAvailable` gate.
|
|
37
|
+
*
|
|
38
|
+
* The test caller destructures what it needs:
|
|
39
|
+
* ```
|
|
40
|
+
* const { bridgeScript, pythonPath, describeIfAvailable } = createPyBackendTestHarness("chromium-py");
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Paths resolve relative to `__tests__/helpers/` using the conventional
|
|
44
|
+
* monorepo layout: `backends/<name>/bridge.py`, `backends/python-base/.venv/bin/python3`.
|
|
45
|
+
*
|
|
46
|
+
* @param bridgeName Backend directory name (e.g. `"chromium-py"`, `"firefox-py"`).
|
|
47
|
+
* @param options Optional engine check.
|
|
48
|
+
*/
|
|
49
|
+
export function createPyBackendTestHarness(
|
|
50
|
+
bridgeName: string,
|
|
51
|
+
options?: PyBackendHarnessOptions,
|
|
52
|
+
): PyBackendTestHarnessResult {
|
|
53
|
+
const bridgeScript = resolve(
|
|
54
|
+
__dirname,
|
|
55
|
+
`../../backends/${bridgeName}/bridge.py`,
|
|
56
|
+
);
|
|
57
|
+
const pythonPath = resolve(
|
|
58
|
+
__dirname,
|
|
59
|
+
"../../backends/python-base/.venv/bin/python3",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const pythonAvailable = (() => {
|
|
63
|
+
if (!existsSync(pythonPath)) return false;
|
|
64
|
+
const result = spawnSync(pythonPath, ["--version"], {
|
|
65
|
+
stdio: "ignore",
|
|
66
|
+
timeout: 5_000,
|
|
67
|
+
});
|
|
68
|
+
return result.status === 0;
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
const bridgeExists = existsSync(bridgeScript);
|
|
72
|
+
|
|
73
|
+
let browserAvailable = true;
|
|
74
|
+
if (options?.engine === "firefox" && pythonAvailable) {
|
|
75
|
+
browserAvailable = (() => {
|
|
76
|
+
try {
|
|
77
|
+
const result = spawnSync(
|
|
78
|
+
pythonPath,
|
|
79
|
+
[
|
|
80
|
+
"-c",
|
|
81
|
+
"from playwright.sync_api import sync_playwright; " +
|
|
82
|
+
"p = sync_playwright().start(); " +
|
|
83
|
+
"import os; print(os.path.exists(p.firefox.executable_path)); " +
|
|
84
|
+
"p.stop()",
|
|
85
|
+
],
|
|
86
|
+
{ stdio: "pipe", timeout: 10_000 },
|
|
87
|
+
);
|
|
88
|
+
return (
|
|
89
|
+
result.status === 0 && result.stdout.toString().trim() === "True"
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const prerequisitesMet = pythonAvailable && bridgeExists && browserAvailable;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
bridgeScript,
|
|
101
|
+
pythonPath,
|
|
102
|
+
prerequisitesMet,
|
|
103
|
+
describeIfAvailable: prerequisitesMet ? describe : describe.skip,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load and validate plugin configuration from a specific settings file path.
|
|
3
|
+
*
|
|
4
|
+
* Moved out of core/plugin-config.ts so the function doesn't ship in the
|
|
5
|
+
* npm tarball (core/ is in the package `files` allowlist).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
parsePluginConfig,
|
|
10
|
+
type PluginConfigLoadResult,
|
|
11
|
+
DEFAULT_BACKEND_ROOTS,
|
|
12
|
+
} from "../../core/plugin-config.js";
|
|
13
|
+
import { readSettingsFile } from "../../core/shared/settings-reader.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Same parsing/validation as {@link import("../../core/plugin-config.js").parsePluginConfig},
|
|
17
|
+
* but reads from a specific file path instead of the merged global + project
|
|
18
|
+
* settings. One-shot read — no caching.
|
|
19
|
+
*
|
|
20
|
+
* The file must have the same top-level shape as `settings.json`:
|
|
21
|
+
* ```json
|
|
22
|
+
* { "browser": { "plugins": [...] } }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* When the file is absent or has no `browser` section, falls back to the
|
|
26
|
+
* default plugin list (same as `parsePluginConfig(undefined, …)` with no settings).
|
|
27
|
+
*
|
|
28
|
+
* @param path Absolute or relative path to a settings.json file.
|
|
29
|
+
* @param roots Optional ordered list of backend roots to resolve
|
|
30
|
+
* plugin `dir` values against. Defaults to
|
|
31
|
+
* `DEFAULT_BACKEND_ROOTS`.
|
|
32
|
+
*/
|
|
33
|
+
export function loadPluginConfigFromFile(
|
|
34
|
+
path: string,
|
|
35
|
+
roots?: readonly string[],
|
|
36
|
+
): PluginConfigLoadResult {
|
|
37
|
+
const raw = readSettingsFile(path);
|
|
38
|
+
const browserConfig = raw["browser"];
|
|
39
|
+
const effectiveRoots = roots ?? DEFAULT_BACKEND_ROOTS;
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
!browserConfig ||
|
|
43
|
+
typeof browserConfig !== "object" ||
|
|
44
|
+
Array.isArray(browserConfig)
|
|
45
|
+
) {
|
|
46
|
+
return parsePluginConfig(undefined, effectiveRoots);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parsePluginConfig(
|
|
50
|
+
browserConfig as Record<string, unknown>,
|
|
51
|
+
effectiveRoots,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -19,10 +19,10 @@ import type {
|
|
|
19
19
|
ClearCookiesOptions,
|
|
20
20
|
StorageStateResult,
|
|
21
21
|
ResultBase,
|
|
22
|
-
} from "../../core/plugin-api";
|
|
23
|
-
import type { PluginConfig } from "../../core/plugin-config";
|
|
24
|
-
import { DEFAULT_CAPABILITIES } from "../../core/plugin-api";
|
|
25
|
-
import type { AriaCachedNode } from "../../core/shared/accessibility-tree";
|
|
22
|
+
} from "../../core/plugin-api.js";
|
|
23
|
+
import type { PluginConfig } from "../../core/plugin-config.js";
|
|
24
|
+
import { DEFAULT_CAPABILITIES } from "../../core/plugin-api.js";
|
|
25
|
+
import type { AriaCachedNode } from "../../core/shared/accessibility-tree.js";
|
|
26
26
|
|
|
27
27
|
export class MockPlugin implements BrowserPlugin {
|
|
28
28
|
readonly name: string;
|
|
@@ -197,8 +197,12 @@ export class MockPlugin implements BrowserPlugin {
|
|
|
197
197
|
throw new Error("clearConsole failed");
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
async evaluate(
|
|
201
|
-
|
|
200
|
+
async evaluate(
|
|
201
|
+
taskId: string,
|
|
202
|
+
expression: string,
|
|
203
|
+
readOnly?: boolean,
|
|
204
|
+
): Promise<EvaluateResult> {
|
|
205
|
+
this.record("evaluate", [taskId, expression, readOnly]);
|
|
202
206
|
if (this.shouldThrow.has("evaluate")) throw new Error("evaluate failed");
|
|
203
207
|
return {
|
|
204
208
|
success: true,
|
|
@@ -239,7 +243,7 @@ export class MockPlugin implements BrowserPlugin {
|
|
|
239
243
|
|
|
240
244
|
async addCookies(
|
|
241
245
|
taskId: string,
|
|
242
|
-
cookies: import("../../core/plugin-api").Cookie[],
|
|
246
|
+
cookies: import("../../core/plugin-api.js").Cookie[],
|
|
243
247
|
): Promise<ResultBase> {
|
|
244
248
|
this.record("addCookies", [taskId, cookies]);
|
|
245
249
|
if (this.shouldThrow.has("addCookies"))
|
|
@@ -36,6 +36,10 @@ def main() -> None:
|
|
|
36
36
|
if method == "ping":
|
|
37
37
|
write_response(req_id, result="pong")
|
|
38
38
|
|
|
39
|
+
elif method == "browser.init":
|
|
40
|
+
# Phase 0: accept plugin config forwarded from the adapter after ping.
|
|
41
|
+
write_response(req_id, result={"ok": True})
|
|
42
|
+
|
|
39
43
|
elif method == "shutdown":
|
|
40
44
|
write_response(req_id, result="shutting_down")
|
|
41
45
|
sys.stdout.flush()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared persistence suite — a generic, backend-agnostic test helper that
|
|
3
|
+
* validates the cookie-persistence lifecycle across navigations.
|
|
4
|
+
*
|
|
5
|
+
* Every interactive browser backend (shipped Python, user-installed stealth)
|
|
6
|
+
* should produce the same persistence behaviour: navigate → consent dialog
|
|
7
|
+
* visible → click Accept All → cookie set → re-navigate → dialog absent
|
|
8
|
+
* → storage-state.json on disk → third navigate still clean.
|
|
9
|
+
*
|
|
10
|
+
* This helper owns the six `it` blocks and the consent-dialog click flow
|
|
11
|
+
* that every persistence file previously copy-pasted. The caller is
|
|
12
|
+
* responsible for:
|
|
13
|
+
*
|
|
14
|
+
* - Prerequisite probing (path-based venv probe or `probeUserBackend`)
|
|
15
|
+
* - Adapter construction (choosing shipped vs contributed paths)
|
|
16
|
+
* - Test server lifecycle (start/stop)
|
|
17
|
+
* - Passing `describe` or `describe.skip` depending on availability
|
|
18
|
+
*
|
|
19
|
+
* The helper references no backend names and no stealth-engine identifiers.
|
|
20
|
+
* It is git-tracked and policy-clean.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { it, expect, beforeAll, afterAll } from "vitest";
|
|
26
|
+
import type { BrowserPlugin } from "../../core/plugin-api.js";
|
|
27
|
+
import { sessionManager } from "../../core/shared/session-manager.js";
|
|
28
|
+
import {
|
|
29
|
+
loadStorageState,
|
|
30
|
+
deleteStorageState,
|
|
31
|
+
} from "../../core/shared/storage-state.js";
|
|
32
|
+
|
|
33
|
+
// ─── Types ─────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Callable signature that matches both `describe` and `describe.skip`.
|
|
37
|
+
* The caller picks which one to pass based on their prerequisite probe.
|
|
38
|
+
*/
|
|
39
|
+
export type DescribeFn = (
|
|
40
|
+
name: string,
|
|
41
|
+
fn: () => void,
|
|
42
|
+
timeout?: number,
|
|
43
|
+
) => void;
|
|
44
|
+
|
|
45
|
+
/** Options for {@link runPersistenceSuite}. */
|
|
46
|
+
export interface PersistenceSuiteOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Backend name, used in `it()` titles and profile prefix for
|
|
49
|
+
* attribution (e.g. `"my-backend"`).
|
|
50
|
+
*/
|
|
51
|
+
name: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Getter that returns the base URL of the local HTTP test server
|
|
55
|
+
* serving {@link COOKIE_PERSISTENCE_HTML} (the consent-dialog page).
|
|
56
|
+
*
|
|
57
|
+
* Implemented as a getter (rather than a plain string) because the
|
|
58
|
+
* URL is only known after the caller's `beforeAll` starts the test
|
|
59
|
+
* server. The helper calls this lazily from its own `beforeAll` /
|
|
60
|
+
* `it` blocks, by which point the caller's `beforeAll` has set the
|
|
61
|
+
* value.
|
|
62
|
+
*
|
|
63
|
+
* Example:
|
|
64
|
+
* ```ts
|
|
65
|
+
* let serverUrl: string;
|
|
66
|
+
* beforeAll(async () => { serverUrl = (await startTestServer(h)).url; });
|
|
67
|
+
* runPersistenceSuite(describe, { getServerUrl: () => serverUrl, ... });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
getServerUrl: () => string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Factory that constructs and **initialises** a fresh plugin instance
|
|
74
|
+
* for the suite. Called once from the helper's `beforeAll`.
|
|
75
|
+
*
|
|
76
|
+
* The caller owns probe + adapter construction (shipped backends use
|
|
77
|
+
* path-based venv probing; contributed backends use
|
|
78
|
+
* `probeUserBackend`, and should pass the correct paths into the
|
|
79
|
+
* adapter constructor here.
|
|
80
|
+
*/
|
|
81
|
+
createPlugin: () => Promise<BrowserPlugin>;
|
|
82
|
+
|
|
83
|
+
/** Per-navigate timeout in milliseconds (default 30_000). */
|
|
84
|
+
navTimeout?: number;
|
|
85
|
+
|
|
86
|
+
/** Timeout for the enclosing `describe` block (default 120_000). */
|
|
87
|
+
describeTimeout?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Public API ─────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run the six persistence `it` blocks against the provided plugin.
|
|
94
|
+
*
|
|
95
|
+
* The describe block is **gated by the caller**: pass a real `describe`
|
|
96
|
+
* when prerequisites are met, or `describe.skip` to skip the entire
|
|
97
|
+
* suite. The helper does **not** probe — that is the caller's job.
|
|
98
|
+
*
|
|
99
|
+
* Six `it` blocks (all parametrised with `opts.name` for attribution):
|
|
100
|
+
* 1. Navigate with named profile → consent dialog visible
|
|
101
|
+
* 2. Click Accept All → cookie set, dialog closes
|
|
102
|
+
* 3. Create session + set persistState (simulates the router's setup)
|
|
103
|
+
* 4. Re-navigate with same profile → dialog absent (cookies survived)
|
|
104
|
+
* 5. storage-state.json on disk with the consent cookie
|
|
105
|
+
* 6. Third navigate → still no dialog (persistence confirmed)
|
|
106
|
+
*
|
|
107
|
+
* @param describeFn `describe` when available, `describe.skip` otherwise.
|
|
108
|
+
* @param opts Suite options. See {@link PersistenceSuiteOptions}.
|
|
109
|
+
*/
|
|
110
|
+
export function runPersistenceSuite(
|
|
111
|
+
describeFn: DescribeFn,
|
|
112
|
+
opts: PersistenceSuiteOptions,
|
|
113
|
+
): void {
|
|
114
|
+
const {
|
|
115
|
+
name,
|
|
116
|
+
getServerUrl,
|
|
117
|
+
createPlugin,
|
|
118
|
+
navTimeout = 30_000,
|
|
119
|
+
describeTimeout = 120_000,
|
|
120
|
+
} = opts;
|
|
121
|
+
|
|
122
|
+
const TASK_ID = `${name}-persistence`;
|
|
123
|
+
|
|
124
|
+
describeFn(
|
|
125
|
+
`${name} cookie persistence across navigations`,
|
|
126
|
+
() => {
|
|
127
|
+
const TEST_PROFILE = `${name}-persist-${Date.now()}`;
|
|
128
|
+
|
|
129
|
+
let plugin: BrowserPlugin;
|
|
130
|
+
|
|
131
|
+
beforeAll(async () => {
|
|
132
|
+
plugin = await createPlugin();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterAll(async () => {
|
|
136
|
+
await plugin.cleanupAll().catch(() => {});
|
|
137
|
+
deleteStorageState(TEST_PROFILE);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it(`${name}: navigates with a named profile — consent dialog is visible`, async () => {
|
|
141
|
+
const nav = await plugin.navigate(getServerUrl(), TASK_ID, navTimeout, {
|
|
142
|
+
profileName: TEST_PROFILE,
|
|
143
|
+
profileMode: "named",
|
|
144
|
+
});
|
|
145
|
+
expect(nav.success).toBe(true);
|
|
146
|
+
expect(nav.title).toContain("Cookie Persistence Test");
|
|
147
|
+
expect(nav.snapshot).toContain("Consent");
|
|
148
|
+
expect(nav.snapshot).toContain("Accept All");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it(`${name}: clicks Accept All — cookie set, consent dialog closes`, async () => {
|
|
152
|
+
const snap = await plugin.snapshot(TASK_ID);
|
|
153
|
+
expect(snap.success).toBe(true);
|
|
154
|
+
|
|
155
|
+
// Find "Accept All" button in the snapshot and extract its @e ref
|
|
156
|
+
const btnMatch = snap.snapshot.match(/@(e\d+)\b.*?button.*?Accept All/);
|
|
157
|
+
expect(btnMatch).toBeTruthy();
|
|
158
|
+
if (!btnMatch) return;
|
|
159
|
+
|
|
160
|
+
const ref = `@${btnMatch[1]!}`;
|
|
161
|
+
const clickResult = await plugin.click(TASK_ID, ref);
|
|
162
|
+
expect(clickResult.success).toBe(true);
|
|
163
|
+
|
|
164
|
+
// Verify dialog is gone from snapshot
|
|
165
|
+
if (clickResult.snapshot) {
|
|
166
|
+
expect(clickResult.snapshot).not.toContain("Consent");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it(`${name}: creates session and sets persistState (simulating the router's setup)`, async () => {
|
|
171
|
+
sessionManager.createSession(TASK_ID, name);
|
|
172
|
+
const session = sessionManager.getSession(TASK_ID);
|
|
173
|
+
expect(session).toBeDefined();
|
|
174
|
+
if (session) {
|
|
175
|
+
session.persistState = true;
|
|
176
|
+
session.profileName = TEST_PROFILE;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it(`${name}: navigates again with same profile — consent dialog does NOT reappear`, async () => {
|
|
181
|
+
const nav = await plugin.navigate(getServerUrl(), TASK_ID, navTimeout, {
|
|
182
|
+
profileName: TEST_PROFILE,
|
|
183
|
+
profileMode: "named",
|
|
184
|
+
});
|
|
185
|
+
expect(nav.success).toBe(true);
|
|
186
|
+
|
|
187
|
+
// Consent dialog should NOT be present — cookies survived in-context
|
|
188
|
+
expect(nav.snapshot).not.toContain("Consent");
|
|
189
|
+
expect(nav.snapshot).not.toContain("Accept All");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it(`${name}: storage-state.json exists on disk with the consent cookie`, async () => {
|
|
193
|
+
const state = loadStorageState(TEST_PROFILE);
|
|
194
|
+
expect(state).not.toBeNull();
|
|
195
|
+
expect(state!.cookies.length).toBeGreaterThan(0);
|
|
196
|
+
|
|
197
|
+
const consentCookie = state!.cookies.find(
|
|
198
|
+
(c: { name: string; value: string }) =>
|
|
199
|
+
c.name === "consent" && c.value === "accepted",
|
|
200
|
+
);
|
|
201
|
+
expect(consentCookie).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it(`${name}: third navigate also has no consent dialog (persistence confirmed)`, async () => {
|
|
205
|
+
const nav = await plugin.navigate(getServerUrl(), TASK_ID, navTimeout, {
|
|
206
|
+
profileName: TEST_PROFILE,
|
|
207
|
+
profileMode: "named",
|
|
208
|
+
});
|
|
209
|
+
expect(nav.success).toBe(true);
|
|
210
|
+
|
|
211
|
+
// Dialog should still be absent
|
|
212
|
+
expect(nav.snapshot).not.toContain("Consent");
|
|
213
|
+
expect(nav.snapshot).not.toContain("Accept All");
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
describeTimeout,
|
|
217
|
+
);
|
|
218
|
+
}
|