@melra/browser-runtime 0.3.0-alpha.10
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/README.md +44 -0
- package/dist/browser-connection.d.ts +25 -0
- package/dist/browser-connection.d.ts.map +1 -0
- package/dist/browser-connection.js +119 -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 +210 -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 +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +939 -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 +28 -0
- package/dist/network-policy.d.ts.map +1 -0
- package/dist/network-policy.js +121 -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 +39 -0
- package/dist/network-policy.test.js.map +1 -0
- package/dist/pinning-proxy.d.ts +8 -0
- package/dist/pinning-proxy.d.ts.map +1 -0
- package/dist/pinning-proxy.js +115 -0
- package/dist/pinning-proxy.js.map +1 -0
- package/dist/pinning-proxy.test.d.ts +2 -0
- package/dist/pinning-proxy.test.d.ts.map +1 -0
- package/dist/pinning-proxy.test.js +80 -0
- package/dist/pinning-proxy.test.js.map +1 -0
- package/dist/popups-profile.test.d.ts +2 -0
- package/dist/popups-profile.test.d.ts.map +1 -0
- package/dist/popups-profile.test.js +170 -0
- package/dist/popups-profile.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,115 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Loopback proxy that closes the DNS-rebinding window.
|
|
4
|
+
//
|
|
5
|
+
// `assertSafeDestination` resolves a hostname and checks every answer, but the
|
|
6
|
+
// browser then resolves the same name again through its own stack. A hostile
|
|
7
|
+
// resolver only has to answer public the first time and private the second, and
|
|
8
|
+
// the check that passed has nothing to do with the socket that opens. Chromium
|
|
9
|
+
// cannot be told to reuse our answer, so the fix is to stop it from connecting
|
|
10
|
+
// at all: it talks to this proxy, and the proxy connects to the address it just
|
|
11
|
+
// checked.
|
|
12
|
+
//
|
|
13
|
+
// Loopback destinations bypass the proxy — Chromium does that by default and
|
|
14
|
+
// there is nothing to rebind about a literal `127.0.0.1`, which the route layer
|
|
15
|
+
// already checks against policy.
|
|
16
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
17
|
+
import { connect } from "node:net";
|
|
18
|
+
import { assertSafeDestination } from "./network-policy.js";
|
|
19
|
+
/** Enough for a refusal to be legible in a network log; the page sees a failure. */
|
|
20
|
+
function refuse(socket, reason) {
|
|
21
|
+
socket.end(`HTTP/1.1 502 ${reason}\r\nConnection: close\r\n\r\n`);
|
|
22
|
+
}
|
|
23
|
+
function pipeBothWays(client, upstream, head) {
|
|
24
|
+
// Either half closing has to take the other down, or a stalled tunnel keeps a
|
|
25
|
+
// socket pair alive for the life of the browser.
|
|
26
|
+
const end = () => {
|
|
27
|
+
client.destroy();
|
|
28
|
+
upstream.destroy();
|
|
29
|
+
};
|
|
30
|
+
client.on("error", end);
|
|
31
|
+
upstream.on("error", end);
|
|
32
|
+
if (head.length > 0)
|
|
33
|
+
upstream.write(head);
|
|
34
|
+
client.pipe(upstream);
|
|
35
|
+
upstream.pipe(client);
|
|
36
|
+
}
|
|
37
|
+
export async function startPinningProxy(policy) {
|
|
38
|
+
const sockets = new Set();
|
|
39
|
+
/**
|
|
40
|
+
* Track a socket for shutdown and take responsibility for its errors. A proxy
|
|
41
|
+
* socket resetting is normal — the browser drops keep-alive connections, and
|
|
42
|
+
* `close()` destroys whatever is still open — and an untracked `error` on a raw
|
|
43
|
+
* socket is an uncaught exception that takes the host process down with it.
|
|
44
|
+
*/
|
|
45
|
+
const track = (socket) => {
|
|
46
|
+
sockets.add(socket);
|
|
47
|
+
socket.on("error", () => socket.destroy());
|
|
48
|
+
socket.on("close", () => sockets.delete(socket));
|
|
49
|
+
};
|
|
50
|
+
const server = createServer((req, res) => {
|
|
51
|
+
void (async () => {
|
|
52
|
+
try {
|
|
53
|
+
const { url, address } = await assertSafeDestination(req.url ?? "", policy);
|
|
54
|
+
const upstream = httpRequest({
|
|
55
|
+
host: address,
|
|
56
|
+
port: url.port === "" ? 80 : Number(url.port),
|
|
57
|
+
method: req.method ?? "GET",
|
|
58
|
+
path: `${url.pathname}${url.search}`,
|
|
59
|
+
// The original headers carry `Host`, so a name-based virtual host
|
|
60
|
+
// still resolves correctly at the far end even though the socket
|
|
61
|
+
// was opened to a literal address.
|
|
62
|
+
headers: req.headers,
|
|
63
|
+
}, (upstreamRes) => {
|
|
64
|
+
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
65
|
+
upstreamRes.pipe(res);
|
|
66
|
+
});
|
|
67
|
+
upstream.on("error", () => res.destroy());
|
|
68
|
+
req.pipe(upstream);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
res.writeHead(502).end();
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
});
|
|
75
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
76
|
+
void (async () => {
|
|
77
|
+
try {
|
|
78
|
+
// CONNECT carries `host:port`, not a URL. Only the authority is checked
|
|
79
|
+
// because a tunnel has no path to check — the request inside it is TLS.
|
|
80
|
+
const { address, url } = await assertSafeDestination(`https://${req.url ?? ""}`, policy);
|
|
81
|
+
const upstream = connect({ host: address, port: url.port === "" ? 443 : Number(url.port) }, () => {
|
|
82
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
83
|
+
pipeBothWays(clientSocket, upstream, head);
|
|
84
|
+
});
|
|
85
|
+
track(upstream);
|
|
86
|
+
upstream.on("error", () => refuse(clientSocket, "Bad Gateway"));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
refuse(clientSocket, "Blocked");
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
});
|
|
93
|
+
server.on("connection", track);
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
server.once("error", reject);
|
|
96
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
97
|
+
});
|
|
98
|
+
const listening = server.address();
|
|
99
|
+
if (listening === null || typeof listening === "string") {
|
|
100
|
+
server.close();
|
|
101
|
+
throw new Error("browser_proxy_unavailable");
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
server: `http://127.0.0.1:${listening.port}`,
|
|
105
|
+
close: () => new Promise((resolve) => {
|
|
106
|
+
// A tunnel keeps its socket open by design, so `close()` alone waits
|
|
107
|
+
// forever. Destroying first is what makes shutdown bounded.
|
|
108
|
+
for (const socket of sockets)
|
|
109
|
+
socket.destroy();
|
|
110
|
+
sockets.clear();
|
|
111
|
+
server.close(() => resolve());
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=pinning-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinning-proxy.js","sourceRoot":"","sources":["../src/pinning-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinning-proxy.test.d.ts","sourceRoot":"","sources":["../src/pinning-proxy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
4
|
+
import { connect } from "node:net";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
// `rebind.test` resolves nowhere in reality, which is what makes this a test of
|
|
7
|
+
// pinning rather than of DNS: only a proxy that connects to the address the
|
|
8
|
+
// policy check returned can reach the server below. One that resolved the name
|
|
9
|
+
// a second time — the rebinding window — would fail outright.
|
|
10
|
+
vi.mock("node:dns/promises", () => ({
|
|
11
|
+
lookup: async () => [{ address: "127.0.0.1", family: 4 }],
|
|
12
|
+
}));
|
|
13
|
+
const { startPinningProxy } = await import("./pinning-proxy.js");
|
|
14
|
+
async function listen() {
|
|
15
|
+
const server = createServer((_req, res) => res.end("pinned"));
|
|
16
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
17
|
+
return {
|
|
18
|
+
port: server.address().port,
|
|
19
|
+
close: () => server.close(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function throughProxy(proxyPort, target) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
// Absolute-form request URI: how a browser addresses a proxy.
|
|
25
|
+
const req = httpRequest({ host: "127.0.0.1", port: proxyPort, path: target }, (res) => {
|
|
26
|
+
let body = "";
|
|
27
|
+
res.on("data", (chunk) => (body += String(chunk)));
|
|
28
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
|
|
29
|
+
});
|
|
30
|
+
req.on("error", reject);
|
|
31
|
+
req.end();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
describe("pinning proxy", () => {
|
|
35
|
+
it("connects to the address the policy check returned", async () => {
|
|
36
|
+
const upstream = await listen();
|
|
37
|
+
const proxy = await startPinningProxy({
|
|
38
|
+
allowedDomains: ["*"],
|
|
39
|
+
allowLocalhost: true,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
const port = Number(new URL(proxy.server).port);
|
|
43
|
+
const response = await throughProxy(port, `http://rebind.test:${upstream.port}/`);
|
|
44
|
+
expect(response).toEqual({ status: 200, body: "pinned" });
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await proxy.close();
|
|
48
|
+
upstream.close();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
it("refuses a destination policy denies", async () => {
|
|
52
|
+
const upstream = await listen();
|
|
53
|
+
const proxy = await startPinningProxy({
|
|
54
|
+
allowedDomains: ["*"],
|
|
55
|
+
allowLocalhost: false,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
const port = Number(new URL(proxy.server).port);
|
|
59
|
+
const response = await throughProxy(port, `http://rebind.test:${upstream.port}/`);
|
|
60
|
+
expect(response.status).toBe(502);
|
|
61
|
+
// CONNECT carries no path to check, so the authority has to be enough.
|
|
62
|
+
const refusal = await new Promise((resolve, reject) => {
|
|
63
|
+
const socket = connect(port, "127.0.0.1", () => {
|
|
64
|
+
socket.write(`CONNECT rebind.test:443 HTTP/1.1\r\nHost: rebind.test:443\r\n\r\n`);
|
|
65
|
+
});
|
|
66
|
+
socket.on("data", (chunk) => {
|
|
67
|
+
resolve(String(chunk));
|
|
68
|
+
socket.destroy();
|
|
69
|
+
});
|
|
70
|
+
socket.on("error", reject);
|
|
71
|
+
});
|
|
72
|
+
expect(refusal).toContain("502");
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await proxy.close();
|
|
76
|
+
upstream.close();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=pinning-proxy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinning-proxy.test.js","sourceRoot":"","sources":["../src/pinning-proxy.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popups-profile.test.d.ts","sourceRoot":"","sources":["../src/popups-profile.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
* Two gaps, both invisible to the caller. A click that spawned a window
|
|
12
|
+
* returned plain success while the window sat there holding focus and a
|
|
13
|
+
* session, and every run started from an empty profile, so a site that had
|
|
14
|
+
* been logged into had to be logged into again on the next task.
|
|
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
|
+
/**
|
|
25
|
+
* `/` opens a window on click; `/set` plants a cookie; `/read` reports whether
|
|
26
|
+
* the cookie survived. One server covers both halves of this file.
|
|
27
|
+
*/
|
|
28
|
+
async function serve() {
|
|
29
|
+
const server = createServer((request, response) => {
|
|
30
|
+
const path = (request.url ?? "/").split("?")[0];
|
|
31
|
+
if (path === "/set") {
|
|
32
|
+
response.writeHead(200, {
|
|
33
|
+
"content-type": "text/html; charset=utf-8",
|
|
34
|
+
"set-cookie": "melra=kept; Path=/; Max-Age=3600",
|
|
35
|
+
});
|
|
36
|
+
response.end("<!doctype html><title>set</title><body>set</body>");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (path === "/read") {
|
|
40
|
+
const seen = (request.headers.cookie ?? "").includes("melra=kept");
|
|
41
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
42
|
+
response.end(`<!doctype html><title>read</title><body><div id="cookie">${seen ? "kept" : "gone"}</div></body>`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
46
|
+
response.end(`<!doctype html><title>opener</title><body>
|
|
47
|
+
<button id="pop">Open</button>
|
|
48
|
+
<script>
|
|
49
|
+
document.getElementById('pop').addEventListener('click', () => {
|
|
50
|
+
window.open('/read', '_blank');
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
</body>`);
|
|
54
|
+
});
|
|
55
|
+
servers.push(server);
|
|
56
|
+
await new Promise((done) => {
|
|
57
|
+
server.listen(0, "127.0.0.1", done);
|
|
58
|
+
});
|
|
59
|
+
const address = server.address();
|
|
60
|
+
if (address === null || typeof address === "string") {
|
|
61
|
+
throw new Error("fixture_server_address_unavailable");
|
|
62
|
+
}
|
|
63
|
+
return `http://127.0.0.1:${address.port}`;
|
|
64
|
+
}
|
|
65
|
+
async function root(name) {
|
|
66
|
+
const created = await mkdtemp(join(tmpdir(), `melra-${name}-`));
|
|
67
|
+
roots.push(created);
|
|
68
|
+
return created;
|
|
69
|
+
}
|
|
70
|
+
const runtime = (workspaceRoot, executablePath, extra = {}) => new BrowserRuntime({
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
artifactDirectory: join(workspaceRoot, "artifacts"),
|
|
73
|
+
executablePath,
|
|
74
|
+
allowedDomains: ["127.0.0.1"],
|
|
75
|
+
allowLocalhost: true,
|
|
76
|
+
...extra,
|
|
77
|
+
});
|
|
78
|
+
const op = (fields) => BrowserOperationSchema.parse({ kind: "browser", ...fields });
|
|
79
|
+
describe("browser popups", () => {
|
|
80
|
+
it("reports the window a page opened and closes it by default", async () => {
|
|
81
|
+
const executablePath = await detectBrowserExecutable();
|
|
82
|
+
if (executablePath === undefined)
|
|
83
|
+
return;
|
|
84
|
+
const url = await serve();
|
|
85
|
+
const session = runtime(await root("popup-block"), executablePath);
|
|
86
|
+
try {
|
|
87
|
+
await session.execute(op({ action: "navigate", url }));
|
|
88
|
+
const clicked = await session.execute(op({ action: "click", target: { selector: "#pop" } }));
|
|
89
|
+
// The point: the caller is told what the click opened behind it.
|
|
90
|
+
expect(clicked.popups).toEqual([
|
|
91
|
+
expect.objectContaining({ url: `${url}/read`, blocked: true }),
|
|
92
|
+
]);
|
|
93
|
+
// And it is gone by the time the next action runs, not eventually.
|
|
94
|
+
const tabs = await session.execute(op({ action: "tabs" }));
|
|
95
|
+
expect(tabs.tabs.length).toBe(1);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await session.close();
|
|
99
|
+
}
|
|
100
|
+
}, 120_000);
|
|
101
|
+
it("keeps the window as an addressable tab when popups are allowed", async () => {
|
|
102
|
+
const executablePath = await detectBrowserExecutable();
|
|
103
|
+
if (executablePath === undefined)
|
|
104
|
+
return;
|
|
105
|
+
const url = await serve();
|
|
106
|
+
const session = runtime(await root("popup-allow"), executablePath, {
|
|
107
|
+
popups: "allow",
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
await session.execute(op({ action: "navigate", url }));
|
|
111
|
+
const clicked = await session.execute(op({ action: "click", target: { selector: "#pop" } }));
|
|
112
|
+
expect(clicked.popups[0]?.blocked).toBe(false);
|
|
113
|
+
const tabs = await session.execute(op({ action: "tabs" }));
|
|
114
|
+
expect(tabs.tabs.length).toBe(2);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await session.close();
|
|
118
|
+
}
|
|
119
|
+
}, 120_000);
|
|
120
|
+
it("does not mistake a tab the caller asked for for a popup", async () => {
|
|
121
|
+
const executablePath = await detectBrowserExecutable();
|
|
122
|
+
if (executablePath === undefined)
|
|
123
|
+
return;
|
|
124
|
+
const url = await serve();
|
|
125
|
+
const session = runtime(await root("popup-tab-new"), executablePath);
|
|
126
|
+
try {
|
|
127
|
+
await session.execute(op({ action: "navigate", url }));
|
|
128
|
+
// Both arrive as the same `page` event. If this were treated as a popup,
|
|
129
|
+
// `tab_new` would close the tab it just opened.
|
|
130
|
+
const opened = await session.execute(op({ action: "tab_new", url: `${url}/read` }));
|
|
131
|
+
expect(opened.popups).toBeUndefined();
|
|
132
|
+
expect((await session.execute(op({ action: "tabs" }))).tabs).toHaveLength(2);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await session.close();
|
|
136
|
+
}
|
|
137
|
+
}, 120_000);
|
|
138
|
+
});
|
|
139
|
+
describe("persistent browser profile", () => {
|
|
140
|
+
it("keeps a cookie across two sessions, and drops it without a profile", async () => {
|
|
141
|
+
const executablePath = await detectBrowserExecutable();
|
|
142
|
+
if (executablePath === undefined)
|
|
143
|
+
return;
|
|
144
|
+
const url = await serve();
|
|
145
|
+
const workspace = await root("profile");
|
|
146
|
+
const userDataDir = join(workspace, "profile");
|
|
147
|
+
const read = async (extra) => {
|
|
148
|
+
const session = runtime(workspace, executablePath, extra);
|
|
149
|
+
try {
|
|
150
|
+
await session.execute(op({ action: "navigate", url: `${url}/read` }));
|
|
151
|
+
const seen = await session.execute(op({ action: "inspect", target: { selector: "#cookie" } }));
|
|
152
|
+
return String(seen.text ?? "").trim();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await session.close();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const first = runtime(workspace, executablePath, { userDataDir });
|
|
159
|
+
try {
|
|
160
|
+
await first.execute(op({ action: "navigate", url: `${url}/set` }));
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await first.close();
|
|
164
|
+
}
|
|
165
|
+
expect(await read({ userDataDir })).toBe("kept");
|
|
166
|
+
// The default is still a throwaway profile, so nothing leaked into it.
|
|
167
|
+
expect(await read({})).toBe("gone");
|
|
168
|
+
}, 180_000);
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=popups-profile.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popups-profile.test.js","sourceRoot":"","sources":["../src/popups-profile.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"see-act.test.d.ts","sourceRoot":"","sources":["../src/see-act.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
* The see/act loop is the thing that was broken: a caller could read a snapshot
|
|
12
|
+
* but had no way to turn what it saw into a target it could act on. These tests
|
|
13
|
+
* drive a real page, so they fail if the emitted selector does not address the
|
|
14
|
+
* element it was reported for.
|
|
15
|
+
*/
|
|
16
|
+
const PAGE = `<!doctype html><html><head><title>See act</title></head><body>
|
|
17
|
+
<form id="login">
|
|
18
|
+
<input name="email" placeholder="Email">
|
|
19
|
+
<input name="password" type="password">
|
|
20
|
+
<button type="submit" data-testid="submit"> Sign in </button>
|
|
21
|
+
</form>
|
|
22
|
+
<div><span>Total</span><section id="cart"><p>2 items</p></section></div>
|
|
23
|
+
<button disabled>Locked</button>
|
|
24
|
+
</body></html>`;
|
|
25
|
+
const roots = [];
|
|
26
|
+
const servers = [];
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await Promise.all(servers.splice(0).map((server) => new Promise((done) => {
|
|
29
|
+
server.close(() => done());
|
|
30
|
+
})));
|
|
31
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
32
|
+
});
|
|
33
|
+
async function fixture() {
|
|
34
|
+
const executablePath = await detectBrowserExecutable();
|
|
35
|
+
if (executablePath === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
const server = createServer((_request, response) => {
|
|
38
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
39
|
+
response.end(PAGE);
|
|
40
|
+
});
|
|
41
|
+
servers.push(server);
|
|
42
|
+
await new Promise((done) => {
|
|
43
|
+
server.listen(0, "127.0.0.1", done);
|
|
44
|
+
});
|
|
45
|
+
const address = server.address();
|
|
46
|
+
if (address === null || typeof address === "string") {
|
|
47
|
+
throw new Error("fixture_server_address_unavailable");
|
|
48
|
+
}
|
|
49
|
+
const root = await mkdtemp(join(tmpdir(), "melra-see-act-"));
|
|
50
|
+
roots.push(root);
|
|
51
|
+
return {
|
|
52
|
+
runtime: new BrowserRuntime({
|
|
53
|
+
workspaceRoot: root,
|
|
54
|
+
artifactDirectory: join(root, "artifacts"),
|
|
55
|
+
executablePath,
|
|
56
|
+
allowedDomains: ["127.0.0.1"],
|
|
57
|
+
allowLocalhost: true,
|
|
58
|
+
}),
|
|
59
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
describe("browser see/act loop", () => {
|
|
63
|
+
it("emits a selector that addresses the element it was reported for", async () => {
|
|
64
|
+
const context = await fixture();
|
|
65
|
+
if (context === undefined)
|
|
66
|
+
return;
|
|
67
|
+
const { runtime, url } = context;
|
|
68
|
+
try {
|
|
69
|
+
const observed = await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
70
|
+
const elements = observed.elements;
|
|
71
|
+
const submit = elements.find((item) => item.testId === "submit");
|
|
72
|
+
expect(submit?.selector).toBe('[data-testid="submit"]');
|
|
73
|
+
const email = elements.find((item) => item.attributeName === "email");
|
|
74
|
+
// Anchored on the form's id rather than a path from <html>.
|
|
75
|
+
expect(email?.selector).toBe("#login > input:nth-child(1)");
|
|
76
|
+
expect(email?.placeholder).toBe("Email");
|
|
77
|
+
expect(elements.find((item) => item.name === "Locked")?.disabled).toBe(true);
|
|
78
|
+
expect(elements.every((item) => typeof item.index === "number")).toBe(true);
|
|
79
|
+
// The loop closes only if the selector can be acted on.
|
|
80
|
+
const typed = await runtime.execute(BrowserOperationSchema.parse({
|
|
81
|
+
kind: "browser",
|
|
82
|
+
action: "type",
|
|
83
|
+
target: { selector: email.selector },
|
|
84
|
+
value: "user@example.com",
|
|
85
|
+
}));
|
|
86
|
+
expect(typed.typed).toBe(true);
|
|
87
|
+
const after = (await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "inspect" }))).elements.find((item) => item.attributeName === "email");
|
|
88
|
+
expect(after?.value).toBe("user@example.com");
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await runtime.close();
|
|
92
|
+
}
|
|
93
|
+
}, 120_000);
|
|
94
|
+
it("matches text through surrounding whitespace instead of failing exact", async () => {
|
|
95
|
+
const context = await fixture();
|
|
96
|
+
if (context === undefined)
|
|
97
|
+
return;
|
|
98
|
+
const { runtime, url } = context;
|
|
99
|
+
try {
|
|
100
|
+
await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
101
|
+
// `<button> Sign in </button>` never matched `getByText("Sign in", {exact:true})`.
|
|
102
|
+
const clicked = await runtime.execute(BrowserOperationSchema.parse({
|
|
103
|
+
kind: "browser",
|
|
104
|
+
action: "click",
|
|
105
|
+
target: { text: "Sign in" },
|
|
106
|
+
}));
|
|
107
|
+
expect(clicked.clicked).toBe(true);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await runtime.close();
|
|
111
|
+
}
|
|
112
|
+
}, 120_000);
|
|
113
|
+
it("reports a target that matches nothing instead of timing out opaquely", async () => {
|
|
114
|
+
const context = await fixture();
|
|
115
|
+
if (context === undefined)
|
|
116
|
+
return;
|
|
117
|
+
const { runtime, url } = context;
|
|
118
|
+
try {
|
|
119
|
+
await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
120
|
+
await expect(runtime.execute(BrowserOperationSchema.parse({
|
|
121
|
+
kind: "browser",
|
|
122
|
+
action: "click",
|
|
123
|
+
target: { selector: "#nothing-here" },
|
|
124
|
+
timeoutMs: 120_000,
|
|
125
|
+
}))).rejects.toThrow("browser_target_not_found");
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await runtime.close();
|
|
129
|
+
}
|
|
130
|
+
}, 120_000);
|
|
131
|
+
it("scopes extraction to a target rather than returning the whole page", async () => {
|
|
132
|
+
const context = await fixture();
|
|
133
|
+
if (context === undefined)
|
|
134
|
+
return;
|
|
135
|
+
const { runtime, url } = context;
|
|
136
|
+
try {
|
|
137
|
+
await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
|
|
138
|
+
const extracted = await runtime.execute(BrowserOperationSchema.parse({
|
|
139
|
+
kind: "browser",
|
|
140
|
+
action: "inspect",
|
|
141
|
+
target: { selector: "#cart" },
|
|
142
|
+
}));
|
|
143
|
+
expect(extracted.text).toBe("2 items");
|
|
144
|
+
expect(extracted.html).toBe("<p>2 items</p>");
|
|
145
|
+
expect(extracted.text).not.toContain("Total");
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await runtime.close();
|
|
149
|
+
}
|
|
150
|
+
}, 120_000);
|
|
151
|
+
});
|
|
152
|
+
//# sourceMappingURL=see-act.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"see-act.test.js","sourceRoot":"","sources":["../src/see-act.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One ancestor step on the path from an anchor down to a snapshot element.
|
|
3
|
+
*
|
|
4
|
+
* The page collects these as plain data and the selector string is assembled
|
|
5
|
+
* here, in Node, so the interesting logic is a pure function of plain objects
|
|
6
|
+
* and can be tested without a DOM.
|
|
7
|
+
*/
|
|
8
|
+
export interface ElementDescriptor {
|
|
9
|
+
tag: string;
|
|
10
|
+
/** 1-based position among the parent's element children. */
|
|
11
|
+
nth: number;
|
|
12
|
+
id?: string;
|
|
13
|
+
testId?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Assemble a CSS selector addressing the element the chain ends at.
|
|
17
|
+
*
|
|
18
|
+
* `snapshot` previously reported `{tag, role, name, type}` and nothing else,
|
|
19
|
+
* which left a caller no way to turn something it had just seen into something
|
|
20
|
+
* it could act on — the only remaining option was `getByText(..., {exact: true})`,
|
|
21
|
+
* which breaks on whitespace, casing, and nested markup. Emitting a selector
|
|
22
|
+
* alongside each element closes that loop.
|
|
23
|
+
*
|
|
24
|
+
* The chain runs anchor-first. Collection stops as soon as it reaches an element
|
|
25
|
+
* carrying an `id` or `data-testid`, so a page that labels its markup produces a
|
|
26
|
+
* short, re-render-durable selector; a page that does not falls back to a
|
|
27
|
+
* positional path, which is exact for the snapshot it came from.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildSelector(chain: ElementDescriptor[]): string;
|
|
30
|
+
//# sourceMappingURL=selector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../src/selector.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAehE"}
|