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.
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
@@ -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(taskId: string, expression: string): Promise<EvaluateResult> {
201
- this.record("evaluate", [taskId, expression]);
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
+ }