@melra/browser-runtime 0.3.0-alpha.3
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/LICENSE +202 -0
- package/dist/browser-connection.d.ts +17 -0
- package/dist/browser-connection.d.ts.map +1 -0
- package/dist/browser-connection.js +95 -0
- package/dist/browser-connection.js.map +1 -0
- package/dist/browser-connection.test.d.ts +2 -0
- package/dist/browser-connection.test.d.ts.map +1 -0
- package/dist/browser-connection.test.js +123 -0
- package/dist/browser-connection.test.js.map +1 -0
- package/dist/dialogs.test.d.ts +2 -0
- package/dist/dialogs.test.d.ts.map +1 -0
- package/dist/dialogs.test.js +165 -0
- package/dist/dialogs.test.js.map +1 -0
- package/dist/frames.test.d.ts +2 -0
- package/dist/frames.test.d.ts.map +1 -0
- package/dist/frames.test.js +157 -0
- package/dist/frames.test.js.map +1 -0
- package/dist/history-tabs.test.d.ts +2 -0
- package/dist/history-tabs.test.d.ts.map +1 -0
- package/dist/history-tabs.test.js +171 -0
- package/dist/history-tabs.test.js.map +1 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +822 -0
- package/dist/index.js.map +1 -0
- package/dist/input-fidelity.test.d.ts +2 -0
- package/dist/input-fidelity.test.d.ts.map +1 -0
- package/dist/input-fidelity.test.js +260 -0
- package/dist/input-fidelity.test.js.map +1 -0
- package/dist/network-policy.d.ts +7 -0
- package/dist/network-policy.d.ts.map +1 -0
- package/dist/network-policy.js +112 -0
- package/dist/network-policy.js.map +1 -0
- package/dist/network-policy.test.d.ts +2 -0
- package/dist/network-policy.test.d.ts.map +1 -0
- package/dist/network-policy.test.js +21 -0
- package/dist/network-policy.test.js.map +1 -0
- package/dist/see-act.test.d.ts +2 -0
- package/dist/see-act.test.d.ts.map +1 -0
- package/dist/see-act.test.js +152 -0
- package/dist/see-act.test.js.map +1 -0
- package/dist/selector.d.ts +30 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/selector.js +39 -0
- package/dist/selector.js.map +1 -0
- package/dist/selector.test.d.ts +2 -0
- package/dist/selector.test.d.ts.map +1 -0
- package/dist/selector.test.js +34 -0
- package/dist/selector.test.js.map +1 -0
- package/dist/stable-dom.d.ts +21 -0
- package/dist/stable-dom.d.ts.map +1 -0
- package/dist/stable-dom.js +76 -0
- package/dist/stable-dom.js.map +1 -0
- package/dist/stable-dom.test.d.ts +2 -0
- package/dist/stable-dom.test.d.ts.map +1 -0
- package/dist/stable-dom.test.js +22 -0
- package/dist/stable-dom.test.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { BrowserOperationSchema } from "@melra/protocol";
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { BrowserRuntime, captchaReport, detectBrowserExecutable } from "./index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Consent banners, cookie walls, and captcha widgets live in iframes, and every
|
|
12
|
+
* locator used to be built from the page — which only searches the main
|
|
13
|
+
* document. An agent could see the banner in a screenshot and had no way to
|
|
14
|
+
* click it. These tests drive real frames, so they fail if that regresses.
|
|
15
|
+
*/
|
|
16
|
+
const BANNER = `<!doctype html><html><body>
|
|
17
|
+
<button id="accept">Accept all cookies</button>
|
|
18
|
+
</body></html>`;
|
|
19
|
+
/** Attaches itself after a delay, so a wait started before it exists must find it. */
|
|
20
|
+
const LATE = `<!doctype html><html><body><p>loading</p><script>
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
document.body.innerHTML = '<button id="late">Continue</button>';
|
|
23
|
+
}, 400);
|
|
24
|
+
</script></body></html>`;
|
|
25
|
+
const PAGE = `<!doctype html><html><head><title>Framed</title></head><body>
|
|
26
|
+
<h1>Article</h1>
|
|
27
|
+
<iframe src="/banner" title="consent"></iframe>
|
|
28
|
+
<iframe src="/late" title="late"></iframe>
|
|
29
|
+
</body></html>`;
|
|
30
|
+
const roots = [];
|
|
31
|
+
const servers = [];
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await Promise.all(servers.splice(0).map((server) => new Promise((done) => {
|
|
34
|
+
server.close(() => done());
|
|
35
|
+
})));
|
|
36
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
37
|
+
});
|
|
38
|
+
async function fixture() {
|
|
39
|
+
const executablePath = await detectBrowserExecutable();
|
|
40
|
+
if (executablePath === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const server = createServer((request, response) => {
|
|
43
|
+
const body = request.url === "/banner" ? BANNER : request.url === "/late" ? LATE : PAGE;
|
|
44
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
45
|
+
response.end(body);
|
|
46
|
+
});
|
|
47
|
+
servers.push(server);
|
|
48
|
+
await new Promise((done) => {
|
|
49
|
+
server.listen(0, "127.0.0.1", done);
|
|
50
|
+
});
|
|
51
|
+
const address = server.address();
|
|
52
|
+
if (address === null || typeof address === "string") {
|
|
53
|
+
throw new Error("fixture_server_address_unavailable");
|
|
54
|
+
}
|
|
55
|
+
const root = await mkdtemp(join(tmpdir(), "melra-frames-"));
|
|
56
|
+
roots.push(root);
|
|
57
|
+
return {
|
|
58
|
+
runtime: new BrowserRuntime({
|
|
59
|
+
workspaceRoot: root,
|
|
60
|
+
artifactDirectory: join(root, "artifacts"),
|
|
61
|
+
executablePath,
|
|
62
|
+
allowedDomains: ["127.0.0.1"],
|
|
63
|
+
allowLocalhost: true,
|
|
64
|
+
}),
|
|
65
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
describe("frame-aware targeting", () => {
|
|
69
|
+
it("lists and clicks an element inside an iframe", async () => {
|
|
70
|
+
const context = await fixture();
|
|
71
|
+
if (context === undefined)
|
|
72
|
+
return;
|
|
73
|
+
const { runtime, url } = context;
|
|
74
|
+
try {
|
|
75
|
+
const observed = await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
76
|
+
const elements = observed.elements;
|
|
77
|
+
const accept = elements.find((item) => item.name === "Accept all cookies");
|
|
78
|
+
// The banner is in a child document, so it is absent from the page text
|
|
79
|
+
// but must still be listed, addressable, and attributed to its frame.
|
|
80
|
+
expect(String(observed.text)).not.toContain("Accept all cookies");
|
|
81
|
+
expect(accept).toBeDefined();
|
|
82
|
+
expect(accept?.frame).toContain("/banner");
|
|
83
|
+
expect(elements.every((item) => typeof item.index === "number")).toBe(true);
|
|
84
|
+
expect(new Set(elements.map((item) => item.index)).size).toBe(elements.length);
|
|
85
|
+
// The whole point: a target resolved without naming the frame.
|
|
86
|
+
const clicked = await runtime.execute(BrowserOperationSchema.parse({
|
|
87
|
+
kind: "browser",
|
|
88
|
+
action: "click",
|
|
89
|
+
target: { text: "Accept all cookies" },
|
|
90
|
+
}));
|
|
91
|
+
expect(clicked.success).toBe(true);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await runtime.close();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it("waits for a target that appears in a frame after the wait starts", async () => {
|
|
98
|
+
const context = await fixture();
|
|
99
|
+
if (context === undefined)
|
|
100
|
+
return;
|
|
101
|
+
const { runtime, url } = context;
|
|
102
|
+
try {
|
|
103
|
+
await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
104
|
+
const waited = await runtime.execute(BrowserOperationSchema.parse({
|
|
105
|
+
kind: "browser",
|
|
106
|
+
action: "wait",
|
|
107
|
+
target: { text: "Continue" },
|
|
108
|
+
state: "visible",
|
|
109
|
+
timeoutMs: 5_000,
|
|
110
|
+
}));
|
|
111
|
+
expect(waited.waited).toBe(true);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await runtime.close();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it("does not report a target as hidden merely because other frames lack it", async () => {
|
|
118
|
+
const context = await fixture();
|
|
119
|
+
if (context === undefined)
|
|
120
|
+
return;
|
|
121
|
+
const { runtime, url } = context;
|
|
122
|
+
try {
|
|
123
|
+
await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
124
|
+
// The banner is visible in exactly one of three frames. A per-candidate
|
|
125
|
+
// "some frame is missing it" test would call that hidden immediately.
|
|
126
|
+
await expect(runtime.execute(BrowserOperationSchema.parse({
|
|
127
|
+
kind: "browser",
|
|
128
|
+
action: "wait",
|
|
129
|
+
target: { text: "Accept all cookies" },
|
|
130
|
+
state: "hidden",
|
|
131
|
+
timeoutMs: 700,
|
|
132
|
+
}))).rejects.toThrow(/browser_wait_timeout:hidden/);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await runtime.close();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("captchaReport", () => {
|
|
140
|
+
it("names the vendor of a human-verification widget", () => {
|
|
141
|
+
const report = captchaReport([
|
|
142
|
+
"http://127.0.0.1:8080/",
|
|
143
|
+
"https://www.google.com/recaptcha/api2/anchor?k=abc",
|
|
144
|
+
"https://challenges.cloudflare.com/cdn-cgi/challenge-platform/turnstile",
|
|
145
|
+
]);
|
|
146
|
+
const captcha = report.captcha;
|
|
147
|
+
expect(captcha.present).toBe(true);
|
|
148
|
+
expect(captcha.vendors).toEqual(["recaptcha", "turnstile"]);
|
|
149
|
+
});
|
|
150
|
+
it("stays silent on a page with no challenge", () => {
|
|
151
|
+
expect(captchaReport(["https://example.com/", "https://cdn.example.com/ad"])).toEqual({});
|
|
152
|
+
});
|
|
153
|
+
it("does not match a lookalike host outside the vendor domain", () => {
|
|
154
|
+
expect(captchaReport(["https://hcaptcha.com.evil.test/x"])).toEqual({});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
//# sourceMappingURL=frames.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.test.js","sourceRoot":"","sources":["../src/frames.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-tabs.test.d.ts","sourceRoot":"","sources":["../src/history-tabs.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { BrowserOperationSchema } from "@melra/protocol";
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { BrowserRuntime, detectBrowserExecutable } from "./index.js";
|
|
10
|
+
/**
|
|
11
|
+
* A session could only ever move forward, in one tab. There was no `back`, so a
|
|
12
|
+
* wrong click was unrecoverable without re-navigating from scratch, and `tabs`
|
|
13
|
+
* reported which page was `active` while nothing could change it — the
|
|
14
|
+
* `tabIndex` schema field existed but was read only by `close`.
|
|
15
|
+
*/
|
|
16
|
+
const roots = [];
|
|
17
|
+
const servers = [];
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await Promise.all(servers.splice(0).map((server) => new Promise((done) => {
|
|
20
|
+
server.close(() => done());
|
|
21
|
+
})));
|
|
22
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
23
|
+
});
|
|
24
|
+
/** Serves /one, /two, /three, each naming itself in the title and body. */
|
|
25
|
+
async function fixture() {
|
|
26
|
+
const executablePath = await detectBrowserExecutable();
|
|
27
|
+
if (executablePath === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const server = createServer((request, response) => {
|
|
30
|
+
const name = (request.url ?? "/").slice(1) || "one";
|
|
31
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
32
|
+
response.end(`<!doctype html><html><head><title>${name}</title></head><body><h1>${name}</h1></body></html>`);
|
|
33
|
+
});
|
|
34
|
+
servers.push(server);
|
|
35
|
+
await new Promise((done) => {
|
|
36
|
+
server.listen(0, "127.0.0.1", done);
|
|
37
|
+
});
|
|
38
|
+
const address = server.address();
|
|
39
|
+
if (address === null || typeof address === "string") {
|
|
40
|
+
throw new Error("fixture_server_address_unavailable");
|
|
41
|
+
}
|
|
42
|
+
const root = await mkdtemp(join(tmpdir(), "melra-history-tabs-"));
|
|
43
|
+
roots.push(root);
|
|
44
|
+
return {
|
|
45
|
+
runtime: new BrowserRuntime({
|
|
46
|
+
workspaceRoot: root,
|
|
47
|
+
artifactDirectory: join(root, "artifacts"),
|
|
48
|
+
executablePath,
|
|
49
|
+
allowedDomains: ["127.0.0.1"],
|
|
50
|
+
allowLocalhost: true,
|
|
51
|
+
}),
|
|
52
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const op = (fields) => BrowserOperationSchema.parse({ kind: "browser", ...fields });
|
|
56
|
+
describe("browser history", () => {
|
|
57
|
+
it("moves back and forward through the history stack", async () => {
|
|
58
|
+
const context = await fixture();
|
|
59
|
+
if (context === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const { runtime, url } = context;
|
|
62
|
+
try {
|
|
63
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
|
|
64
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/two` }));
|
|
65
|
+
const back = await runtime.execute(op({ action: "back" }));
|
|
66
|
+
expect(back.moved).toBe(true);
|
|
67
|
+
expect(back.title).toBe("one");
|
|
68
|
+
const forward = await runtime.execute(op({ action: "forward" }));
|
|
69
|
+
expect(forward.moved).toBe(true);
|
|
70
|
+
expect(forward.title).toBe("two");
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await runtime.close();
|
|
74
|
+
}
|
|
75
|
+
}, 120_000);
|
|
76
|
+
it("reports a move with no HTTP response, and the end of the stack, differently", async () => {
|
|
77
|
+
const context = await fixture();
|
|
78
|
+
if (context === undefined)
|
|
79
|
+
return;
|
|
80
|
+
const { runtime, url } = context;
|
|
81
|
+
try {
|
|
82
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
|
|
83
|
+
// A page starts on `about:blank`, which is a real history entry but
|
|
84
|
+
// produces no HTTP response, so Playwright reports the same `null` it
|
|
85
|
+
// reports for "nowhere to go". This moved.
|
|
86
|
+
const blank = await runtime.execute(op({ action: "back" }));
|
|
87
|
+
expect(blank.moved).toBe(true);
|
|
88
|
+
expect(blank.status).toBe(null);
|
|
89
|
+
expect(blank.url).toBe("about:blank");
|
|
90
|
+
// Now there genuinely is nothing before it. "Go back unless already at
|
|
91
|
+
// the start" is a normal thing to want, so this reports rather than
|
|
92
|
+
// throws, and it must not look like the case above.
|
|
93
|
+
const end = await runtime.execute(op({ action: "back" }));
|
|
94
|
+
expect(end.moved).toBe(false);
|
|
95
|
+
expect(end.url).toBe("about:blank");
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await runtime.close();
|
|
99
|
+
}
|
|
100
|
+
}, 120_000);
|
|
101
|
+
it("reloads the current page in place", async () => {
|
|
102
|
+
const context = await fixture();
|
|
103
|
+
if (context === undefined)
|
|
104
|
+
return;
|
|
105
|
+
const { runtime, url } = context;
|
|
106
|
+
try {
|
|
107
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/three` }));
|
|
108
|
+
const reloaded = await runtime.execute(op({ action: "reload" }));
|
|
109
|
+
expect(reloaded.status).toBe(200);
|
|
110
|
+
expect(reloaded.url).toBe(`${url}/three`);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await runtime.close();
|
|
114
|
+
}
|
|
115
|
+
}, 120_000);
|
|
116
|
+
});
|
|
117
|
+
describe("browser tabs", () => {
|
|
118
|
+
it("opens a tab, switches between tabs, and closes one", async () => {
|
|
119
|
+
const context = await fixture();
|
|
120
|
+
if (context === undefined)
|
|
121
|
+
return;
|
|
122
|
+
const { runtime, url } = context;
|
|
123
|
+
try {
|
|
124
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
|
|
125
|
+
const opened = await runtime.execute(op({ action: "tab_new", url: `${url}/two` }));
|
|
126
|
+
expect(opened.opened).toBe(true);
|
|
127
|
+
expect(opened.index).toBe(1);
|
|
128
|
+
// A new tab becomes the active one, so the next action lands on it.
|
|
129
|
+
expect((await runtime.execute(op({ action: "inspect" }))).title).toBe("two");
|
|
130
|
+
const switched = await runtime.execute(op({ action: "tab_switch", tabIndex: 0 }));
|
|
131
|
+
expect(switched.switched).toBe(true);
|
|
132
|
+
expect(switched.tabs.find((tab) => tab.active)?.index).toBe(0);
|
|
133
|
+
expect((await runtime.execute(op({ action: "inspect" }))).title).toBe("one");
|
|
134
|
+
const closed = await runtime.execute(op({ action: "close", tabIndex: 1 }));
|
|
135
|
+
expect(closed.tabs.length).toBe(1);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await runtime.close();
|
|
139
|
+
}
|
|
140
|
+
}, 120_000);
|
|
141
|
+
it("refuses a tab index that does not exist", async () => {
|
|
142
|
+
const context = await fixture();
|
|
143
|
+
if (context === undefined)
|
|
144
|
+
return;
|
|
145
|
+
const { runtime, url } = context;
|
|
146
|
+
try {
|
|
147
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
|
|
148
|
+
await expect(runtime.execute(op({ action: "tab_switch", tabIndex: 7 }))).rejects.toThrow("browser_tab_not_found");
|
|
149
|
+
await expect(runtime.execute(op({ action: "tab_switch" }))).rejects.toThrow("browser_tab_switch_requires_tab_index");
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await runtime.close();
|
|
153
|
+
}
|
|
154
|
+
}, 120_000);
|
|
155
|
+
it("blocks a disallowed domain when opening a tab", async () => {
|
|
156
|
+
const context = await fixture();
|
|
157
|
+
if (context === undefined)
|
|
158
|
+
return;
|
|
159
|
+
const { runtime, url } = context;
|
|
160
|
+
try {
|
|
161
|
+
await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
|
|
162
|
+
// `tab_new` takes a url, so it has to run the same guard `navigate` does
|
|
163
|
+
// rather than becoming a way around the allowlist.
|
|
164
|
+
await expect(runtime.execute(op({ action: "tab_new", url: "http://169.254.169.254/latest" }))).rejects.toThrow();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await runtime.close();
|
|
168
|
+
}
|
|
169
|
+
}, 120_000);
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=history-tabs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-tabs.test.js","sourceRoot":"","sources":["../src/history-tabs.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { BrowserOperation } from "@melra/protocol";
|
|
2
|
+
import { type NetworkPolicy } from "./network-policy.js";
|
|
3
|
+
export interface BrowserRuntimeOptions extends NetworkPolicy {
|
|
4
|
+
artifactDirectory: string;
|
|
5
|
+
workspaceRoot: string;
|
|
6
|
+
executablePath?: string;
|
|
7
|
+
headless?: boolean;
|
|
8
|
+
cdpEndpoint?: string;
|
|
9
|
+
cdpContextIndex?: number;
|
|
10
|
+
recordHarPath?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Name any human-verification widget embedded in the page.
|
|
14
|
+
*
|
|
15
|
+
* Returns nothing when the page is clear, so a normal snapshot is unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export declare function captchaReport(frameUrls: string[]): Record<string, unknown>;
|
|
18
|
+
export declare class BrowserRuntime {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private browser;
|
|
21
|
+
private context;
|
|
22
|
+
private activePage;
|
|
23
|
+
private connection;
|
|
24
|
+
private routeHandler;
|
|
25
|
+
/**
|
|
26
|
+
* Dialogs raised by the action currently running, cleared by `execute`.
|
|
27
|
+
*
|
|
28
|
+
* One buffer on the runtime rather than one per page because the handler is
|
|
29
|
+
* registered on the context, and because operations execute one at a time —
|
|
30
|
+
* a task owns its runtime for the length of one action.
|
|
31
|
+
*/
|
|
32
|
+
private dialogs;
|
|
33
|
+
constructor(options: BrowserRuntimeOptions);
|
|
34
|
+
private uploadPaths;
|
|
35
|
+
private ensureContext;
|
|
36
|
+
private page;
|
|
37
|
+
/**
|
|
38
|
+
* The tab list, with the index each entry can be addressed by.
|
|
39
|
+
*
|
|
40
|
+
* Returned by every tab action rather than only by `tabs`, because opening,
|
|
41
|
+
* switching, and closing all renumber the list — a caller that had to issue a
|
|
42
|
+
* separate `tabs` call after each one would be acting on a stale index.
|
|
43
|
+
*/
|
|
44
|
+
private describeTabs;
|
|
45
|
+
/**
|
|
46
|
+
* The frames a target may live in, main frame first.
|
|
47
|
+
*
|
|
48
|
+
* Consent banners, cookie walls, payment fields, and captcha widgets are
|
|
49
|
+
* almost always in an iframe, and `page.locator` only ever searched the main
|
|
50
|
+
* document — so the element an agent could plainly see in a screenshot was
|
|
51
|
+
* unaddressable, and the run died on an action timeout. Searching every frame
|
|
52
|
+
* with the main one first keeps precise callers unaffected and makes the
|
|
53
|
+
* embedded case work without a new field to pass.
|
|
54
|
+
*
|
|
55
|
+
* Ad and analytics pages can attach dozens of frames, so the list is bounded.
|
|
56
|
+
*/
|
|
57
|
+
private frames;
|
|
58
|
+
/**
|
|
59
|
+
* Every way the caller gave us to find the element, in the order we trust
|
|
60
|
+
* them. Split out from `locator` because waiting and acting need the same
|
|
61
|
+
* list but disagree about what to do when nothing matches yet.
|
|
62
|
+
*/
|
|
63
|
+
private candidates;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a target to a locator that actually matches something.
|
|
66
|
+
*
|
|
67
|
+
* Text matching used to be `exact: true` only, which fails on the whitespace,
|
|
68
|
+
* casing, and nested-markup differences that real pages are full of — a button
|
|
69
|
+
* rendered as `<button> Sign in </button>` never matched `"Sign in"`. Exact is
|
|
70
|
+
* still tried first so a precise caller keeps precise behaviour; the substring
|
|
71
|
+
* form is only consulted when exact found nothing.
|
|
72
|
+
*
|
|
73
|
+
* A target that matches nothing is reported as such instead of being handed to
|
|
74
|
+
* Playwright to fail as an opaque action timeout thirty seconds later.
|
|
75
|
+
*/
|
|
76
|
+
private locator;
|
|
77
|
+
/**
|
|
78
|
+
* Wait for a target to reach a state, re-resolving it on every poll.
|
|
79
|
+
*
|
|
80
|
+
* Playwright's own `waitFor` is bound to one frame decided up front, which is
|
|
81
|
+
* exactly wrong here: the consent iframe or captcha widget being waited for
|
|
82
|
+
* usually does not exist yet when the wait starts. Re-reading `page.frames()`
|
|
83
|
+
* each round costs a poll interval of latency and covers the frame that
|
|
84
|
+
* appears halfway through.
|
|
85
|
+
*
|
|
86
|
+
* `visible` and `attached` are satisfied by any one candidate; `hidden` and
|
|
87
|
+
* `detached` must hold for all of them. Fanning out across frames makes that
|
|
88
|
+
* distinction load-bearing — most frames never contain the target, so "some
|
|
89
|
+
* candidate is absent" is true from the first poll and would report a banner
|
|
90
|
+
* as dismissed while it is still on screen.
|
|
91
|
+
*/
|
|
92
|
+
private waitForTarget;
|
|
93
|
+
/**
|
|
94
|
+
* Type by pressing keys rather than by assigning the value.
|
|
95
|
+
*
|
|
96
|
+
* `.fill()` sets the field and fires a single `input` event. Anything that
|
|
97
|
+
* listens for keystrokes never sees the text: React inputs with key handlers,
|
|
98
|
+
* autocomplete dropdowns, comboboxes that filter as you type, and fields that
|
|
99
|
+
* enable a submit button only after a `keyup`. The old server used
|
|
100
|
+
* `page.type`, and losing that is most of why typing "stopped working".
|
|
101
|
+
* `.fill("")` is still how the field gets cleared, because it empties one
|
|
102
|
+
* reliably no matter what is in it.
|
|
103
|
+
*/
|
|
104
|
+
private typeInto;
|
|
105
|
+
private snapshot;
|
|
106
|
+
/**
|
|
107
|
+
* Read one element rather than the whole page.
|
|
108
|
+
*
|
|
109
|
+
* The old server had `extract_text` and `extract_html`; both were dropped, so
|
|
110
|
+
* a caller wanting one table out of a large document had to take the entire
|
|
111
|
+
* page text and cut it up itself. `inspect` already accepts a target, so
|
|
112
|
+
* scoping it needs no new action.
|
|
113
|
+
*/
|
|
114
|
+
private extract;
|
|
115
|
+
private settledSnapshot;
|
|
116
|
+
execute(operation: BrowserOperation): Promise<Record<string, unknown>>;
|
|
117
|
+
private run;
|
|
118
|
+
close(): Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
export * from "./network-policy.js";
|
|
121
|
+
export * from "./selector.js";
|
|
122
|
+
export * from "./stable-dom.js";
|
|
123
|
+
export * from "./browser-connection.js";
|
|
124
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,gBAAgB,EAAiB,MAAM,iBAAiB,CAAC;AAKvE,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAI7B,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAsCD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiB1E;AAgGD,qBAAa,cAAc;IAeb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAdpC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAAgD;IACpE;;;;;;OAMG;IACH,OAAO,CAAC,OAAO,CAAiC;gBAEnB,OAAO,EAAE,qBAAqB;YAE7C,WAAW;YA8BX,aAAa;YAqFb,IAAI;IASlB;;;;;;OAMG;YACW,YAAY;IAa1B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM;IAId;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAwBlB;;;;;;;;;;;OAWG;YACW,OAAO;IAarB;;;;;;;;;;;;;;OAcG;YACW,aAAa;IAsC3B;;;;;;;;;;OAUG;YACW,QAAQ;YAeR,QAAQ;IAwCtB;;;;;;;OAOG;YACW,OAAO;YAqBP,eAAe;IAcvB,OAAO,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAe9D,GAAG;IAsTX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAuB7B;AAED,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC"}
|