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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared browser-data validation tests.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that ``browser-data.json`` is well-formed and contains all
|
|
5
|
+
* expected keys with the correct types. Both the TypeScript loader
|
|
6
|
+
* (browser-data.ts) and the Python bridge's JSON loader depend on this
|
|
7
|
+
* file, so a break here means both sides are broken in the same way.
|
|
8
|
+
*
|
|
9
|
+
* These tests are browser-free — no Chromium/Firefox needed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
BOT_SIGNALS,
|
|
15
|
+
ACCESSIBILITY,
|
|
16
|
+
NAV_SETTLE,
|
|
17
|
+
} from "../core/shared/browser-data.js";
|
|
18
|
+
|
|
19
|
+
// ─── Bot signals ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
describe("shared browser-data.json — bot signals", () => {
|
|
22
|
+
it("has all four signal categories as arrays of strings", () => {
|
|
23
|
+
expect(Array.isArray(BOT_SIGNALS.blockSignals)).toBe(true);
|
|
24
|
+
expect(BOT_SIGNALS.blockSignals.length).toBeGreaterThanOrEqual(10);
|
|
25
|
+
for (const s of BOT_SIGNALS.blockSignals) {
|
|
26
|
+
expect(typeof s).toBe("string");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(Array.isArray(BOT_SIGNALS.bodyOnlySignals)).toBe(true);
|
|
30
|
+
expect(BOT_SIGNALS.bodyOnlySignals.length).toBeGreaterThanOrEqual(1);
|
|
31
|
+
for (const s of BOT_SIGNALS.bodyOnlySignals) {
|
|
32
|
+
expect(typeof s).toBe("string");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expect(Array.isArray(BOT_SIGNALS.bodyOnlyPatterns)).toBe(true);
|
|
36
|
+
expect(BOT_SIGNALS.bodyOnlyPatterns.length).toBeGreaterThanOrEqual(1);
|
|
37
|
+
for (const s of BOT_SIGNALS.bodyOnlyPatterns) {
|
|
38
|
+
expect(typeof s).toBe("string");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
expect(Array.isArray(BOT_SIGNALS.htmlSignals)).toBe(true);
|
|
42
|
+
expect(BOT_SIGNALS.htmlSignals.length).toBeGreaterThanOrEqual(4);
|
|
43
|
+
for (const s of BOT_SIGNALS.htmlSignals) {
|
|
44
|
+
expect(typeof s).toBe("string");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("includes known block signals", () => {
|
|
49
|
+
expect(BOT_SIGNALS.blockSignals).toContain("please verify you are human");
|
|
50
|
+
expect(BOT_SIGNALS.blockSignals).toContain("cf-challenge");
|
|
51
|
+
expect(BOT_SIGNALS.blockSignals).toContain("cdn-cgi/challenge");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("includes known body-only signals", () => {
|
|
55
|
+
expect(BOT_SIGNALS.bodyOnlySignals).toContain("errors.edgesuite.net");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("includes known html signals", () => {
|
|
59
|
+
expect(BOT_SIGNALS.htmlSignals).toContain("recaptcha");
|
|
60
|
+
expect(BOT_SIGNALS.htmlSignals).toContain("hcaptcha");
|
|
61
|
+
expect(BOT_SIGNALS.htmlSignals).toContain("data-sitekey");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ─── Accessibility ───────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe("shared browser-data.json — accessibility", () => {
|
|
68
|
+
it("has interactiveRoles as a non-empty array of strings", () => {
|
|
69
|
+
expect(Array.isArray(ACCESSIBILITY.interactiveRoles)).toBe(true);
|
|
70
|
+
expect(ACCESSIBILITY.interactiveRoles.length).toBeGreaterThanOrEqual(40);
|
|
71
|
+
for (const r of ACCESSIBILITY.interactiveRoles) {
|
|
72
|
+
expect(typeof r).toBe("string");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("has informationalRoles as a non-empty array of strings", () => {
|
|
77
|
+
expect(Array.isArray(ACCESSIBILITY.informationalRoles)).toBe(true);
|
|
78
|
+
expect(ACCESSIBILITY.informationalRoles.length).toBeGreaterThanOrEqual(15);
|
|
79
|
+
for (const r of ACCESSIBILITY.informationalRoles) {
|
|
80
|
+
expect(typeof r).toBe("string");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("has roleIcons as a map with known entries", () => {
|
|
85
|
+
expect(typeof ACCESSIBILITY.roleIcons).toBe("object");
|
|
86
|
+
expect(ACCESSIBILITY.roleIcons).not.toBeNull();
|
|
87
|
+
expect(Object.keys(ACCESSIBILITY.roleIcons).length).toBeGreaterThanOrEqual(
|
|
88
|
+
60,
|
|
89
|
+
);
|
|
90
|
+
expect(ACCESSIBILITY.roleIcons["button"]).toBe("🔘 ");
|
|
91
|
+
expect(ACCESSIBILITY.roleIcons["link"]).toBe("🔗 ");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("includes common interactive roles like button and link", () => {
|
|
95
|
+
expect(ACCESSIBILITY.interactiveRoles).toContain("button");
|
|
96
|
+
expect(ACCESSIBILITY.interactiveRoles).toContain("link");
|
|
97
|
+
expect(ACCESSIBILITY.interactiveRoles).toContain("textbox");
|
|
98
|
+
expect(ACCESSIBILITY.interactiveRoles).toContain("checkbox");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("includes informational roles like paragraph and text", () => {
|
|
102
|
+
expect(ACCESSIBILITY.informationalRoles).toContain("paragraph");
|
|
103
|
+
expect(ACCESSIBILITY.informationalRoles).toContain("text");
|
|
104
|
+
expect(ACCESSIBILITY.informationalRoles).toContain("group");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── Nav-settle ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe("shared browser-data.json — nav-settle", () => {
|
|
111
|
+
it("has expected numeric timeout values", () => {
|
|
112
|
+
expect(NAV_SETTLE.navTimeoutMs).toBe(5000);
|
|
113
|
+
expect(NAV_SETTLE.settleTimeoutMs).toBe(400);
|
|
114
|
+
expect(NAV_SETTLE.settleRaceMs).toBe(150);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("has a non-empty domStabilizationJs string", () => {
|
|
118
|
+
expect(typeof NAV_SETTLE.domStabilizationJs).toBe("string");
|
|
119
|
+
expect(NAV_SETTLE.domStabilizationJs.length).toBeGreaterThan(50);
|
|
120
|
+
expect(NAV_SETTLE.domStabilizationJs).toContain(
|
|
121
|
+
"document.querySelectorAll",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
queryElementCache,
|
|
22
22
|
formatElementList,
|
|
23
23
|
formatRoleCountSummary,
|
|
24
|
+
type QueryStatus,
|
|
24
25
|
} from "../core/shared/dom-extractor.js";
|
|
25
26
|
|
|
26
27
|
// ─── Helpers ───────────────────────────────────────────────────────
|
|
@@ -171,15 +172,16 @@ describe("runExtractor()", () => {
|
|
|
171
172
|
result: MOCK_EXTRACTOR_RESPONSE,
|
|
172
173
|
};
|
|
173
174
|
|
|
174
|
-
const
|
|
175
|
-
expect(
|
|
176
|
-
|
|
177
|
-
expect(result
|
|
178
|
-
expect(result
|
|
179
|
-
expect(result!.
|
|
180
|
-
expect(result
|
|
181
|
-
expect(result
|
|
182
|
-
expect(result
|
|
175
|
+
const outcome = await runExtractor("test-task", mockPlugin);
|
|
176
|
+
expect(outcome.ok).toBe(true);
|
|
177
|
+
if (!outcome.ok) return;
|
|
178
|
+
expect(outcome.result.title).toBe("Test Page");
|
|
179
|
+
expect(outcome.result.headings).toHaveLength(1);
|
|
180
|
+
expect(outcome.result.headings[0]!.text).toBe("Heading One");
|
|
181
|
+
expect(outcome.result.paragraphs).toHaveLength(1);
|
|
182
|
+
expect(outcome.result.links).toHaveLength(1);
|
|
183
|
+
expect(outcome.result.images).toHaveLength(1);
|
|
184
|
+
expect(outcome.result.interactive).toHaveLength(1);
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
it("returns null on evaluate failure", async () => {
|
|
@@ -189,8 +191,10 @@ describe("runExtractor()", () => {
|
|
|
189
191
|
error: "evaluate failed",
|
|
190
192
|
};
|
|
191
193
|
|
|
192
|
-
const
|
|
193
|
-
expect(
|
|
194
|
+
const outcome = await runExtractor("test-task", mockPlugin);
|
|
195
|
+
expect(outcome.ok).toBe(false);
|
|
196
|
+
if (outcome.ok) return;
|
|
197
|
+
expect(outcome.error).toContain("evaluate failed");
|
|
194
198
|
});
|
|
195
199
|
|
|
196
200
|
it("returns null on invalid JSON from evaluate", async () => {
|
|
@@ -200,8 +204,10 @@ describe("runExtractor()", () => {
|
|
|
200
204
|
result: "not valid json {{{",
|
|
201
205
|
};
|
|
202
206
|
|
|
203
|
-
const
|
|
204
|
-
expect(
|
|
207
|
+
const outcome = await runExtractor("test-task", mockPlugin);
|
|
208
|
+
expect(outcome.ok).toBe(false);
|
|
209
|
+
if (outcome.ok) return;
|
|
210
|
+
expect(outcome.error).toContain("invalid JSON");
|
|
205
211
|
});
|
|
206
212
|
|
|
207
213
|
it("returns null when script returns error field", async () => {
|
|
@@ -211,8 +217,26 @@ describe("runExtractor()", () => {
|
|
|
211
217
|
result: JSON.stringify({ error: "Something went wrong" }),
|
|
212
218
|
};
|
|
213
219
|
|
|
214
|
-
const
|
|
215
|
-
expect(
|
|
220
|
+
const outcome = await runExtractor("test-task", mockPlugin);
|
|
221
|
+
expect(outcome.ok).toBe(false);
|
|
222
|
+
if (outcome.ok) return;
|
|
223
|
+
expect(outcome.error).toBe("Something went wrong");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("forwards readOnly: true to plugin.evaluate", async () => {
|
|
227
|
+
const mockPlugin = new MockPlugin("test");
|
|
228
|
+
mockPlugin.evalResult = {
|
|
229
|
+
success: true,
|
|
230
|
+
result: MOCK_EXTRACTOR_RESPONSE,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
await runExtractor("test-task", mockPlugin);
|
|
234
|
+
const evaluateCalls = mockPlugin.calls.get("evaluate");
|
|
235
|
+
expect(evaluateCalls).toBeDefined();
|
|
236
|
+
expect(evaluateCalls!.length).toBe(1);
|
|
237
|
+
// Third arg must be true (readOnly)
|
|
238
|
+
const args = evaluateCalls![0] as unknown[];
|
|
239
|
+
expect(args[2]).toBe(true);
|
|
216
240
|
});
|
|
217
241
|
});
|
|
218
242
|
|
|
@@ -491,6 +515,55 @@ describe("queryElementCache()", () => {
|
|
|
491
515
|
expect(results).toHaveLength(2); // Two Submit buttons
|
|
492
516
|
expect(results.every((n) => n.role === "button")).toBe(true);
|
|
493
517
|
});
|
|
518
|
+
|
|
519
|
+
describe("QueryStatus out-param", () => {
|
|
520
|
+
it("ref present + extra role filter that does not match", () => {
|
|
521
|
+
const status: QueryStatus = {};
|
|
522
|
+
const results = queryElementCache(
|
|
523
|
+
cache,
|
|
524
|
+
{ ref: "@e6", role: "button" },
|
|
525
|
+
status,
|
|
526
|
+
);
|
|
527
|
+
expect(results).toHaveLength(0);
|
|
528
|
+
expect(status.refFilteredOut).toBeDefined();
|
|
529
|
+
expect(status.refFilteredOut!.filter).toBe("role");
|
|
530
|
+
expect(status.refFilteredOut!.value).toBe("button");
|
|
531
|
+
expect(status.refFilteredOut!.node.role).toBe("dialog");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("ref present + extra name filter that does not match", () => {
|
|
535
|
+
const status: QueryStatus = {};
|
|
536
|
+
const results = queryElementCache(
|
|
537
|
+
cache,
|
|
538
|
+
{ ref: "@e6", name: "close" },
|
|
539
|
+
status,
|
|
540
|
+
);
|
|
541
|
+
expect(results).toHaveLength(0);
|
|
542
|
+
expect(status.refFilteredOut).toBeDefined();
|
|
543
|
+
expect(status.refFilteredOut!.filter).toBe("name");
|
|
544
|
+
expect(status.refFilteredOut!.value).toBe("close");
|
|
545
|
+
expect(status.refFilteredOut!.node.name).toBe("Sign Up");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("ref genuinely absent", () => {
|
|
549
|
+
const status: QueryStatus = {};
|
|
550
|
+
const results = queryElementCache(cache, { ref: "@e99" }, status);
|
|
551
|
+
expect(results).toHaveLength(0);
|
|
552
|
+
expect(status.refFilteredOut).toBeUndefined();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("ref present + matching extra filter", () => {
|
|
556
|
+
const status: QueryStatus = {};
|
|
557
|
+
const results = queryElementCache(
|
|
558
|
+
cache,
|
|
559
|
+
{ ref: "@e6", role: "dialog" },
|
|
560
|
+
status,
|
|
561
|
+
);
|
|
562
|
+
expect(results).toHaveLength(1);
|
|
563
|
+
expect(results[0]!.ref).toBe("e6");
|
|
564
|
+
expect(status.refFilteredOut).toBeUndefined();
|
|
565
|
+
});
|
|
566
|
+
});
|
|
494
567
|
});
|
|
495
568
|
|
|
496
569
|
// ─── Router dispatch integration tests ─────────────────────────────
|
|
@@ -577,6 +650,41 @@ describe("router.browserInspect()", () => {
|
|
|
577
650
|
expect(result.content).toContain("Lexical Analysis");
|
|
578
651
|
});
|
|
579
652
|
|
|
653
|
+
it("ref + role mismatch shows found-but-filtered message", async () => {
|
|
654
|
+
await router.navigate("https://example.com", { taskId: "test-5b" });
|
|
655
|
+
|
|
656
|
+
const result = await router.browserInspect("test-5b", {
|
|
657
|
+
ref: "@e6",
|
|
658
|
+
role: "button",
|
|
659
|
+
});
|
|
660
|
+
expect(result.success).toBe(true);
|
|
661
|
+
expect(result.content).toContain("found in cache");
|
|
662
|
+
expect(result.content).toContain("does not match");
|
|
663
|
+
expect(result.content).toContain('filter role="button"');
|
|
664
|
+
expect(result.content).not.toContain("not found in cache");
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("ref genuinely absent keeps not-found message", async () => {
|
|
668
|
+
await router.navigate("https://example.com", { taskId: "test-5c" });
|
|
669
|
+
|
|
670
|
+
const result = await router.browserInspect("test-5c", { ref: "@e99" });
|
|
671
|
+
expect(result.success).toBe(true);
|
|
672
|
+
expect(result.content).toContain("not found in cache");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("ref + role match returns element normally", async () => {
|
|
676
|
+
await router.navigate("https://example.com", { taskId: "test-5d" });
|
|
677
|
+
|
|
678
|
+
const result = await router.browserInspect("test-5d", {
|
|
679
|
+
ref: "@e6",
|
|
680
|
+
role: "dialog",
|
|
681
|
+
});
|
|
682
|
+
expect(result.success).toBe(true);
|
|
683
|
+
expect(result.content).toContain("@e6");
|
|
684
|
+
expect(result.content).toContain("Sign Up");
|
|
685
|
+
expect(result.content).not.toContain("found in cache");
|
|
686
|
+
});
|
|
687
|
+
|
|
580
688
|
it("empty element cache returns 'no elements cached' message", async () => {
|
|
581
689
|
const emptyMock = new MockPlugin("empty");
|
|
582
690
|
emptyMock.elementCache = new Map();
|
|
@@ -828,7 +936,75 @@ describe("router.browserInspect()", () => {
|
|
|
828
936
|
|
|
829
937
|
const result = await router.browserInspect("test-10", { text: true });
|
|
830
938
|
expect(result.success).toBe(false);
|
|
831
|
-
expect(result.error).toContain(
|
|
939
|
+
expect(result.error).toContain(
|
|
940
|
+
"Text extraction failed: browser disconnected",
|
|
941
|
+
);
|
|
942
|
+
expect(result.error).toContain("browser-inspect with role=");
|
|
943
|
+
expect(result.error).not.toContain("browser-snapshot to inspect visually");
|
|
944
|
+
expect(result.error).toMatch(/\(8 elements\) is available/);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
describe("Changes 3 & 4 — silent-empty guards", () => {
|
|
948
|
+
it("correlateElements valid-but-empty result appends notice", () => {
|
|
949
|
+
const cache = new Map<string, AriaCachedNode>();
|
|
950
|
+
const result = correlateElements(
|
|
951
|
+
{
|
|
952
|
+
title: "",
|
|
953
|
+
headings: [],
|
|
954
|
+
paragraphs: [],
|
|
955
|
+
links: [],
|
|
956
|
+
images: [],
|
|
957
|
+
interactive: [],
|
|
958
|
+
},
|
|
959
|
+
cache,
|
|
960
|
+
true,
|
|
961
|
+
);
|
|
962
|
+
expect(result.text).toContain("No extractable content");
|
|
963
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
964
|
+
expect(result.matchedRefs).toBe(0);
|
|
965
|
+
expect(result.staleCache).toBe(false);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("text=true + empty extractor + populated cache returns notice", async () => {
|
|
969
|
+
mock.evalResult = {
|
|
970
|
+
success: true,
|
|
971
|
+
result: JSON.stringify({
|
|
972
|
+
title: "",
|
|
973
|
+
headings: [],
|
|
974
|
+
paragraphs: [],
|
|
975
|
+
links: [],
|
|
976
|
+
images: [],
|
|
977
|
+
interactive: [],
|
|
978
|
+
}),
|
|
979
|
+
};
|
|
980
|
+
await router.navigate("https://example.com", { taskId: "test-11" });
|
|
981
|
+
|
|
982
|
+
const result = await router.browserInspect("test-11", { text: true });
|
|
983
|
+
expect(result.success).toBe(true);
|
|
984
|
+
expect(result.content).toContain("No extractable content");
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it("text=true + query + empty extractor input appends no-match notice", async () => {
|
|
988
|
+
mock.evalResult = {
|
|
989
|
+
success: true,
|
|
990
|
+
result: JSON.stringify({
|
|
991
|
+
title: "",
|
|
992
|
+
headings: [],
|
|
993
|
+
paragraphs: [],
|
|
994
|
+
links: [],
|
|
995
|
+
images: [],
|
|
996
|
+
interactive: [],
|
|
997
|
+
}),
|
|
998
|
+
};
|
|
999
|
+
await router.navigate("https://example.com", { taskId: "test-12" });
|
|
1000
|
+
|
|
1001
|
+
const result = await router.browserInspect("test-12", {
|
|
1002
|
+
text: true,
|
|
1003
|
+
query: "anything",
|
|
1004
|
+
});
|
|
1005
|
+
expect(result.success).toBe(true);
|
|
1006
|
+
expect(result.content).toContain('No content matched "anything"');
|
|
1007
|
+
});
|
|
832
1008
|
});
|
|
833
1009
|
});
|
|
834
1010
|
|
|
@@ -24,8 +24,8 @@ vi.mock("node:fs", () => ({
|
|
|
24
24
|
unlinkSync: vi.fn(),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
-
import browserToggle from "../browser-toggle";
|
|
28
|
-
import { listProfiles, formatProfileList } from "../browser-profile";
|
|
27
|
+
import browserToggle from "../browser-toggle.js";
|
|
28
|
+
import { listProfiles, formatProfileList } from "../browser-profile.js";
|
|
29
29
|
|
|
30
30
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|
31
31
|
|
|
@@ -113,7 +113,7 @@ describe("listProfiles()", () => {
|
|
|
113
113
|
(statSync as Mock).mockReturnValue({ size: 100 });
|
|
114
114
|
|
|
115
115
|
const profiles = listProfiles();
|
|
116
|
-
expect(profiles.map((p) => p.name)).toEqual([
|
|
116
|
+
expect(profiles.map((p: { name: string }) => p.name)).toEqual([
|
|
117
117
|
"default",
|
|
118
118
|
"shopping",
|
|
119
119
|
"work",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* /web on | off | status commands for toggling browser automation tools.
|
|
4
4
|
*
|
|
5
5
|
* Tests cover:
|
|
6
|
-
* - Filtering logic (
|
|
6
|
+
* - Filtering logic (getRegisteredIn)
|
|
7
7
|
* - State queries (isBrowserEnabled)
|
|
8
8
|
* - State mutations (applyBrowserState, persistState)
|
|
9
9
|
* - Branch-aware restoration (restoreFromBranch)
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import browserToggle, { getToggleState } from "../browser-toggle.js";
|
|
21
21
|
import {
|
|
22
|
-
|
|
22
|
+
getRegisteredIn,
|
|
23
23
|
isBrowserEnabled,
|
|
24
24
|
applyBrowserState,
|
|
25
25
|
persistState,
|
|
@@ -27,12 +27,10 @@ import {
|
|
|
27
27
|
_resetToggleStateForTest,
|
|
28
28
|
readBrowserToggleConfig,
|
|
29
29
|
applyConfigDefault,
|
|
30
|
-
getRegisteredLearnTools,
|
|
31
|
-
getRegisteredSiblingTools,
|
|
32
30
|
isLearnEnabled,
|
|
33
31
|
applyLearnState,
|
|
34
32
|
type BrowserToggleState,
|
|
35
|
-
} from "
|
|
33
|
+
} from "../browser-toggle.js";
|
|
36
34
|
|
|
37
35
|
// Mock node:fs so readBrowserToggleConfig and applyConfigDefault can be
|
|
38
36
|
// tested without touching the real filesystem.
|
|
@@ -119,38 +117,52 @@ function mockContext(
|
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
// ==================================================================
|
|
122
|
-
//
|
|
120
|
+
// getRegisteredIn
|
|
123
121
|
// ==================================================================
|
|
124
|
-
describe("
|
|
122
|
+
describe("getRegisteredIn", () => {
|
|
123
|
+
const BROWSER_NAMES = new Set([
|
|
124
|
+
"web-fetch",
|
|
125
|
+
"browser-navigate",
|
|
126
|
+
"browser-snapshot",
|
|
127
|
+
"browser-click",
|
|
128
|
+
"browser-type",
|
|
129
|
+
"browser-scroll",
|
|
130
|
+
"browser-back",
|
|
131
|
+
"browser-press",
|
|
132
|
+
"browser-console",
|
|
133
|
+
"browser-inspect",
|
|
134
|
+
"web-guide",
|
|
135
|
+
]);
|
|
136
|
+
|
|
125
137
|
it("returns empty array when no tools are registered", () => {
|
|
126
138
|
const pi = mockPi({ tools: [] });
|
|
127
|
-
expect(
|
|
139
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual([]);
|
|
128
140
|
});
|
|
129
141
|
|
|
130
|
-
it("returns empty array when only non-
|
|
142
|
+
it("returns empty array when only non-matching tools are registered", () => {
|
|
131
143
|
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
132
|
-
expect(
|
|
144
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual([]);
|
|
133
145
|
});
|
|
134
146
|
|
|
135
|
-
it("returns only
|
|
147
|
+
it("returns only matching tools when mixed with non-matching tools", () => {
|
|
136
148
|
const pi = mockPi({
|
|
137
149
|
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
138
150
|
});
|
|
139
|
-
expect(
|
|
151
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(
|
|
140
152
|
ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
141
153
|
);
|
|
142
154
|
});
|
|
143
155
|
|
|
144
|
-
it("returns all
|
|
156
|
+
it("returns all matching tools when all are present", () => {
|
|
145
157
|
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
146
|
-
expect(
|
|
158
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(
|
|
147
159
|
ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
148
160
|
);
|
|
149
161
|
});
|
|
150
162
|
|
|
151
|
-
it("returns a subset when only some
|
|
163
|
+
it("returns a subset when only some matching tools are registered", () => {
|
|
152
164
|
const pi = mockPi({ tools: SOME_BROWSER_TOOLS });
|
|
153
|
-
expect(
|
|
165
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(
|
|
154
166
|
SOME_BROWSER_TOOLS.map((t) => t.name),
|
|
155
167
|
);
|
|
156
168
|
});
|
|
@@ -164,7 +176,7 @@ describe("getRegisteredBrowserTools", () => {
|
|
|
164
176
|
],
|
|
165
177
|
});
|
|
166
178
|
// Only the exact-lowercase match should be returned
|
|
167
|
-
expect(
|
|
179
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(["web-fetch"]);
|
|
168
180
|
});
|
|
169
181
|
|
|
170
182
|
it("ignores tool metadata beyond the name field", () => {
|
|
@@ -173,7 +185,7 @@ describe("getRegisteredBrowserTools", () => {
|
|
|
173
185
|
{ name: "browser-scroll", description: "b", promptGuidelines: [] },
|
|
174
186
|
];
|
|
175
187
|
const pi = mockPi({ tools: tools as any });
|
|
176
|
-
expect(
|
|
188
|
+
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual([
|
|
177
189
|
"web-fetch",
|
|
178
190
|
"browser-scroll",
|
|
179
191
|
]);
|
|
@@ -208,12 +220,12 @@ describe("isBrowserEnabled", () => {
|
|
|
208
220
|
expect(isBrowserEnabled(pi)).toBe(true);
|
|
209
221
|
});
|
|
210
222
|
|
|
211
|
-
it("returns
|
|
223
|
+
it("returns false when no browser tools exist", () => {
|
|
212
224
|
const pi = mockPi({
|
|
213
225
|
tools: NON_BROWSER_TOOLS,
|
|
214
226
|
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
215
227
|
});
|
|
216
|
-
expect(isBrowserEnabled(pi)).toBe(
|
|
228
|
+
expect(isBrowserEnabled(pi)).toBe(false);
|
|
217
229
|
});
|
|
218
230
|
|
|
219
231
|
it("returns false when active set is empty and browser tools exist", () => {
|
|
@@ -825,12 +837,14 @@ describe("command handler dispatch (via factory closure)", () => {
|
|
|
825
837
|
});
|
|
826
838
|
|
|
827
839
|
// ==================================================================
|
|
828
|
-
//
|
|
840
|
+
// getRegisteredIn (learn) / isLearnEnabled / applyLearnState
|
|
829
841
|
// ==================================================================
|
|
830
|
-
describe("
|
|
842
|
+
describe("getRegisteredIn with learn tool names", () => {
|
|
843
|
+
const LEARN_NAMES = new Set(["web-learn"]);
|
|
844
|
+
|
|
831
845
|
it("returns empty array when no learn tools registered", () => {
|
|
832
846
|
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
833
|
-
expect(
|
|
847
|
+
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual([]);
|
|
834
848
|
});
|
|
835
849
|
|
|
836
850
|
it("returns web-learn when it's in the tool list", () => {
|
|
@@ -839,7 +853,7 @@ describe("getRegisteredLearnTools", () => {
|
|
|
839
853
|
{ name: "web-learn", description: "learn" },
|
|
840
854
|
];
|
|
841
855
|
const pi = mockPi({ tools });
|
|
842
|
-
expect(
|
|
856
|
+
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual(["web-learn"]);
|
|
843
857
|
});
|
|
844
858
|
|
|
845
859
|
it("is case-sensitive (tool names are always lowercase)", () => {
|
|
@@ -849,7 +863,7 @@ describe("getRegisteredLearnTools", () => {
|
|
|
849
863
|
{ name: "web-learn" },
|
|
850
864
|
];
|
|
851
865
|
const pi = mockPi({ tools });
|
|
852
|
-
expect(
|
|
866
|
+
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual(["web-learn"]);
|
|
853
867
|
});
|
|
854
868
|
});
|
|
855
869
|
|
|
@@ -878,9 +892,9 @@ describe("isLearnEnabled", () => {
|
|
|
878
892
|
expect(isLearnEnabled(pi)).toBe(false);
|
|
879
893
|
});
|
|
880
894
|
|
|
881
|
-
it("returns
|
|
895
|
+
it("returns false when no learn tools exist", () => {
|
|
882
896
|
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
883
|
-
expect(isLearnEnabled(pi)).toBe(
|
|
897
|
+
expect(isLearnEnabled(pi)).toBe(false);
|
|
884
898
|
});
|
|
885
899
|
});
|
|
886
900
|
|
|
@@ -924,28 +938,28 @@ describe("applyLearnState", () => {
|
|
|
924
938
|
});
|
|
925
939
|
|
|
926
940
|
// ==================================================================
|
|
927
|
-
//
|
|
941
|
+
// getRegisteredIn (sibling)
|
|
928
942
|
// ==================================================================
|
|
929
|
-
describe("
|
|
930
|
-
|
|
943
|
+
describe("getRegisteredIn with sibling tool names", () => {
|
|
944
|
+
const SIBLING_NAMES = new Set(["web-search"]);
|
|
945
|
+
|
|
946
|
+
it("returns web-search when it's in the tool list", () => {
|
|
931
947
|
const tools = [
|
|
932
948
|
...ALL_BROWSER_TOOLS,
|
|
933
949
|
{ name: "web-search", description: "SearXNG search" },
|
|
934
950
|
];
|
|
935
951
|
const pi = mockPi({ tools });
|
|
936
|
-
expect(
|
|
952
|
+
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual(["web-search"]);
|
|
937
953
|
});
|
|
938
954
|
|
|
939
955
|
it("uses exact-name Set.has() membership — no regex false positives", () => {
|
|
940
|
-
// A tool named "web-search-news" should NOT match the "web-search" entry,
|
|
941
|
-
// confirming exact-name membership rather than a prefix regex.
|
|
942
956
|
const tools = [
|
|
943
957
|
...ALL_BROWSER_TOOLS,
|
|
944
958
|
{ name: "web-search-news", description: "news" },
|
|
945
959
|
{ name: "web-search-images", description: "images" },
|
|
946
960
|
];
|
|
947
961
|
const pi = mockPi({ tools });
|
|
948
|
-
expect(
|
|
962
|
+
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual([]);
|
|
949
963
|
});
|
|
950
964
|
|
|
951
965
|
it("is case-sensitive — only exact lower-case matches", () => {
|
|
@@ -955,7 +969,7 @@ describe("getRegisteredSiblingTools", () => {
|
|
|
955
969
|
{ name: "web-search" },
|
|
956
970
|
];
|
|
957
971
|
const pi = mockPi({ tools });
|
|
958
|
-
expect(
|
|
972
|
+
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual(["web-search"]);
|
|
959
973
|
});
|
|
960
974
|
});
|
|
961
975
|
|