pi-lean-dimension 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -1,203 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
* Chromium-Py Cookie Persistence Integration Test — end-to-end validation
|
|
3
|
-
* of Bug A (router loads storage state) and Bug B (plugin saves state on
|
|
4
|
-
* re-navigate) for the Python adapter / chromium-py bridge.
|
|
5
|
-
*
|
|
6
|
-
* Uses a live Chromium browser via the Chromium-Py bridge against a local
|
|
7
|
-
* HTTP test server that serves a page with a cookie-based consent dialog.
|
|
8
|
-
*
|
|
9
|
-
* The test:
|
|
10
|
-
* 1. Navigates with a named profile — dialog visible
|
|
11
|
-
* 2. Clicks "Accept All" — sets a consent cookie
|
|
12
|
-
* 3. Navigates again with the same profile — dialog should NOT reappear
|
|
13
|
-
* because the bridge reuses the BrowserContext (so cookies survive), and
|
|
14
|
-
* `_persistState` saves the state to disk for cross-session persistence.
|
|
15
|
-
*
|
|
16
|
-
* Auto-skips the entire suite when Python/chromium-py prerequisites are
|
|
17
|
-
* unavailable.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
21
|
-
import { resolve } from "node:path";
|
|
22
|
-
import { existsSync } from "node:fs";
|
|
23
|
-
import { spawnSync } from "node:child_process";
|
|
24
|
-
|
|
1
|
+
import { describe, beforeAll, afterAll } from "vitest";
|
|
25
2
|
import { PythonPluginAdapter } from "../backends/python-adapter.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "./helpers/test-server.js";
|
|
30
|
-
import {
|
|
31
|
-
loadStorageState,
|
|
32
|
-
deleteStorageState,
|
|
33
|
-
} from "../core/shared/storage-state.js";
|
|
34
|
-
import { sessionManager } from "../core/shared/session-manager.js";
|
|
35
|
-
|
|
36
|
-
// ─── Paths ──────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
const BRIDGE_SCRIPT = resolve(__dirname, "../backends/chromium-py/bridge.py");
|
|
39
|
-
const PYTHON_PATH = resolve(
|
|
40
|
-
__dirname,
|
|
41
|
-
"../backends/python-base/.venv/bin/python3",
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// ─── Prerequisites check — skip gracefully if absent ────────────────
|
|
45
|
-
|
|
46
|
-
const prerequisitesMet = (() => {
|
|
47
|
-
if (!existsSync(PYTHON_PATH)) return false;
|
|
48
|
-
if (!existsSync(BRIDGE_SCRIPT)) return false;
|
|
49
|
-
const result = spawnSync(PYTHON_PATH, ["--version"], {
|
|
50
|
-
stdio: "ignore",
|
|
51
|
-
timeout: 5_000,
|
|
52
|
-
});
|
|
53
|
-
if (result.status !== 0) return false;
|
|
54
|
-
|
|
55
|
-
// Quick check: can we import playwright?
|
|
56
|
-
const check = spawnSync(
|
|
57
|
-
PYTHON_PATH,
|
|
58
|
-
["-c", "import playwright; print('ok')"],
|
|
59
|
-
{ stdio: "pipe", timeout: 5_000 },
|
|
60
|
-
);
|
|
61
|
-
return check.status === 0 && check.stdout.toString().trim() === "ok";
|
|
62
|
-
})();
|
|
63
|
-
|
|
64
|
-
const describeIfAvailable = prerequisitesMet ? describe : describe.skip;
|
|
65
|
-
|
|
66
|
-
// ─── Test fixture HTML (imported from shared helper) ───────────────
|
|
67
|
-
// See ``COOKIE_PERSISTENCE_HTML`` in ``helpers/test-server.ts``.
|
|
68
|
-
|
|
69
|
-
// ─── Test Server Setup (always runs, server is cheap to start/stop) ─
|
|
70
|
-
|
|
71
|
-
let serverUrl: string;
|
|
72
|
-
let stopServer: () => Promise<void>;
|
|
3
|
+
import { createPyBackendTestHarness } from "./helpers/create-py-backend-harness.js";
|
|
4
|
+
import { startCookiePersistenceServer } from "./helpers/test-server.js";
|
|
5
|
+
import { runPersistenceSuite } from "./helpers/persistence-suite.js";
|
|
73
6
|
|
|
7
|
+
const { bridgeScript, pythonPath, prerequisitesMet } =
|
|
8
|
+
createPyBackendTestHarness("chromium-py");
|
|
9
|
+
let serverUrl: string, stopServer: () => Promise<void>;
|
|
74
10
|
beforeAll(async () => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
79
|
-
res.end(COOKIE_PERSISTENCE_HTML);
|
|
80
|
-
} else {
|
|
81
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
82
|
-
res.end("404");
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
serverUrl = server.url;
|
|
86
|
-
stopServer = server.stop;
|
|
11
|
+
const s = await startCookiePersistenceServer();
|
|
12
|
+
serverUrl = s.url;
|
|
13
|
+
stopServer = s.stop;
|
|
87
14
|
});
|
|
88
|
-
|
|
89
15
|
afterAll(async () => {
|
|
90
16
|
await stopServer();
|
|
91
17
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
() => {
|
|
103
|
-
const TEST_PROFILE = `chromium-py-persist-${Date.now()}`;
|
|
104
|
-
|
|
105
|
-
let plugin: PythonPluginAdapter;
|
|
106
|
-
|
|
107
|
-
beforeAll(async () => {
|
|
108
|
-
plugin = new PythonPluginAdapter("chromium-py-persist", {
|
|
109
|
-
bridgeScript: BRIDGE_SCRIPT,
|
|
110
|
-
pythonPath: PYTHON_PATH,
|
|
111
|
-
});
|
|
112
|
-
await plugin.init();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
afterAll(async () => {
|
|
116
|
-
await plugin.cleanupAll().catch(() => {});
|
|
117
|
-
deleteStorageState(TEST_PROFILE);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("navigates with a named profile — consent dialog is visible", async () => {
|
|
121
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
122
|
-
profileName: TEST_PROFILE,
|
|
123
|
-
profileMode: "named",
|
|
124
|
-
});
|
|
125
|
-
expect(nav.success).toBe(true);
|
|
126
|
-
expect(nav.title).toContain("Cookie Persistence Test");
|
|
127
|
-
expect(nav.snapshot).toContain("Consent");
|
|
128
|
-
expect(nav.snapshot).toContain("Accept All");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("clicks Accept All — cookie set, consent dialog closes", async () => {
|
|
132
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
133
|
-
expect(snap.success).toBe(true);
|
|
134
|
-
|
|
135
|
-
// Find "Accept All" button in the snapshot and extract its @e ref
|
|
136
|
-
const btnMatch = snap.snapshot.match(/@(e\d+)\b.*?button.*?Accept All/);
|
|
137
|
-
expect(btnMatch).toBeTruthy();
|
|
138
|
-
if (!btnMatch) return;
|
|
139
|
-
|
|
140
|
-
const ref = `@${btnMatch[1]!}`;
|
|
141
|
-
const clickResult = await plugin.click(TASK_ID, ref);
|
|
142
|
-
expect(clickResult.success).toBe(true);
|
|
143
|
-
|
|
144
|
-
// Verify dialog is gone from snapshot
|
|
145
|
-
if (clickResult.snapshot) {
|
|
146
|
-
expect(clickResult.snapshot).not.toContain("Consent");
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("creates session and sets persistState (simulating the router's setup)", async () => {
|
|
151
|
-
sessionManager.createSession(TASK_ID, "chromium-py-persist");
|
|
152
|
-
const session = sessionManager.getSession(TASK_ID);
|
|
153
|
-
expect(session).toBeDefined();
|
|
154
|
-
if (session) {
|
|
155
|
-
session.persistState = true;
|
|
156
|
-
session.profileName = TEST_PROFILE;
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("navigates again with same profile — consent dialog does NOT reappear", async () => {
|
|
161
|
-
// This navigate triggers navigate() which:
|
|
162
|
-
// 1. Detects re-navigate (task already in _pages)
|
|
163
|
-
// 2. Calls _persistState → saves cookies to disk via getStorageState RPC
|
|
164
|
-
// 3. Sends browser.navigate RPC → bridge reuses existing context
|
|
165
|
-
// The cookie set during the previous click is still present in the
|
|
166
|
-
// reused BrowserContext, so the dialog should not reappear.
|
|
167
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
168
|
-
profileName: TEST_PROFILE,
|
|
169
|
-
profileMode: "named",
|
|
170
|
-
});
|
|
171
|
-
expect(nav.success).toBe(true);
|
|
172
|
-
|
|
173
|
-
// Consent dialog should NOT be present — cookies survived in-context
|
|
174
|
-
expect(nav.snapshot).not.toContain("Consent");
|
|
175
|
-
expect(nav.snapshot).not.toContain("Accept All");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("storage-state.json exists on disk with the consent cookie", async () => {
|
|
179
|
-
const state = loadStorageState(TEST_PROFILE);
|
|
180
|
-
expect(state).not.toBeNull();
|
|
181
|
-
expect(state!.cookies.length).toBeGreaterThan(0);
|
|
182
|
-
|
|
183
|
-
const consentCookie = state!.cookies.find(
|
|
184
|
-
(c: { name: string; value: string }) =>
|
|
185
|
-
c.name === "consent" && c.value === "accepted",
|
|
186
|
-
);
|
|
187
|
-
expect(consentCookie).toBeDefined();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("third navigate also has no consent dialog (persistence confirmed)", async () => {
|
|
191
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
192
|
-
profileName: TEST_PROFILE,
|
|
193
|
-
profileMode: "named",
|
|
194
|
-
});
|
|
195
|
-
expect(nav.success).toBe(true);
|
|
196
|
-
|
|
197
|
-
// Dialog should still be absent
|
|
198
|
-
expect(nav.snapshot).not.toContain("Consent");
|
|
199
|
-
expect(nav.snapshot).not.toContain("Accept All");
|
|
200
|
-
});
|
|
18
|
+
runPersistenceSuite(prerequisitesMet ? describe : describe.skip, {
|
|
19
|
+
name: "chromium-py",
|
|
20
|
+
getServerUrl: () => serverUrl,
|
|
21
|
+
createPlugin: async () => {
|
|
22
|
+
const p = new PythonPluginAdapter("chromium-py-persist", {
|
|
23
|
+
bridgeScript,
|
|
24
|
+
pythonPath,
|
|
25
|
+
});
|
|
26
|
+
await p.init();
|
|
27
|
+
return p;
|
|
201
28
|
},
|
|
202
|
-
|
|
203
|
-
);
|
|
29
|
+
});
|
|
@@ -1,87 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract tests for the Chromium-Py backend — validates that the
|
|
3
|
-
* PythonPluginAdapter + Chromium-Py bridge satisfies the BrowserPlugin
|
|
4
|
-
* contract against a real browser.
|
|
5
|
-
*
|
|
6
|
-
* Phase B6: Test Chromium-Py against the contract harness.
|
|
7
|
-
*
|
|
8
|
-
* This test file instantiates a PythonPluginAdapter pointing at the
|
|
9
|
-
* Chromium-Py bridge script and runs both structural and behavioral
|
|
10
|
-
* contract tests. The Python subprocess is managed by the adapter;
|
|
11
|
-
* no manual setup is needed beyond having Playwright Chromium installed
|
|
12
|
-
* in the Python virtual environment.
|
|
13
|
-
*
|
|
14
|
-
* Prerequisites:
|
|
15
|
-
* - Python venv at backends/python-base/.venv with playwright installed
|
|
16
|
-
* - Playwright Chromium browsers installed (playwright install chromium)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { resolve } from "node:path";
|
|
20
|
-
import { existsSync } from "node:fs";
|
|
21
|
-
import { spawnSync } from "node:child_process";
|
|
22
|
-
|
|
23
1
|
import { PythonPluginAdapter } from "../backends/python-adapter.js";
|
|
24
2
|
import { runContractTests } from "./helpers/plugin-contract.js";
|
|
3
|
+
import { createPyBackendTestHarness } from "./helpers/create-py-backend-harness.js";
|
|
25
4
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const BRIDGE_SCRIPT = resolve(__dirname, "../backends/chromium-py/bridge.py");
|
|
29
|
-
|
|
30
|
-
const PYTHON_PATH = resolve(
|
|
31
|
-
__dirname,
|
|
32
|
-
"../backends/python-base/.venv/bin/python3",
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
// ─── Environment check ───────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const pythonAvailable = (() => {
|
|
38
|
-
if (!existsSync(PYTHON_PATH)) return false;
|
|
39
|
-
const result = spawnSync(PYTHON_PATH, ["--version"], {
|
|
40
|
-
stdio: "ignore",
|
|
41
|
-
timeout: 5_000,
|
|
42
|
-
});
|
|
43
|
-
return result.status === 0;
|
|
44
|
-
})();
|
|
45
|
-
|
|
46
|
-
const bridgeExists = existsSync(BRIDGE_SCRIPT);
|
|
47
|
-
|
|
48
|
-
// ─── Plugin factory ─────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Create a PythonPluginAdapter that drives the Chromium-Py bridge.
|
|
52
|
-
*
|
|
53
|
-
* The adapter is lazy — the Python subprocess won't start until the
|
|
54
|
-
* first operation. We pass the venv Python path so the bridge has
|
|
55
|
-
* access to playwright and the shared pi_browser_bridge library.
|
|
56
|
-
*/
|
|
57
|
-
function createChromiumPyPlugin(): PythonPluginAdapter {
|
|
58
|
-
return new PythonPluginAdapter("chromium-py", {
|
|
59
|
-
bridgeScript: BRIDGE_SCRIPT,
|
|
60
|
-
pythonPath: PYTHON_PATH,
|
|
61
|
-
// Chromium-Py supports everything except AbortSignal
|
|
62
|
-
capabilities: {
|
|
63
|
-
supportsFullPageScreenshot: true,
|
|
64
|
-
supportsConsoleCapture: true,
|
|
65
|
-
supportsJavaScriptEvaluate: true,
|
|
66
|
-
supportsBotDetection: true,
|
|
67
|
-
supportsDialogAutoDismissal: true,
|
|
68
|
-
supportsAbortSignal: false,
|
|
69
|
-
engine: "chromium",
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── Run contract tests ──────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
// Skip the entire suite if prerequisites aren't met (CI without
|
|
77
|
-
// Playwright installed, local dev without venv, etc.)
|
|
78
|
-
const describeIfAvailable =
|
|
79
|
-
pythonAvailable && bridgeExists ? describe : describe.skip;
|
|
5
|
+
const { bridgeScript, pythonPath, describeIfAvailable } =
|
|
6
|
+
createPyBackendTestHarness("chromium-py");
|
|
80
7
|
|
|
81
8
|
describeIfAvailable("Chromium-Py contract tests", () => {
|
|
82
|
-
runContractTests(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
9
|
+
runContractTests(
|
|
10
|
+
"chromium-py",
|
|
11
|
+
() =>
|
|
12
|
+
new PythonPluginAdapter("chromium-py", {
|
|
13
|
+
bridgeScript,
|
|
14
|
+
pythonPath,
|
|
15
|
+
capabilities: {
|
|
16
|
+
supportsFullPageScreenshot: true,
|
|
17
|
+
supportsJavaScriptEvaluate: true,
|
|
18
|
+
engine: "chromium",
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
{ realBrowser: true, navigateTimeout: 30_000, navigationSettle: true },
|
|
22
|
+
);
|
|
87
23
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
import { describe } from "vitest";
|
|
4
|
+
import { runContractTests } from "./helpers/plugin-contract.js";
|
|
5
|
+
import { ChromiumPlugin } from "../backends/chromium/index.js";
|
|
6
|
+
|
|
7
|
+
const describeIfAvailable = (() => {
|
|
8
|
+
try {
|
|
9
|
+
return existsSync(chromium.executablePath()) ? describe : describe.skip;
|
|
10
|
+
} catch {
|
|
11
|
+
return describe.skip;
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
describeIfAvailable(
|
|
16
|
+
"ChromiumPlugin contract tests",
|
|
17
|
+
() => {
|
|
18
|
+
runContractTests("chromium", () => new ChromiumPlugin(), {
|
|
19
|
+
realBrowser: true,
|
|
20
|
+
navigateTimeout: 30_000,
|
|
21
|
+
navigationSettle: true,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
60_000,
|
|
25
|
+
);
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invisible-py-specific behavioral tests (user-backends stealth engine,
|
|
3
|
+
* pattern #2) — exercises lifecycle pattern #2, multi-context
|
|
4
|
+
* deadlock avoidance, and the back-nav referrer workaround.
|
|
5
|
+
*
|
|
6
|
+
* The shared BrowserPlugin contract suite for invisible-py runs via
|
|
7
|
+
* `run-contributed-suites.test.ts` (`CONTRIB_RUN=1`). This file adds
|
|
8
|
+
* assertions unique to pattern #2 that the generic suite cannot cover.
|
|
9
|
+
*
|
|
10
|
+
* This is the **only** test suite that exercises lifecycle pattern #2
|
|
11
|
+
* (engine owns its own Playwright instance via `_ensure_playwright` /
|
|
12
|
+
* `_maybe_stop_playwright`). Camoufox uses pattern #1 (accepts an
|
|
13
|
+
* external Playwright), so this suite serves as the unique probe for
|
|
14
|
+
* the pattern-#2 code path.
|
|
15
|
+
*
|
|
16
|
+
* Prerequisites (all auto-detected via probeUserBackend):
|
|
17
|
+
* - `~/.pi/agent/pi-lean-portal/user-backends/invisible-py/bridge.py`
|
|
18
|
+
* exists on disk.
|
|
19
|
+
* - `.venv/bin/python3` runs and has `invisible_playwright` installed.
|
|
20
|
+
* - Patched Firefox binary is fetched and runnable.
|
|
21
|
+
*
|
|
22
|
+
* Auto-skips the entire suite when prerequisites are absent.
|
|
23
|
+
*
|
|
24
|
+
* Assertions:
|
|
25
|
+
* - **Pattern #2 lifecycle no-leak** — init/navigate/cleanupAll then
|
|
26
|
+
* init/navigate again proves `_maybe_stop_playwright` actually tears
|
|
27
|
+
* down the InvisiblePlaywright context manager and a second init
|
|
28
|
+
* doesn't spawn a second Playwright instance.
|
|
29
|
+
* - **Multi-context no-deadlock** — two sessions created concurrently
|
|
30
|
+
* both navigate successfully, validating the `_capture_user_agent =
|
|
31
|
+
* False` workaround that prevents the patched Firefox binary from
|
|
32
|
+
* leaking implicit BrowserContexts.
|
|
33
|
+
* - **Back-nav honest limitation** — `goBack()` after a link click
|
|
34
|
+
* succeeds via the `document.referrer` workaround; `goBack()` after
|
|
35
|
+
* a direct navigation (no referrer) returns `success: false` with
|
|
36
|
+
* the documented limitation error.
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
42
|
+
|
|
43
|
+
import { PythonPluginAdapter } from "../../../backends/python-adapter.js";
|
|
44
|
+
import { startTestServer } from "../../helpers/test-server.js";
|
|
45
|
+
|
|
46
|
+
import { probeUserBackend } from "../../helpers/probe-user-backend.js";
|
|
47
|
+
|
|
48
|
+
// ─── Prerequisites check via probeUserBackend ──────────────────────
|
|
49
|
+
|
|
50
|
+
const probe = probeUserBackend("invisible-py");
|
|
51
|
+
|
|
52
|
+
const invisiblePyAvailable = probe.available;
|
|
53
|
+
|
|
54
|
+
// ─── Plugin factory ─────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a PythonPluginAdapter that drives the invisible-py bridge
|
|
58
|
+
* from the user-backends directory.
|
|
59
|
+
*
|
|
60
|
+
* The adapter is lazy — the Python subprocess won't start until the
|
|
61
|
+
* first operation. Capabilities advertise `engine: "firefox"` because
|
|
62
|
+
* invisible-py wraps a patched Firefox binary.
|
|
63
|
+
*/
|
|
64
|
+
function createInvisiblePyPlugin(): PythonPluginAdapter {
|
|
65
|
+
return new PythonPluginAdapter("invisible-py", {
|
|
66
|
+
bridgeScript: probe.bridgePath,
|
|
67
|
+
pythonPath: probe.venvPython,
|
|
68
|
+
capabilities: {
|
|
69
|
+
supportsFullPageScreenshot: true,
|
|
70
|
+
supportsJavaScriptEvaluate: true,
|
|
71
|
+
engine: "firefox",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Test Server Setup (used for invisible-py-specific tests) ────────
|
|
77
|
+
|
|
78
|
+
const PAGE_A_HTML = `<!DOCTYPE html>
|
|
79
|
+
<html><head><title>Page A</title></head>
|
|
80
|
+
<body>
|
|
81
|
+
<h1>Page A</h1>
|
|
82
|
+
<p>This is page A.</p>
|
|
83
|
+
<a href="/page-b" id="link-to-b">Go to Page B</a>
|
|
84
|
+
</body></html>`;
|
|
85
|
+
|
|
86
|
+
const PAGE_B_HTML = `<!DOCTYPE html>
|
|
87
|
+
<html><head><title>Page B</title></head>
|
|
88
|
+
<body>
|
|
89
|
+
<h1>Page B</h1>
|
|
90
|
+
<p>You reached page B via a link from page A.</p>
|
|
91
|
+
<a href="/direct-nav" id="link-to-direct">Go to Direct Nav</a>
|
|
92
|
+
</body></html>`;
|
|
93
|
+
|
|
94
|
+
const DIRECT_NAV_HTML = `<!DOCTYPE html>
|
|
95
|
+
<html><head><title>Direct Nav Page</title></head>
|
|
96
|
+
<body>
|
|
97
|
+
<h1>Direct Navigation Page</h1>
|
|
98
|
+
<p>Navigated here directly, no referrer.</p>
|
|
99
|
+
</body></html>`;
|
|
100
|
+
|
|
101
|
+
const SIMPLE_HTML = `<!DOCTYPE html>
|
|
102
|
+
<html><head><title>Simple Page</title></head>
|
|
103
|
+
<body><h1>Simple</h1></body></html>`;
|
|
104
|
+
|
|
105
|
+
const MULTI_CONTEXT_HTML = `<!DOCTYPE html>
|
|
106
|
+
<html><head><title>Multi Context Test</title></head>
|
|
107
|
+
<body><h1>Multi Context Test Page</h1>
|
|
108
|
+
<p id="marker">Session marker: loading complete.</p></body></html>`;
|
|
109
|
+
|
|
110
|
+
let serverUrl: string;
|
|
111
|
+
let stopServer: () => Promise<void>;
|
|
112
|
+
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
const server = await startTestServer((req, res) => {
|
|
115
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
116
|
+
switch (url.pathname) {
|
|
117
|
+
case "/page-a":
|
|
118
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
119
|
+
res.end(PAGE_A_HTML);
|
|
120
|
+
break;
|
|
121
|
+
case "/page-b":
|
|
122
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
123
|
+
res.end(PAGE_B_HTML);
|
|
124
|
+
break;
|
|
125
|
+
case "/direct-nav":
|
|
126
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
127
|
+
res.end(DIRECT_NAV_HTML);
|
|
128
|
+
break;
|
|
129
|
+
case "/simple":
|
|
130
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
131
|
+
res.end(SIMPLE_HTML);
|
|
132
|
+
break;
|
|
133
|
+
case "/multi-context":
|
|
134
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
135
|
+
res.end(MULTI_CONTEXT_HTML);
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
139
|
+
res.end("404");
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
serverUrl = server.url;
|
|
143
|
+
stopServer = server.stop;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterAll(async () => {
|
|
147
|
+
await stopServer();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const describeIfAvailable = invisiblePyAvailable ? describe : describe.skip;
|
|
151
|
+
|
|
152
|
+
describeIfAvailable("Invisible-Py contract tests", () => {
|
|
153
|
+
// ── Invisible-py-specific assertions ────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// These go beyond the generic contract suite to exercise
|
|
156
|
+
// architecture-under-test features unique to pattern-#2 backends.
|
|
157
|
+
|
|
158
|
+
describe("Invisible-py-specific behaviors", () => {
|
|
159
|
+
let plugin: PythonPluginAdapter;
|
|
160
|
+
|
|
161
|
+
// Each test in this block runs with its own browser sessions.
|
|
162
|
+
// The patched Firefox binary limits concurrent BrowserContexts
|
|
163
|
+
// to ~2 per InvisiblePlaywright instance, so we tear down and
|
|
164
|
+
// re-init between tests to prevent session leaks — otherwise
|
|
165
|
+
// a test that creates 2 sessions blocks the next test's first
|
|
166
|
+
// navigate from creating a third context.
|
|
167
|
+
beforeEach(async () => {
|
|
168
|
+
plugin = createInvisiblePyPlugin();
|
|
169
|
+
await plugin.init();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
afterEach(async () => {
|
|
173
|
+
await plugin.cleanupAll().catch(() => {});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── Pattern #2 lifecycle: no Playwright leak across cycles ─
|
|
177
|
+
|
|
178
|
+
it("pattern #2 lifecycle: init→navigate→cleanupAll→init→navigate succeeds without leak", async () => {
|
|
179
|
+
// Long timeout: two init cycles + two 30s navigations + cleanupAll.
|
|
180
|
+
// Each individual navigate call has a 30s timeout, so the test
|
|
181
|
+
// itself needs 60s+ to accommodate both cycles plus bridge ops.
|
|
182
|
+
// First cycle
|
|
183
|
+
const nav1 = await plugin.navigate(
|
|
184
|
+
`${serverUrl}/simple`,
|
|
185
|
+
"lifecycle-cycle-1",
|
|
186
|
+
30_000,
|
|
187
|
+
);
|
|
188
|
+
expect(nav1.success).toBe(true);
|
|
189
|
+
expect(nav1.title).toContain("Simple Page");
|
|
190
|
+
|
|
191
|
+
// Full teardown
|
|
192
|
+
await plugin.cleanupAll();
|
|
193
|
+
|
|
194
|
+
// Second cycle — init again (must not leak a Playwright instance)
|
|
195
|
+
await plugin.init();
|
|
196
|
+
|
|
197
|
+
const nav2 = await plugin.navigate(
|
|
198
|
+
`${serverUrl}/simple`,
|
|
199
|
+
"lifecycle-cycle-2",
|
|
200
|
+
30_000,
|
|
201
|
+
);
|
|
202
|
+
expect(nav2.success).toBe(true);
|
|
203
|
+
expect(nav2.title).toContain("Simple Page");
|
|
204
|
+
}, 120_000);
|
|
205
|
+
|
|
206
|
+
// ── Multi-context no-deadlock ─────────────────────────────
|
|
207
|
+
//
|
|
208
|
+
// Validates the `_capture_user_agent = False` workaround that
|
|
209
|
+
// prevents the patched Firefox binary from leaking implicit
|
|
210
|
+
// BrowserContexts. Two independent sessions must both navigate
|
|
211
|
+
// without hanging.
|
|
212
|
+
|
|
213
|
+
it("multi-context: two independent sessions both navigate without deadlock", async () => {
|
|
214
|
+
// Long timeout: two sequential 30s navigations on a patched
|
|
215
|
+
// binary that may be slower than stock. 90s keeps the budget
|
|
216
|
+
// comfortable while still failing fast on a real deadlock.
|
|
217
|
+
const navA = await plugin.navigate(
|
|
218
|
+
`${serverUrl}/multi-context`,
|
|
219
|
+
"multi-context-a",
|
|
220
|
+
30_000,
|
|
221
|
+
);
|
|
222
|
+
expect(navA.success).toBe(true);
|
|
223
|
+
expect(navA.title).toContain("Multi Context Test");
|
|
224
|
+
|
|
225
|
+
const navB = await plugin.navigate(
|
|
226
|
+
`${serverUrl}/multi-context`,
|
|
227
|
+
"multi-context-b",
|
|
228
|
+
30_000,
|
|
229
|
+
);
|
|
230
|
+
expect(navB.success).toBe(true);
|
|
231
|
+
expect(navB.title).toContain("Multi Context Test");
|
|
232
|
+
}, 90_000);
|
|
233
|
+
|
|
234
|
+
// ── Back-nav honest limitation ───────────────────────────
|
|
235
|
+
//
|
|
236
|
+
// invisible-py wraps a patched Firefox binary that disables
|
|
237
|
+
// bfcache. Standard `page.go_back()` is non-functional. The
|
|
238
|
+
// bridge implements a `document.referrer` workaround that
|
|
239
|
+
// handles link-click → back scenarios but cannot handle
|
|
240
|
+
// direct navigations (no referrer).
|
|
241
|
+
|
|
242
|
+
it("goBack succeeds after a link click (referrer workaround)", async () => {
|
|
243
|
+
// Long timeout: navigate + snapshot + click + snapshot + goBack
|
|
244
|
+
// + snapshot — six bridge round-trips on a slow binary.
|
|
245
|
+
// Navigate to page A
|
|
246
|
+
const navA = await plugin.navigate(
|
|
247
|
+
`${serverUrl}/page-a`,
|
|
248
|
+
"back-ref-a",
|
|
249
|
+
30_000,
|
|
250
|
+
);
|
|
251
|
+
expect(navA.success).toBe(true);
|
|
252
|
+
expect(navA.title).toContain("Page A");
|
|
253
|
+
|
|
254
|
+
// Click the link to page B
|
|
255
|
+
const snapA = await plugin.snapshot("back-ref-a");
|
|
256
|
+
expect(snapA.success).toBe(true);
|
|
257
|
+
|
|
258
|
+
const linkMatch = snapA.snapshot.match(/@(e\d+)\b.*?link.*?Go to Page B/);
|
|
259
|
+
expect(linkMatch).toBeTruthy();
|
|
260
|
+
if (!linkMatch) return;
|
|
261
|
+
|
|
262
|
+
const ref = `@${linkMatch[1]!}`;
|
|
263
|
+
const clickResult = await plugin.click("back-ref-a", ref);
|
|
264
|
+
expect(clickResult.success).toBe(true);
|
|
265
|
+
|
|
266
|
+
// Verify we're on page B
|
|
267
|
+
const snapB = await plugin.snapshot("back-ref-a");
|
|
268
|
+
expect(snapB.snapshot).toContain("Page B");
|
|
269
|
+
|
|
270
|
+
// Go back via referrer workaround
|
|
271
|
+
const backResult = await plugin.goBack("back-ref-a");
|
|
272
|
+
expect(backResult.success).toBe(true);
|
|
273
|
+
|
|
274
|
+
// Verify we're back on page A
|
|
275
|
+
const snapAfterBack = await plugin.snapshot("back-ref-a");
|
|
276
|
+
expect(snapAfterBack.snapshot).toContain("Page A");
|
|
277
|
+
}, 90_000);
|
|
278
|
+
|
|
279
|
+
it("goBack after a direct navigation returns success: false (limitation)", async () => {
|
|
280
|
+
// navigate + goBack — two bridge calls, each may be slow.
|
|
281
|
+
// Navigate directly (no link click, no referrer)
|
|
282
|
+
const nav = await plugin.navigate(
|
|
283
|
+
`${serverUrl}/direct-nav`,
|
|
284
|
+
"back-direct",
|
|
285
|
+
30_000,
|
|
286
|
+
);
|
|
287
|
+
expect(nav.success).toBe(true);
|
|
288
|
+
expect(nav.title).toContain("Direct Nav Page");
|
|
289
|
+
|
|
290
|
+
// Attempt goBack — should fail because there's no referrer
|
|
291
|
+
const backResult = await plugin.goBack("back-direct");
|
|
292
|
+
expect(backResult.success).toBe(false);
|
|
293
|
+
|
|
294
|
+
// Should include a descriptive error message about the limitation
|
|
295
|
+
expect(typeof backResult.error).toBe("string");
|
|
296
|
+
expect((backResult.error as string).length).toBeGreaterThan(0);
|
|
297
|
+
}, 60_000);
|
|
298
|
+
});
|
|
299
|
+
});
|