gz-laya-pilot 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LayaPilot contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,11 @@
1
+ # Source and changes
2
+
3
+ Derived from [anthonyli/laya-pilot](https://github.com/anthonyli/laya-pilot), MIT, copyright 2026 LayaPilot contributors. This is an independent package, not an official upstream release.
4
+
5
+ The repository already contains the complete adapted source snapshot under `tools/laya-pilot`. Its original archive SHA-256 is `88d4932109b64f40a8c13f82f43cfc6d33e371344f13b688bad4967705f14b30`. An upstream commit was not retained with that snapshot; no commit is claimed here.
6
+
7
+ Ported `lib/browser-provider.mjs`, the `waitForReady` function in `lib/readiness.mjs`, and decision validation / HTTP protocol from `lib/model.mjs` to strict TypeScript functions. Added schema validation, deterministic recording replay, declarative assertions and semantic target metadata. Removed application routes, demo credentials, workflow classes, implicit logs and unbounded caches from this package. LICENSE retains the upstream notice.
8
+
9
+ The legacy Excel generator and Python model worker remain in the repository's old tools directory. They are not silently replaced by this package. Recorded JSON cases use Playwright without model inference; `createApiDecision` and `chooseStep` support an explicitly configured Laya-compatible decision endpoint. Local model weights and inference runtime are not distributed.
10
+
11
+ Playwright and dom-accessibility-api retain their own licenses. No source or dependency claims an npm release of upstream `laya-pilot`.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # gz-laya-pilot
2
+
3
+ Node 22+ / TypeScript functional runtime derived from LayaPilot. ESM with declarations; no framework, business login or persistence built in.
4
+
5
+ ```sh
6
+ npm install gz-laya-pilot
7
+ npx playwright install chromium
8
+ ```
9
+
10
+ ```ts
11
+ import { runCase } from 'gz-laya-pilot';
12
+ const result = await runCase(
13
+ {
14
+ schema: 'zhidao-recorded-case',
15
+ version: 1,
16
+ name: 'Customer search',
17
+ path: '/customers',
18
+ steps: [
19
+ {
20
+ kind: 'assert',
21
+ assertion: {
22
+ name: 'Page ready',
23
+ source: 'element-state',
24
+ selector: '[data-testid="search"]',
25
+ operator: 'visible',
26
+ timeout: 5000,
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ {
32
+ origin: 'http://127.0.0.1:3000',
33
+ launch: { headless: true },
34
+ onProgress: console.log,
35
+ signal: new AbortController().signal,
36
+ },
37
+ );
38
+ ```
39
+
40
+ Use `beforeRun(page, context)` for your host's authentication or test setup; use `context.storageState` for an existing test login. Credentials never belong in case JSON. `allowedOrigins` explicitly permits API/asset origins when the test app needs them. By default only the supplied origin is allowed. Abort closes the browser. A run with no assertions reports `completed`, not `passed`.
41
+
42
+ Browser consumers import `gz-laya-pilot/schema` or `gz-laya-pilot/browser`; neither entry imports Node or Playwright. `locate()` uses unique CSS plus W3C accessible name/role. Version 1 CSS/text targets still work. Automatic retry never substitutes another business control.
43
+
44
+ `createApiDecision({base, model, key})` returns a Laya-protocol decision function. `chooseStep(instruction, observedCandidates, decide)` selects a structured step or returns null for uncertainty. It does not execute the candidate or invent locators. The host must observe candidates and review the draft; `runCase` evaluates assertions against real DOM, not model opinions. JSON replay does not require Python. The existing Excel / local Python pipeline remains under `tools/laya-pilot` in the source repository.
45
+
46
+ See NOTICE.md and LICENSE for upstream provenance and the scope of the port.
@@ -0,0 +1,3 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Assertion, AssertionResult } from './schema.js';
3
+ export declare function runAssertion(page: Page, assertion: Assertion, step: number, isStopped?: () => boolean): Promise<AssertionResult>;
@@ -0,0 +1,111 @@
1
+ // Declarative browser assertions. Native CSS only; imported cases never execute scripts.
2
+ export async function runAssertion(page, assertion, step, isStopped = () => false) {
3
+ const started = Date.now();
4
+ let actual = '尚未采样';
5
+ let passed = false;
6
+ let stableSince;
7
+ const names = {
8
+ contains: '包含',
9
+ 'not-contains': '不包含',
10
+ equals: '等于',
11
+ 'not-equals': '不等于',
12
+ visible: '可见',
13
+ hidden: '不可见或不存在',
14
+ };
15
+ while (Date.now() - started < assertion.timeout) {
16
+ if (isStopped())
17
+ throw Error('已停止');
18
+ const sample = await page.evaluate((a) => {
19
+ const normalize = (text) => String(text ?? '')
20
+ .replace(/\s+/g, ' ')
21
+ .trim();
22
+ const secret = (element) => /password|passwd|token|secret|credential|验证码|密码|密钥|口令/i.test([
23
+ element.type,
24
+ element.name,
25
+ element.id,
26
+ element.autocomplete,
27
+ element.getAttribute('aria-label'),
28
+ element.getAttribute('placeholder'),
29
+ element.labels?.[0]?.textContent,
30
+ ].join(' '));
31
+ let value;
32
+ if (a.source === 'page-text')
33
+ value = document.body.innerText;
34
+ else {
35
+ let elements;
36
+ try {
37
+ elements = [...document.querySelectorAll(a.selector)].filter((e) => !e.closest('[data-research-assistant],.research-assistant'));
38
+ }
39
+ catch {
40
+ return { fatal: 'CSS 定位语法无效' };
41
+ }
42
+ if (a.source === 'element-count')
43
+ value = String(elements.length);
44
+ else if (!elements.length && a.source === 'element-state' && a.operator === 'hidden')
45
+ value = 'hidden';
46
+ else if (elements.length !== 1)
47
+ return { actual: `匹配到 ${elements.length} 个元素,要求唯一元素`, matched: false };
48
+ else {
49
+ const element = elements[0];
50
+ if (a.source === 'element-state') {
51
+ const rect = element.getBoundingClientRect();
52
+ const style = getComputedStyle(element);
53
+ value =
54
+ rect.width > 0 &&
55
+ rect.height > 0 &&
56
+ style.visibility !== 'hidden' &&
57
+ style.visibility !== 'collapse'
58
+ ? 'visible'
59
+ : 'hidden';
60
+ }
61
+ else if (a.source === 'element-value') {
62
+ if (secret(element))
63
+ return { fatal: '不读取敏感字段的值' };
64
+ if (!element.matches('input,textarea,select'))
65
+ return { fatal: '输入框值断言只支持 input、textarea、select' };
66
+ value = element.value;
67
+ }
68
+ else
69
+ value = element.innerText ?? element.textContent;
70
+ }
71
+ }
72
+ // Text follows rendered whitespace; input values stay exact, including spaces.
73
+ const expected = a.source === 'element-value' ? (a.expected ?? '') : normalize(a.expected);
74
+ const current = a.source === 'element-value' ? String(value) : normalize(value);
75
+ const equal = a.source === 'element-count' ? Number(current) === Number(expected) : current === expected;
76
+ const matched = a.operator === 'contains'
77
+ ? current.includes(expected)
78
+ : a.operator === 'not-contains'
79
+ ? !current.includes(expected)
80
+ : a.operator === 'equals'
81
+ ? equal
82
+ : a.operator === 'not-equals'
83
+ ? !equal
84
+ : current === a.operator;
85
+ return { actual: current.slice(0, 500), matched };
86
+ }, assertion);
87
+ actual = sample.fatal ?? sample.actual ?? '';
88
+ if (sample.fatal)
89
+ break;
90
+ if (sample.matched) {
91
+ stableSince ??= Date.now();
92
+ // Avoid accepting a transient match while a post-action view is settling.
93
+ if (Date.now() - stableSince >= 200) {
94
+ passed = true;
95
+ break;
96
+ }
97
+ }
98
+ else
99
+ stableSince = undefined;
100
+ await page.waitForTimeout(Math.min(100, Math.max(1, assertion.timeout - (Date.now() - started))));
101
+ }
102
+ return {
103
+ step,
104
+ name: assertion.name,
105
+ passed,
106
+ expected: `${names[assertion.operator]}${assertion.expected === undefined ? '' : ` ${assertion.expected === '' ? '空字符串' : assertion.expected}`}`,
107
+ actual: actual === 'visible' ? '可见' : actual === 'hidden' ? '不可见或不存在' : actual,
108
+ duration: Date.now() - started,
109
+ };
110
+ }
111
+ //# sourceMappingURL=assertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertions.js","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type LaunchOptions, type BrowserContextOptions } from 'playwright';
2
+ export declare function openBrowser(options?: {
3
+ launch?: LaunchOptions;
4
+ context?: BrowserContextOptions;
5
+ }): Promise<{
6
+ browser: import("playwright").Browser;
7
+ context: import("playwright").BrowserContext;
8
+ page: import("playwright").Page;
9
+ }>;
@@ -0,0 +1,20 @@
1
+ // Adapted from anthonyli/laya-pilot lib/browser-provider.mjs (MIT).
2
+ import { chromium } from 'playwright';
3
+ export async function openBrowser(options = {}) {
4
+ const browser = await chromium.launch(options.launch);
5
+ try {
6
+ const context = await browser.newContext({
7
+ viewport: { width: 1500, height: 980 },
8
+ locale: 'zh-CN',
9
+ ...options.context,
10
+ });
11
+ const page = await context.newPage();
12
+ page.setDefaultTimeout(10000);
13
+ return { browser, context, page };
14
+ }
15
+ catch (error) {
16
+ await browser.close();
17
+ throw error;
18
+ }
19
+ }
20
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,OAAO,EAAE,QAAQ,EAAkD,MAAM,YAAY,CAAC;AACtF,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAAuE,EAAE;IAEzE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;YACtC,MAAM,EAAE,OAAO;YACf,GAAG,OAAO,CAAC,OAAO;SACnB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ export interface Decision {
2
+ choice: string;
3
+ probabilities: Record<string, number>;
4
+ confidence: number | null;
5
+ }
6
+ export type Decide = (state: string, criteria: Record<string, string>) => Promise<Decision>;
7
+ export declare function decodeDecision(body: unknown, criteria: Record<string, string>): Decision;
8
+ export declare function createApiDecision(options: {
9
+ base: string;
10
+ model: string;
11
+ key: string;
12
+ timeout?: number;
13
+ fetchImpl?: typeof fetch;
14
+ }): Decide;
@@ -0,0 +1,65 @@
1
+ export function decodeDecision(body, criteria) {
2
+ const envelope = body;
3
+ let result;
4
+ try {
5
+ result = JSON.parse(envelope.choices[0].message.content).result.answers.scenario;
6
+ }
7
+ catch {
8
+ throw Error('模型没有返回有效的 Laya 决策 JSON');
9
+ }
10
+ const keys = Object.keys(criteria);
11
+ if (!result ||
12
+ !Object.hasOwn(criteria, result.choice) ||
13
+ !result.probabilities ||
14
+ Object.keys(result.probabilities).length !== keys.length ||
15
+ keys.some((key) => !Number.isFinite(result.probabilities[key]) ||
16
+ result.probabilities[key] < 0 ||
17
+ result.probabilities[key] > 1))
18
+ throw Error('模型返回了候选集合之外的选择或无效概率');
19
+ if (Math.abs(keys.reduce((sum, key) => sum + result.probabilities[key], 0) - 1) > 0.02 ||
20
+ keys.some((key) => result.probabilities[key] > result.probabilities[result.choice]))
21
+ throw Error('模型概率与选择不一致');
22
+ return {
23
+ choice: result.choice,
24
+ probabilities: result.probabilities,
25
+ confidence: Number.isFinite(result.confidence) ? result.confidence : null,
26
+ };
27
+ }
28
+ export function createApiDecision(options) {
29
+ const url = new URL(options.base);
30
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash)
31
+ throw Error('API 地址必须是无凭据及查询参数的 HTTPS 地址');
32
+ if (!options.key.trim())
33
+ throw Error('缺少模型 API 密钥');
34
+ return async (state, criteria) => {
35
+ const response = await (options.fetchImpl ?? fetch)(url.href.replace(/\/$/, '') + '/chat/completions', {
36
+ method: 'POST',
37
+ redirect: 'error',
38
+ signal: AbortSignal.timeout(options.timeout ?? 30000),
39
+ headers: { Authorization: `Bearer ${options.key}`, 'Content-Type': 'application/json' },
40
+ body: JSON.stringify({
41
+ model: options.model,
42
+ stream: false,
43
+ messages: [
44
+ {
45
+ role: 'system',
46
+ content: '你是浏览器测试决策器。只返回 JSON:{"result":{"answers":{"scenario":{"choice":"候选原文","probabilities":{"每个候选":0.0},"confidence":0.0}}}}。choice 必须属于 criteria,概率覆盖全部候选且和为1,所选概率最大。证据不足选择停止,不编造确定性。',
47
+ },
48
+ {
49
+ role: 'user',
50
+ content: JSON.stringify({
51
+ state,
52
+ questions: {
53
+ scenario: { type: 'choice', instructions: '用户请求对应哪个操作?', criteria },
54
+ },
55
+ }),
56
+ },
57
+ ],
58
+ }),
59
+ });
60
+ if (!response.ok)
61
+ throw Error(`模型 API 返回 HTTP ${response.status}`);
62
+ return decodeDecision(await response.json(), criteria);
63
+ };
64
+ }
65
+ //# sourceMappingURL=decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.js","sourceRoot":"","sources":["../src/decision.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export * from './schema.js';
2
+ export { runCase, type ReplayOptions } from './replay.js';
3
+ export { runAssertion } from './assertions.js';
4
+ export { openBrowser } from './browser.js';
5
+ export { waitForReady } from './readiness.js';
6
+ export { createApiDecision, decodeDecision, type Decision, type Decide } from './decision.js';
7
+ export { chooseStep } from './planner.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './schema.js';
2
+ export { runCase } from './replay.js';
3
+ export { runAssertion } from './assertions.js';
4
+ export { openBrowser } from './browser.js';
5
+ export { waitForReady } from './readiness.js';
6
+ export { createApiDecision, decodeDecision } from './decision.js';
7
+ export { chooseStep } from './planner.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAsB,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAA8B,MAAM,eAAe,CAAC;AAC9F,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Decide } from './decision.js';
2
+ import type { Step } from './schema.js';
3
+ /** A model selects from observed, host-provided candidates. It never generates executable code. */
4
+ export declare function chooseStep(instruction: string, candidates: {
5
+ description: string;
6
+ step: Step;
7
+ }[], decide: Decide): Promise<Step | null>;
@@ -0,0 +1,19 @@
1
+ /** A model selects from observed, host-provided candidates. It never generates executable code. */
2
+ export async function chooseStep(instruction, candidates, decide) {
3
+ if (candidates.length < 1 || candidates.length > 7)
4
+ throw Error('每次提供 1–7 个可观察的候选操作');
5
+ const criteria = Object.fromEntries(candidates.map((candidate, index) => [String(index), candidate.description]));
6
+ criteria.stop = '无法判断,停止并由用户确认';
7
+ const result = await decide(instruction, criteria);
8
+ if (result.choice === 'stop')
9
+ return null;
10
+ const index = Number(result.choice);
11
+ if (!Number.isInteger(index) || !Object.hasOwn(criteria, result.choice) || !candidates[index])
12
+ throw Error('模型选择不属于候选集');
13
+ const probabilities = Object.values(result.probabilities).sort((a, b) => b - a);
14
+ if ((result.probabilities[result.choice] ?? 0) < 0.7 ||
15
+ probabilities[0] - (probabilities[1] ?? 0) < 0.15)
16
+ return null;
17
+ return structuredClone(candidates[index].step);
18
+ }
19
+ //# sourceMappingURL=planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.js","sourceRoot":"","sources":["../src/planner.ts"],"names":[],"mappings":"AAGA,mGAAmG;AACnG,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,WAAmB,EACnB,UAAiD,EACjD,MAAc;IAEd,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAChD,MAAM,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CACjC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAC7E,CAAC;IACF,QAAQ,CAAC,IAAI,GAAG,eAAe,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACnD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAC3F,MAAM,KAAK,CAAC,YAAY,CAAC,CAAC;IAC5B,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChF,IACE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG;QAChD,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI;QAEjD,OAAO,IAAI,CAAC;IACd,OAAO,eAAe,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { Page } from 'playwright';
2
+ type Failure = {
3
+ status: number;
4
+ path: string;
5
+ resourceType: string;
6
+ };
7
+ type ReadyOptions = {
8
+ timeout?: number;
9
+ minimum?: number;
10
+ failures?: () => Failure[];
11
+ };
12
+ export declare function waitForReady(page: Page, { timeout, minimum, failures }?: ReadyOptions): Promise<{
13
+ duration_ms: number;
14
+ controls: number;
15
+ }>;
16
+ export {};
@@ -0,0 +1,65 @@
1
+ // Wait for the application, not merely the outer document's DOMContentLoaded.
2
+ export async function waitForReady(page, { timeout = 18000, minimum = 600, failures = () => [] } = {}) {
3
+ const start = Date.now();
4
+ let stableSince = Date.now(), previous = '', last;
5
+ while (Date.now() - start < timeout) {
6
+ const errors = failures().filter((x) => x.status >= 500 && (/\.html?$|\.js$|\.css$/i.test(x.path) || x.resourceType === 'document'));
7
+ const states = [];
8
+ for (const frame of page.frames())
9
+ try {
10
+ states.push(await frame.evaluate(() => {
11
+ const visible = (e) => {
12
+ const r = e.getBoundingClientRect(), s = getComputedStyle(e);
13
+ return (r.width > 0 &&
14
+ r.height > 0 &&
15
+ s.display !== 'none' &&
16
+ s.visibility !== 'hidden' &&
17
+ !e.closest('[aria-hidden="true"],[inert]'));
18
+ };
19
+ const body = document.body;
20
+ if (!body)
21
+ return { controls: 0, busy: true, text: '' };
22
+ const text = body.innerText.trim(), controls = [
23
+ ...document.querySelectorAll('button,input:not([type=hidden]),select,textarea,a[href],[role=tab],[role=combobox],[role=menuitem]'),
24
+ ].filter(visible);
25
+ const loading = [
26
+ ...document.querySelectorAll('[aria-busy=true],.ant-spin-spinning,.el-loading-mask,[role=progressbar]'),
27
+ ].some(visible) ||
28
+ text
29
+ .split('\n')
30
+ .some((x) => /^(?:loading\.{0,3}|加载中[.。…]*|正在加载[.。…]*)$/i.test(x.trim()));
31
+ return {
32
+ controls: controls.length,
33
+ busy: loading,
34
+ text: text.slice(0, 240),
35
+ signature: controls
36
+ .slice(0, 60)
37
+ .map((x) => [
38
+ x.getAttribute('role'),
39
+ x.tagName,
40
+ x.getAttribute('placeholder'),
41
+ x.getAttribute('aria-label'),
42
+ x.textContent?.trim().slice(0, 25),
43
+ ].join(':'))
44
+ .join('|'),
45
+ };
46
+ }));
47
+ }
48
+ catch { /* A frame can detach while the page is settling. */ }
49
+ const count = states.reduce((n, s) => n + s.controls, 0), busy = states.some((s) => s.busy), fingerprint = states.map((s) => s.signature || '').join('|');
50
+ last = { count, busy };
51
+ // A shell may preload unrelated microfrontends. Their errors must not block
52
+ // an already usable page; only correlate critical errors with an unready UI.
53
+ if (errors.length && (busy || count === 0))
54
+ throw new Error(errors.map((x) => x.path + ' HTTP ' + x.status).join(';'));
55
+ if (fingerprint !== previous || busy || count === 0) {
56
+ previous = fingerprint;
57
+ stableSince = Date.now();
58
+ }
59
+ if (count > 0 && !busy && Date.now() - stableSince >= minimum)
60
+ return { duration_ms: Date.now() - start, controls: count };
61
+ await page.waitForTimeout(150);
62
+ }
63
+ throw new Error(`等待${timeout}ms后页面仍为空白或加载中(控件${last?.count || 0},loading=${last?.busy}),停止本次会话,不继续刷新后续用例`);
64
+ }
65
+ //# sourceMappingURL=readiness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.js","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Page, BrowserContext, LaunchOptions, BrowserContextOptions } from 'playwright';
2
+ import { type RunStatus } from './schema.js';
3
+ export interface ReplayOptions {
4
+ origin: string;
5
+ allowedOrigins?: string[];
6
+ launch?: LaunchOptions;
7
+ context?: BrowserContextOptions;
8
+ delay?: number;
9
+ signal?: AbortSignal;
10
+ /** Host-owned login/setup, never serialized into a case or exposed to the browser. */
11
+ beforeRun?: (page: Page, context: BrowserContext) => Promise<void>;
12
+ onProgress?: (status: RunStatus) => void;
13
+ }
14
+ /** Functions compose the execution path. No business route, login or data fixture is built in. */
15
+ export declare function runCase(value: unknown, options: ReplayOptions): Promise<RunStatus>;