@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,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"}
|
package/dist/selector.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/** CSS identifiers may not start with a digit and may not contain punctuation. */
|
|
4
|
+
const PLAIN_IDENTIFIER = /^[A-Za-z_-][\w-]*$/;
|
|
5
|
+
function attributeValue(value) {
|
|
6
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Assemble a CSS selector addressing the element the chain ends at.
|
|
10
|
+
*
|
|
11
|
+
* `snapshot` previously reported `{tag, role, name, type}` and nothing else,
|
|
12
|
+
* which left a caller no way to turn something it had just seen into something
|
|
13
|
+
* it could act on — the only remaining option was `getByText(..., {exact: true})`,
|
|
14
|
+
* which breaks on whitespace, casing, and nested markup. Emitting a selector
|
|
15
|
+
* alongside each element closes that loop.
|
|
16
|
+
*
|
|
17
|
+
* The chain runs anchor-first. Collection stops as soon as it reaches an element
|
|
18
|
+
* carrying an `id` or `data-testid`, so a page that labels its markup produces a
|
|
19
|
+
* short, re-render-durable selector; a page that does not falls back to a
|
|
20
|
+
* positional path, which is exact for the snapshot it came from.
|
|
21
|
+
*/
|
|
22
|
+
export function buildSelector(chain) {
|
|
23
|
+
if (chain.length === 0)
|
|
24
|
+
throw new Error("browser_selector_requires_chain");
|
|
25
|
+
return chain
|
|
26
|
+
.map((step) => {
|
|
27
|
+
if (step.id !== undefined) {
|
|
28
|
+
return PLAIN_IDENTIFIER.test(step.id)
|
|
29
|
+
? `#${step.id}`
|
|
30
|
+
: `[id="${attributeValue(step.id)}"]`;
|
|
31
|
+
}
|
|
32
|
+
if (step.testId !== undefined) {
|
|
33
|
+
return `[data-testid="${attributeValue(step.testId)}"]`;
|
|
34
|
+
}
|
|
35
|
+
return `${step.tag}:nth-child(${step.nth})`;
|
|
36
|
+
})
|
|
37
|
+
.join(" > ");
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=selector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selector.js","sourceRoot":"","sources":["../src/selector.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,sCAAsC;AAiBtC,kFAAkF;AAClF,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AAE9C,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAAC,KAA0B;IACtD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IAC3E,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,EAAE;gBACf,CAAC,CAAC,QAAQ,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;QAC1C,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,iBAAiB,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;QAC1D,CAAC;QACD,OAAO,GAAG,IAAI,CAAC,GAAG,cAAc,IAAI,CAAC,GAAG,GAAG,CAAC;IAC9C,CAAC,CAAC;SACD,IAAI,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selector.test.d.ts","sourceRoot":"","sources":["../src/selector.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { buildSelector } from "./selector.js";
|
|
5
|
+
describe("buildSelector", () => {
|
|
6
|
+
it("anchors on an id when the page provides one", () => {
|
|
7
|
+
expect(buildSelector([
|
|
8
|
+
{ tag: "form", nth: 2, id: "login" },
|
|
9
|
+
{ tag: "input", nth: 3 },
|
|
10
|
+
])).toBe("#login > input:nth-child(3)");
|
|
11
|
+
});
|
|
12
|
+
it("prefers a test id over positional descent", () => {
|
|
13
|
+
expect(buildSelector([
|
|
14
|
+
{ tag: "div", nth: 1, testId: "cart" },
|
|
15
|
+
{ tag: "button", nth: 2 },
|
|
16
|
+
])).toBe('[data-testid="cart"] > button:nth-child(2)');
|
|
17
|
+
});
|
|
18
|
+
it("falls back to a positional path when nothing is labelled", () => {
|
|
19
|
+
expect(buildSelector([
|
|
20
|
+
{ tag: "html", nth: 1 },
|
|
21
|
+
{ tag: "body", nth: 2 },
|
|
22
|
+
{ tag: "a", nth: 5 },
|
|
23
|
+
])).toBe("html:nth-child(1) > body:nth-child(2) > a:nth-child(5)");
|
|
24
|
+
});
|
|
25
|
+
it("quotes an id that is not a bare CSS identifier", () => {
|
|
26
|
+
// Frameworks emit ids like `:r1:` and `user.email`, which `#id` cannot spell.
|
|
27
|
+
expect(buildSelector([{ tag: "input", nth: 1, id: ":r1:" }])).toBe('[id=":r1:"]');
|
|
28
|
+
expect(buildSelector([{ tag: "input", nth: 1, id: 'a"b' }])).toBe('[id="a\\"b"]');
|
|
29
|
+
});
|
|
30
|
+
it("refuses an empty chain rather than emitting a selector matching everything", () => {
|
|
31
|
+
expect(() => buildSelector([])).toThrow("browser_selector_requires_chain");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=selector.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selector.test.js","sourceRoot":"","sources":["../src/selector.test.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,sCAAsC;AAEtC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CACJ,aAAa,CAAC;YACZ,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE;YACpC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE;SACzB,CAAC,CACH,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CACJ,aAAa,CAAC;YACZ,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE;YACtC,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE;SAC1B,CAAC,CACH,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,CACJ,aAAa,CAAC;YACZ,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE;YACvB,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE;YACvB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE;SACrB,CAAC,CACH,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,8EAA8E;QAC9E,MAAM,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAChE,aAAa,CACd,CAAC;QACF,MAAM,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAC/D,cAAc,CACf,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Page } from "playwright-core";
|
|
2
|
+
export interface DomStabilityOptions {
|
|
3
|
+
quietWindowMs?: number;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface DomStabilityResult {
|
|
7
|
+
waitedMs: number;
|
|
8
|
+
mutationCount: number;
|
|
9
|
+
timedOut: boolean;
|
|
10
|
+
reason: "quiet_window" | "timeout" | "page_unavailable";
|
|
11
|
+
}
|
|
12
|
+
export declare function normalizeDomStabilityOptions(options?: DomStabilityOptions): Required<DomStabilityOptions>;
|
|
13
|
+
/**
|
|
14
|
+
* Wait until DOM mutations have been quiet for a bounded window.
|
|
15
|
+
*
|
|
16
|
+
* This is intentionally condition-based instead of a fixed sleep. It returns
|
|
17
|
+
* its own measurement so callers can distinguish a stable observation from a
|
|
18
|
+
* timeout and preserve that distinction in execution evidence.
|
|
19
|
+
*/
|
|
20
|
+
export declare function waitForStableDom(page: Page, options?: DomStabilityOptions): Promise<DomStabilityResult>;
|
|
21
|
+
//# sourceMappingURL=stable-dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-dom.d.ts","sourceRoot":"","sources":["../src/stable-dom.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,kBAAkB,CAAC;CACzD;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,mBAAwB,GAChC,QAAQ,CAAC,mBAAmB,CAAC,CAU/B;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAqE7B"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
export function normalizeDomStabilityOptions(options = {}) {
|
|
4
|
+
const quietWindowMs = Math.max(25, Math.min(2_000, Math.round(options.quietWindowMs ?? 180)));
|
|
5
|
+
const timeoutMs = Math.max(quietWindowMs, Math.min(10_000, Math.round(options.timeoutMs ?? 1_500)));
|
|
6
|
+
return { quietWindowMs, timeoutMs };
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Wait until DOM mutations have been quiet for a bounded window.
|
|
10
|
+
*
|
|
11
|
+
* This is intentionally condition-based instead of a fixed sleep. It returns
|
|
12
|
+
* its own measurement so callers can distinguish a stable observation from a
|
|
13
|
+
* timeout and preserve that distinction in execution evidence.
|
|
14
|
+
*/
|
|
15
|
+
export async function waitForStableDom(page, options = {}) {
|
|
16
|
+
if (page.isClosed()) {
|
|
17
|
+
return {
|
|
18
|
+
waitedMs: 0,
|
|
19
|
+
mutationCount: 0,
|
|
20
|
+
timedOut: false,
|
|
21
|
+
reason: "page_unavailable",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const normalized = normalizeDomStabilityOptions(options);
|
|
25
|
+
try {
|
|
26
|
+
return await page.evaluate(({ quietWindowMs, timeoutMs }) => new Promise((resolve) => {
|
|
27
|
+
const startedAt = performance.now();
|
|
28
|
+
let mutations = 0;
|
|
29
|
+
let quietTimer;
|
|
30
|
+
let timeoutTimer;
|
|
31
|
+
let settled = false;
|
|
32
|
+
const finish = (reason, timedOut) => {
|
|
33
|
+
if (settled)
|
|
34
|
+
return;
|
|
35
|
+
settled = true;
|
|
36
|
+
observer.disconnect();
|
|
37
|
+
if (quietTimer !== undefined)
|
|
38
|
+
clearTimeout(quietTimer);
|
|
39
|
+
if (timeoutTimer !== undefined)
|
|
40
|
+
clearTimeout(timeoutTimer);
|
|
41
|
+
resolve({
|
|
42
|
+
waitedMs: Math.round(performance.now() - startedAt),
|
|
43
|
+
mutationCount: mutations,
|
|
44
|
+
timedOut,
|
|
45
|
+
reason,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const armQuietWindow = () => {
|
|
49
|
+
if (quietTimer !== undefined)
|
|
50
|
+
clearTimeout(quietTimer);
|
|
51
|
+
quietTimer = setTimeout(() => finish("quiet_window", false), quietWindowMs);
|
|
52
|
+
};
|
|
53
|
+
const observer = new MutationObserver((records) => {
|
|
54
|
+
mutations += records.length;
|
|
55
|
+
armQuietWindow();
|
|
56
|
+
});
|
|
57
|
+
observer.observe(document.documentElement, {
|
|
58
|
+
attributes: true,
|
|
59
|
+
characterData: true,
|
|
60
|
+
childList: true,
|
|
61
|
+
subtree: true,
|
|
62
|
+
});
|
|
63
|
+
timeoutTimer = setTimeout(() => finish("timeout", true), timeoutMs);
|
|
64
|
+
armQuietWindow();
|
|
65
|
+
}), normalized);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return {
|
|
69
|
+
waitedMs: 0,
|
|
70
|
+
mutationCount: 0,
|
|
71
|
+
timedOut: false,
|
|
72
|
+
reason: "page_unavailable",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=stable-dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-dom.js","sourceRoot":"","sources":["../src/stable-dom.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-dom.test.d.ts","sourceRoot":"","sources":["../src/stable-dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { normalizeDomStabilityOptions } from "./stable-dom.js";
|
|
5
|
+
describe("DOM stability options", () => {
|
|
6
|
+
it("applies bounded defaults", () => {
|
|
7
|
+
expect(normalizeDomStabilityOptions()).toEqual({
|
|
8
|
+
quietWindowMs: 180,
|
|
9
|
+
timeoutMs: 1_500,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it("keeps the timeout at or above the quiet window", () => {
|
|
13
|
+
expect(normalizeDomStabilityOptions({
|
|
14
|
+
quietWindowMs: 900,
|
|
15
|
+
timeoutMs: 100,
|
|
16
|
+
})).toEqual({
|
|
17
|
+
quietWindowMs: 900,
|
|
18
|
+
timeoutMs: 900,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=stable-dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-dom.test.js","sourceRoot":"","sources":["../src/stable-dom.test.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,sCAAsC;AAEtC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAE/D,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,CAAC,4BAA4B,EAAE,CAAC,CAAC,OAAO,CAAC;YAC7C,aAAa,EAAE,GAAG;YAClB,SAAS,EAAE,KAAK;SACjB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CACJ,4BAA4B,CAAC;YAC3B,aAAa,EAAE,GAAG;YAClB,SAAS,EAAE,GAAG;SACf,CAAC,CACH,CAAC,OAAO,CAAC;YACR,aAAa,EAAE,GAAG;YAClB,SAAS,EAAE,GAAG;SACf,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@melra/browser-runtime",
|
|
3
|
+
"version": "0.3.0-alpha.3",
|
|
4
|
+
"description": "Policy-governed browser automation runtime for MELRA.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/XAGI-Lab/melra.git",
|
|
9
|
+
"directory": "packages/browser-runtime"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/XAGI-Lab/melra#readme",
|
|
12
|
+
"bugs": "https://github.com/XAGI-Lab/melra/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"playwright-core": "1.62.0",
|
|
25
|
+
"@melra/protocol": "0.3.0-alpha.3"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"clean": "rm -rf dist",
|
|
30
|
+
"test": "vitest run --testTimeout=60000 --hookTimeout=60000",
|
|
31
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
32
|
+
}
|
|
33
|
+
}
|