@ggui-ai/ui-visual-tester 0.1.0-rc.1
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 +201 -0
- package/README.md +58 -0
- package/dist/fixture-runtime.d.ts +84 -0
- package/dist/fixture-runtime.d.ts.map +1 -0
- package/dist/fixture-runtime.js +280 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/validate.d.ts +31 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +195 -0
- package/fixture/fixture-runtime.js +50833 -0
- package/package.json +75 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface for `@ggui-ai/ui-visual-tester`.
|
|
3
|
+
*
|
|
4
|
+
* One function — {@link validateContractBehavior} — drives a generated
|
|
5
|
+
* component through every entry in its `actionSpec`, observes whether
|
|
6
|
+
* each click actually does something (mutates DOM that is bound to
|
|
7
|
+
* `contextSpec` state OR dispatches an action), and reports per-action
|
|
8
|
+
* failures.
|
|
9
|
+
*
|
|
10
|
+
* Closes the gap structural validators (zod schema, wire-preservation,
|
|
11
|
+
* compile/lint) miss: a component whose buttons render correctly but
|
|
12
|
+
* are wired to no-op handlers. The motivating regression: synthesized
|
|
13
|
+
* Counter contracts that wired increment/decrement to `useAction(...)`
|
|
14
|
+
* (one-shot dispatch to agent) instead of local state, so the visible
|
|
15
|
+
* `count` never updated.
|
|
16
|
+
*
|
|
17
|
+
* Playwright is an OPTIONAL peer dep. Callers must pass the
|
|
18
|
+
* `playwright-core` module (or compatible) via factory dep injection:
|
|
19
|
+
*
|
|
20
|
+
* import { chromium } from 'playwright-core';
|
|
21
|
+
* const result = await validateContractBehavior({
|
|
22
|
+
* componentCode, contract,
|
|
23
|
+
* playwright: { chromium },
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* If `playwright` is omitted, the validator throws a clear error
|
|
27
|
+
* pointing the operator at the advanced-pod opt-in. This keeps the
|
|
28
|
+
* default OSS install free of the Chromium binary (~300MB).
|
|
29
|
+
*
|
|
30
|
+
* Classification gate:
|
|
31
|
+
*
|
|
32
|
+
* For each `actionSpec[name]`:
|
|
33
|
+
* - If the action's handler mutates a `contextSpec` slot (detected by
|
|
34
|
+
* `nextStep` ABSENT on the entry — the action is "context-bound"),
|
|
35
|
+
* require a DOM change as the success signal.
|
|
36
|
+
* - If the action's handler dispatches to the agent (detected by
|
|
37
|
+
* `nextStep` PRESENT on the entry — the action is "agent-bound"),
|
|
38
|
+
* require a `dispatch(...)` call as the success signal.
|
|
39
|
+
*
|
|
40
|
+
* When neither holds (label-only entry, no `nextStep`), accept EITHER
|
|
41
|
+
* signal — DOM mutation OR dispatch — as success. This is the safest
|
|
42
|
+
* default for actions whose authoring intent is ambiguous from the
|
|
43
|
+
* contract alone.
|
|
44
|
+
*
|
|
45
|
+
* This aligns with the actions-vs-context principle: `nextStep`
|
|
46
|
+
* declares the agent-route intent at authoring time; without it,
|
|
47
|
+
* the action is observation-only or local-only.
|
|
48
|
+
*
|
|
49
|
+
* Behavior
|
|
50
|
+
* --------
|
|
51
|
+
*
|
|
52
|
+
* For each entry in `contract.actionSpec`:
|
|
53
|
+
*
|
|
54
|
+
* 1. Render the component in a real Chromium tab via Playwright.
|
|
55
|
+
* 2. Find a button matching the action's `label` (aria-label first,
|
|
56
|
+
* then visible-text contains, case-insensitive).
|
|
57
|
+
* 3. Snapshot the current DOM state.
|
|
58
|
+
* 4. Click the button.
|
|
59
|
+
* 5. Wait up to `timeoutMs` for the required signal (per the gate
|
|
60
|
+
* above) to fire.
|
|
61
|
+
* 6. Classify: ok / action-no-effect / action-not-rendered /
|
|
62
|
+
* render-failed.
|
|
63
|
+
*
|
|
64
|
+
* Empty `actionSpec` returns `{ ok: true, failures: [] }` (not
|
|
65
|
+
* applicable — nothing to test).
|
|
66
|
+
*/
|
|
67
|
+
import type { DataContract } from '@ggui-ai/protocol';
|
|
68
|
+
import type { Browser as PlaywrightBrowser, BrowserContext as PlaywrightBrowserContext, Page as PlaywrightPage, LaunchOptions as PlaywrightLaunchOptions } from 'playwright-core';
|
|
69
|
+
/**
|
|
70
|
+
* Minimal Playwright `chromium` surface the validator depends on.
|
|
71
|
+
*
|
|
72
|
+
* The full `playwright-core` shape is intentionally NOT imported here
|
|
73
|
+
* — we only require `chromium.launch(...)`. Callers may pass any
|
|
74
|
+
* module satisfying this shape (real Playwright, a stub for tests,
|
|
75
|
+
* `playwright` instead of `playwright-core`).
|
|
76
|
+
*/
|
|
77
|
+
export interface PlaywrightModule {
|
|
78
|
+
readonly chromium: {
|
|
79
|
+
launch(options?: PlaywrightLaunchOptions): Promise<PlaywrightBrowser>;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Re-exported for callers building their own driver. */
|
|
83
|
+
export type { PlaywrightBrowser, PlaywrightBrowserContext, PlaywrightPage, };
|
|
84
|
+
export interface ValidateContractBehaviorInput {
|
|
85
|
+
readonly componentCode: string;
|
|
86
|
+
readonly contract: DataContract;
|
|
87
|
+
readonly timeoutMs?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Playwright module. Pass `{ chromium }` from `playwright-core` or
|
|
90
|
+
* `playwright`. Required — when absent the validator throws with a
|
|
91
|
+
* pointer to the advanced-pod opt-in.
|
|
92
|
+
*/
|
|
93
|
+
readonly playwright: PlaywrightModule;
|
|
94
|
+
}
|
|
95
|
+
export type BehaviorFailureKind = 'action-no-effect' | 'action-not-rendered' | 'render-failed' | 'timeout';
|
|
96
|
+
export interface BehaviorFailure {
|
|
97
|
+
readonly kind: BehaviorFailureKind;
|
|
98
|
+
readonly actionName?: string;
|
|
99
|
+
readonly diagnostic: string;
|
|
100
|
+
}
|
|
101
|
+
export interface ValidateContractBehaviorResult {
|
|
102
|
+
readonly ok: boolean;
|
|
103
|
+
readonly failures: readonly BehaviorFailure[];
|
|
104
|
+
}
|
|
105
|
+
export { validateContractBehavior } from './validate.js';
|
|
106
|
+
/**
|
|
107
|
+
* Error thrown when {@link validateContractBehavior} is called without
|
|
108
|
+
* a `playwright` module. Surfaced verbatim so operator scripts can
|
|
109
|
+
* branch on it.
|
|
110
|
+
*/
|
|
111
|
+
export declare class PlaywrightNotAvailableError extends Error {
|
|
112
|
+
readonly name = "PlaywrightNotAvailableError";
|
|
113
|
+
constructor();
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EACV,OAAO,IAAI,iBAAiB,EAC5B,cAAc,IAAI,wBAAwB,EAC1C,IAAI,IAAI,cAAc,EACtB,aAAa,IAAI,uBAAuB,EACzC,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE;QACjB,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;KACvE,CAAC;CACH;AAED,yDAAyD;AACzD,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,GACf,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;CACvC;AAED,MAAM,MAAM,mBAAmB,GAC3B,kBAAkB,GAClB,qBAAqB,GACrB,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAED,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,SAAkB,IAAI,iCAAiC;;CASxD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { validateContractBehavior } from './validate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when {@link validateContractBehavior} is called without
|
|
4
|
+
* a `playwright` module. Surfaced verbatim so operator scripts can
|
|
5
|
+
* branch on it.
|
|
6
|
+
*/
|
|
7
|
+
export class PlaywrightNotAvailableError extends Error {
|
|
8
|
+
name = 'PlaywrightNotAvailableError';
|
|
9
|
+
constructor() {
|
|
10
|
+
super('@ggui-ai/ui-visual-tester: a Playwright module is required but was not passed. ' +
|
|
11
|
+
'Behavioral validation needs a Playwright module + Chromium binary. ' +
|
|
12
|
+
'Pass `{ chromium }` from `playwright-core` via the `playwright` field on the input. ' +
|
|
13
|
+
'Playwright is an optional peer dependency — install it only if you run behavioral validation.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DataContract } from '@ggui-ai/protocol';
|
|
2
|
+
import type { ValidateContractBehaviorInput, ValidateContractBehaviorResult } from './index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Per-action signal-classification, derived from the contract via
|
|
5
|
+
* Option C (the actions-vs-context principle).
|
|
6
|
+
*
|
|
7
|
+
* - `agent-bound` — `actionSpec[name].nextStep` is present. The
|
|
8
|
+
* author has hinted that the agent should pick up
|
|
9
|
+
* this action; success ⇔ a dispatch fired.
|
|
10
|
+
* - `context-bound` — `nextStep` is absent. The action is observed
|
|
11
|
+
* locally (the rest of the page reads via
|
|
12
|
+
* `contextSpec`/local state) without agent routing;
|
|
13
|
+
* success ⇔ DOM changed.
|
|
14
|
+
*
|
|
15
|
+
* The classification is computed up-front in Node and forwarded into
|
|
16
|
+
* the page so the fixture's branch is deterministic.
|
|
17
|
+
*/
|
|
18
|
+
export type ActionClassification = 'agent-bound' | 'context-bound';
|
|
19
|
+
/**
|
|
20
|
+
* Per Option C: classify each action by the contract shape alone.
|
|
21
|
+
*
|
|
22
|
+
* `nextStep` present → agent-bound (dispatch required)
|
|
23
|
+
* `nextStep` absent → context-bound (DOM change required)
|
|
24
|
+
*
|
|
25
|
+
* The classification is deterministic and fully contract-derivable;
|
|
26
|
+
* no source-AST inspection, no LLM call. When a contract evolves to
|
|
27
|
+
* include `nextStep` mid-flight, the classification follows.
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyAction(contract: DataContract, actionName: string): ActionClassification;
|
|
30
|
+
export declare function validateContractBehavior(input: ValidateContractBehaviorInput): Promise<ValidateContractBehaviorResult>;
|
|
31
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAIV,6BAA6B,EAC7B,8BAA8B,EAC/B,MAAM,YAAY,CAAC;AAYpB;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,eAAe,CAAC;AAqKnE;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,GACjB,oBAAoB,CAMtB;AAeD,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,8BAA8B,CAAC,CAsCzC"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-side Playwright driver for `validateContractBehavior`.
|
|
3
|
+
*
|
|
4
|
+
* Spins up one headless Chromium browser per call, then iterates each
|
|
5
|
+
* `actionSpec` entry: one fresh `BrowserContext` + `Page` per action
|
|
6
|
+
* for isolation. Inside the page, the IIFE-bundled fixture runtime
|
|
7
|
+
* (built by `scripts/build-fixture-bundle.mjs` → `fixture/fixture-runtime.js`)
|
|
8
|
+
* mounts the component and runs the click-and-observe loop.
|
|
9
|
+
*
|
|
10
|
+
* The fixture is loaded via `addScriptTag({content: ...})` rather than
|
|
11
|
+
* a file URL — works around CORS in headless launch, lets us cache
|
|
12
|
+
* the bundle source on the Node side.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { PlaywrightNotAvailableError } from './index.js';
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
19
|
+
const SETTLE_MS = 200;
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const BUNDLE_PATH = resolve(__dirname, '../fixture/fixture-runtime.js');
|
|
22
|
+
let bundleSourceCache = null;
|
|
23
|
+
async function loadBundleSource() {
|
|
24
|
+
if (bundleSourceCache !== null)
|
|
25
|
+
return bundleSourceCache;
|
|
26
|
+
const src = await readFile(BUNDLE_PATH, 'utf8');
|
|
27
|
+
bundleSourceCache = src;
|
|
28
|
+
return src;
|
|
29
|
+
}
|
|
30
|
+
const FIXTURE_HTML = `<!doctype html>
|
|
31
|
+
<html><head><meta charset="utf-8"><title>ggui-vt</title></head>
|
|
32
|
+
<body><div id="root"></div></body></html>`;
|
|
33
|
+
async function preparePage(browser) {
|
|
34
|
+
const ctx = await browser.newContext();
|
|
35
|
+
const page = await ctx.newPage();
|
|
36
|
+
await page.setContent(FIXTURE_HTML, { waitUntil: 'load' });
|
|
37
|
+
const bundle = await loadBundleSource();
|
|
38
|
+
await page.addScriptTag({ content: bundle });
|
|
39
|
+
await page.waitForFunction(() => typeof window
|
|
40
|
+
.__validateContractBehavior_run__ === 'function', undefined, { timeout: 10_000 });
|
|
41
|
+
return page;
|
|
42
|
+
}
|
|
43
|
+
function parseRunOutcome(value) {
|
|
44
|
+
if (value === null ||
|
|
45
|
+
typeof value !== 'object' ||
|
|
46
|
+
typeof value.status !== 'string') {
|
|
47
|
+
return {
|
|
48
|
+
status: 'render-failed',
|
|
49
|
+
diagnostic: `fixture returned unparseable result: ${JSON.stringify(value)}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const status = value.status;
|
|
53
|
+
switch (status) {
|
|
54
|
+
case 'render-failed': {
|
|
55
|
+
const diagnostic = value.diagnostic;
|
|
56
|
+
return {
|
|
57
|
+
status: 'render-failed',
|
|
58
|
+
diagnostic: typeof diagnostic === 'string' ? diagnostic : 'unknown',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
case 'action-not-rendered':
|
|
62
|
+
return { status: 'action-not-rendered' };
|
|
63
|
+
case 'action-no-effect': {
|
|
64
|
+
const diagnostic = value.diagnostic;
|
|
65
|
+
return {
|
|
66
|
+
status: 'action-no-effect',
|
|
67
|
+
diagnostic: typeof diagnostic === 'string' ? diagnostic : 'no signal',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case 'ok': {
|
|
71
|
+
const dispatchFired = value.dispatchFired === true;
|
|
72
|
+
const domChanged = value.domChanged === true;
|
|
73
|
+
return { status: 'ok', dispatchFired, domChanged };
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
return {
|
|
77
|
+
status: 'render-failed',
|
|
78
|
+
diagnostic: `fixture returned unknown status: ${status}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function runOne(page, input) {
|
|
83
|
+
const serialized = {
|
|
84
|
+
componentCode: input.componentCode,
|
|
85
|
+
contractJson: JSON.stringify(input.contract),
|
|
86
|
+
actionName: input.actionName,
|
|
87
|
+
classification: input.classification,
|
|
88
|
+
settleMs: input.settleMs,
|
|
89
|
+
waitMs: input.waitMs,
|
|
90
|
+
};
|
|
91
|
+
const result = await page.evaluate(async (arg) => {
|
|
92
|
+
const fn = window.__validateContractBehavior_run__;
|
|
93
|
+
if (fn === undefined) {
|
|
94
|
+
return { status: 'render-failed', diagnostic: 'fixture runtime not loaded' };
|
|
95
|
+
}
|
|
96
|
+
return await fn({
|
|
97
|
+
componentCode: arg.componentCode,
|
|
98
|
+
contract: JSON.parse(arg.contractJson),
|
|
99
|
+
actionName: arg.actionName,
|
|
100
|
+
classification: arg.classification,
|
|
101
|
+
settleMs: arg.settleMs,
|
|
102
|
+
waitMs: arg.waitMs,
|
|
103
|
+
});
|
|
104
|
+
}, serialized);
|
|
105
|
+
return parseRunOutcome(result);
|
|
106
|
+
}
|
|
107
|
+
function classify(actionName, outcome) {
|
|
108
|
+
switch (outcome.status) {
|
|
109
|
+
case 'ok':
|
|
110
|
+
return null;
|
|
111
|
+
case 'render-failed':
|
|
112
|
+
return {
|
|
113
|
+
kind: 'render-failed',
|
|
114
|
+
actionName,
|
|
115
|
+
diagnostic: outcome.diagnostic,
|
|
116
|
+
};
|
|
117
|
+
case 'action-not-rendered':
|
|
118
|
+
return {
|
|
119
|
+
kind: 'action-not-rendered',
|
|
120
|
+
actionName,
|
|
121
|
+
diagnostic: `no button found matching actionSpec.${actionName}.label`,
|
|
122
|
+
};
|
|
123
|
+
case 'action-no-effect':
|
|
124
|
+
return {
|
|
125
|
+
kind: 'action-no-effect',
|
|
126
|
+
actionName,
|
|
127
|
+
diagnostic: `clicking actionSpec.${actionName} produced no required signal — ` +
|
|
128
|
+
outcome.diagnostic,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Per Option C: classify each action by the contract shape alone.
|
|
134
|
+
*
|
|
135
|
+
* `nextStep` present → agent-bound (dispatch required)
|
|
136
|
+
* `nextStep` absent → context-bound (DOM change required)
|
|
137
|
+
*
|
|
138
|
+
* The classification is deterministic and fully contract-derivable;
|
|
139
|
+
* no source-AST inspection, no LLM call. When a contract evolves to
|
|
140
|
+
* include `nextStep` mid-flight, the classification follows.
|
|
141
|
+
*/
|
|
142
|
+
export function classifyAction(contract, actionName) {
|
|
143
|
+
const entry = contract.actionSpec?.[actionName];
|
|
144
|
+
if (entry && typeof entry.nextStep === 'string' && entry.nextStep.length > 0) {
|
|
145
|
+
return 'agent-bound';
|
|
146
|
+
}
|
|
147
|
+
return 'context-bound';
|
|
148
|
+
}
|
|
149
|
+
function assertPlaywright(playwright) {
|
|
150
|
+
if (playwright === undefined ||
|
|
151
|
+
playwright === null ||
|
|
152
|
+
typeof playwright !== 'object' ||
|
|
153
|
+
typeof playwright.chromium !== 'object') {
|
|
154
|
+
throw new PlaywrightNotAvailableError();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function validateContractBehavior(input) {
|
|
158
|
+
const actionSpec = input.contract.actionSpec;
|
|
159
|
+
const actionNames = actionSpec ? Object.keys(actionSpec) : [];
|
|
160
|
+
if (actionNames.length === 0) {
|
|
161
|
+
return { ok: true, failures: [] };
|
|
162
|
+
}
|
|
163
|
+
assertPlaywright(input.playwright);
|
|
164
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
165
|
+
const failures = [];
|
|
166
|
+
let browser = null;
|
|
167
|
+
try {
|
|
168
|
+
browser = await input.playwright.chromium.launch({ headless: true });
|
|
169
|
+
for (const name of actionNames) {
|
|
170
|
+
const page = await preparePage(browser);
|
|
171
|
+
try {
|
|
172
|
+
const classification = classifyAction(input.contract, name);
|
|
173
|
+
const outcome = await runOne(page, {
|
|
174
|
+
componentCode: input.componentCode,
|
|
175
|
+
contract: input.contract,
|
|
176
|
+
actionName: name,
|
|
177
|
+
classification,
|
|
178
|
+
settleMs: SETTLE_MS,
|
|
179
|
+
waitMs: timeoutMs,
|
|
180
|
+
});
|
|
181
|
+
const f = classify(name, outcome);
|
|
182
|
+
if (f !== null)
|
|
183
|
+
failures.push(f);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await page.context().close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (browser !== null)
|
|
192
|
+
await browser.close();
|
|
193
|
+
}
|
|
194
|
+
return { ok: failures.length === 0, failures };
|
|
195
|
+
}
|