cssprobe-cli 0.3.1 → 0.4.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/AGENTS.md +35 -7
- package/CHANGELOG.md +21 -0
- package/README.md +54 -4
- package/dist/browser/frames.d.ts +15 -0
- package/dist/browser/frames.js +60 -0
- package/dist/browser/interactions.d.ts +1 -0
- package/dist/browser/interactions.js +9 -1
- package/dist/browser/launcher.d.ts +30 -2
- package/dist/browser/launcher.js +141 -34
- package/dist/browser/sampler.d.ts +43 -0
- package/dist/browser/sampler.js +111 -0
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.js +92 -5
- package/dist/cli/input.d.ts +6 -0
- package/dist/cli/input.js +24 -0
- package/dist/cli/program.js +152 -1
- package/dist/cli/session-ops.d.ts +8 -0
- package/dist/cli/session-ops.js +24 -1
- package/dist/cli/state-check.d.ts +17 -0
- package/dist/cli/state-check.js +43 -0
- package/dist/cli/url-guard.d.ts +2 -0
- package/dist/cli/url-guard.js +24 -0
- package/dist/daemon/daemon.js +67 -19
- package/dist/daemon/expect.d.ts +8 -0
- package/dist/daemon/expect.js +47 -0
- package/dist/daemon/flowRunner.d.ts +12 -1
- package/dist/daemon/flowRunner.js +22 -0
- package/dist/engine/evalSerializer.js +2 -2
- package/dist/help.json +94 -16
- package/dist/scenario/assertions.d.ts +13 -0
- package/dist/scenario/assertions.js +111 -0
- package/dist/scenario/reporters.d.ts +13 -0
- package/dist/scenario/reporters.js +79 -0
- package/dist/scenario/runner.d.ts +10 -0
- package/dist/scenario/runner.js +244 -0
- package/dist/scenario/schema.d.ts +1210 -0
- package/dist/scenario/schema.js +145 -0
- package/docs/guide/installation.md +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvePath = resolvePath;
|
|
4
|
+
exports.evaluateAssertion = evaluateAssertion;
|
|
5
|
+
function resolvePath(value, path) {
|
|
6
|
+
if (!path)
|
|
7
|
+
return value;
|
|
8
|
+
let current = value;
|
|
9
|
+
for (const part of path.split('.')) {
|
|
10
|
+
if (current === null || current === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
current = current[part];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function deepEqual(a, b) {
|
|
17
|
+
if (a === b)
|
|
18
|
+
return true;
|
|
19
|
+
if (typeof a !== typeof b)
|
|
20
|
+
return false;
|
|
21
|
+
if (a === null || b === null)
|
|
22
|
+
return false;
|
|
23
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
24
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
25
|
+
return false;
|
|
26
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
27
|
+
}
|
|
28
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
29
|
+
const aKeys = Object.keys(a).sort();
|
|
30
|
+
const bKeys = Object.keys(b).sort();
|
|
31
|
+
if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index]))
|
|
32
|
+
return false;
|
|
33
|
+
return aKeys.every(key => deepEqual(a[key], b[key]));
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function label(assertion) {
|
|
38
|
+
return assertion.label ?? 'assert';
|
|
39
|
+
}
|
|
40
|
+
function evaluateAssertion(assertion, context) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
if (assertion.url) {
|
|
43
|
+
let regex;
|
|
44
|
+
try {
|
|
45
|
+
regex = new RegExp(assertion.url);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return `${label(assertion)}: invalid url pattern ${assertion.url}`;
|
|
49
|
+
}
|
|
50
|
+
if (!regex.test(context.state.url)) {
|
|
51
|
+
errors.push(`url ${JSON.stringify(context.state.url)} does not match ${assertion.url}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [name, expected] of Object.entries(assertion.globals ?? {})) {
|
|
55
|
+
const actual = context.globals[name] ?? 'undefined';
|
|
56
|
+
if (actual !== expected) {
|
|
57
|
+
errors.push(`global ${name}: expected ${expected}, received ${actual}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const name of assertion.absent ?? []) {
|
|
61
|
+
const actual = context.globals[name] ?? 'undefined';
|
|
62
|
+
if (actual !== 'undefined') {
|
|
63
|
+
errors.push(`global ${name}: expected absent, received ${actual}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const hasValueCheck = ['eq', 'ne', 'match', 'within', 'min', 'max'].some(key => key in assertion);
|
|
67
|
+
if (!hasValueCheck) {
|
|
68
|
+
return errors.length ? `${label(assertion)}: ${errors.join('; ')}` : null;
|
|
69
|
+
}
|
|
70
|
+
let value;
|
|
71
|
+
if (assertion.on) {
|
|
72
|
+
if (assertion.on === 'state') {
|
|
73
|
+
value = context.state;
|
|
74
|
+
}
|
|
75
|
+
else if (assertion.on in context.vars) {
|
|
76
|
+
value = context.vars[assertion.on];
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return `${label(assertion)}: unknown variable "${assertion.on}"`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
value = context.state;
|
|
84
|
+
}
|
|
85
|
+
value = resolvePath(value, assertion.path);
|
|
86
|
+
if ('eq' in assertion && !deepEqual(value, assertion.eq)) {
|
|
87
|
+
errors.push(`expected ${JSON.stringify(assertion.eq)}, received ${JSON.stringify(value)}`);
|
|
88
|
+
}
|
|
89
|
+
if ('ne' in assertion && deepEqual(value, assertion.ne)) {
|
|
90
|
+
errors.push(`expected value different from ${JSON.stringify(assertion.ne)}`);
|
|
91
|
+
}
|
|
92
|
+
if (assertion.match !== undefined) {
|
|
93
|
+
const actual = typeof value === 'string' ? value : JSON.stringify(value);
|
|
94
|
+
if (!new RegExp(assertion.match).test(actual)) {
|
|
95
|
+
errors.push(`expected ${JSON.stringify(actual)} to match ${assertion.match}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (assertion.within) {
|
|
99
|
+
if (typeof value !== 'number' || Math.abs(value - assertion.within.expected) > assertion.within.tolerance) {
|
|
100
|
+
errors.push(`expected ${assertion.within.expected} ±${assertion.within.tolerance}, received ${JSON.stringify(value)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (assertion.min !== undefined && (typeof value !== 'number' || value < assertion.min)) {
|
|
104
|
+
errors.push(`expected >= ${assertion.min}, received ${JSON.stringify(value)}`);
|
|
105
|
+
}
|
|
106
|
+
if (assertion.max !== undefined && (typeof value !== 'number' || value > assertion.max)) {
|
|
107
|
+
errors.push(`expected <= ${assertion.max}, received ${JSON.stringify(value)}`);
|
|
108
|
+
}
|
|
109
|
+
return errors.length ? `${label(assertion)}: ${errors.join('; ')}` : null;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=assertions.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CaseResult } from './schema';
|
|
2
|
+
export declare function slugify(value: string): string;
|
|
3
|
+
export interface ScenarioSummary {
|
|
4
|
+
total: number;
|
|
5
|
+
passed: number;
|
|
6
|
+
failed: number;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
ok: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function summarize(results: CaseResult[]): ScenarioSummary;
|
|
11
|
+
export declare function formatConsole(suiteName: string, results: CaseResult[]): string;
|
|
12
|
+
export declare function escapeXml(value: string): string;
|
|
13
|
+
export declare function toJunit(suiteName: string, results: CaseResult[]): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.slugify = slugify;
|
|
4
|
+
exports.summarize = summarize;
|
|
5
|
+
exports.formatConsole = formatConsole;
|
|
6
|
+
exports.escapeXml = escapeXml;
|
|
7
|
+
exports.toJunit = toJunit;
|
|
8
|
+
function slugify(value) {
|
|
9
|
+
return value
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '')
|
|
13
|
+
.slice(0, 60) || 'case';
|
|
14
|
+
}
|
|
15
|
+
function summarize(results) {
|
|
16
|
+
const passed = results.filter(result => result.ok).length;
|
|
17
|
+
const durationMs = results.reduce((total, result) => total + result.durationMs, 0);
|
|
18
|
+
return {
|
|
19
|
+
total: results.length,
|
|
20
|
+
passed,
|
|
21
|
+
failed: results.length - passed,
|
|
22
|
+
durationMs,
|
|
23
|
+
ok: passed === results.length,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function formatConsole(suiteName, results) {
|
|
27
|
+
const summary = summarize(results);
|
|
28
|
+
const lines = [];
|
|
29
|
+
for (const result of results) {
|
|
30
|
+
const marker = result.ok ? 'PASS' : result.skipped ? 'SKIP' : 'FAIL';
|
|
31
|
+
lines.push(`${marker} ${result.name} (${result.durationMs}ms)`);
|
|
32
|
+
if (result.error)
|
|
33
|
+
lines.push(` ${result.error}`);
|
|
34
|
+
for (const step of result.steps) {
|
|
35
|
+
if (!step.ok)
|
|
36
|
+
lines.push(` step ${step.index + 1} (${step.action}): ${step.error}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.artifactsDir)
|
|
39
|
+
lines.push(` artifacts: ${result.artifactsDir}`);
|
|
40
|
+
}
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(`${suiteName}: ${summary.passed}/${summary.total} passed, ${summary.failed} failed in ${summary.durationMs}ms`);
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
function escapeXml(value) {
|
|
46
|
+
return value
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/"/g, '"')
|
|
51
|
+
.replace(/'/g, ''');
|
|
52
|
+
}
|
|
53
|
+
function toJunit(suiteName, results) {
|
|
54
|
+
const summary = summarize(results);
|
|
55
|
+
const cases = results
|
|
56
|
+
.map(result => {
|
|
57
|
+
const time = (result.durationMs / 1000).toFixed(3);
|
|
58
|
+
const failureDetails = [
|
|
59
|
+
result.error,
|
|
60
|
+
...result.steps.filter(step => !step.ok).map(step => `step ${step.index + 1} (${step.action}): ${step.error}`),
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join('\n');
|
|
64
|
+
const body = result.ok
|
|
65
|
+
? ''
|
|
66
|
+
: `<failure message="${escapeXml(result.error ?? 'scenario case failed')}">${escapeXml(failureDetails)}</failure>`;
|
|
67
|
+
return ` <testcase classname="${escapeXml(suiteName)}" name="${escapeXml(result.name)}" time="${time}">${body}</testcase>`;
|
|
68
|
+
})
|
|
69
|
+
.join('\n');
|
|
70
|
+
return [
|
|
71
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
72
|
+
`<testsuites tests="${summary.total}" failures="${summary.failed}" time="${(summary.durationMs / 1000).toFixed(3)}">`,
|
|
73
|
+
` <testsuite name="${escapeXml(suiteName)}" tests="${summary.total}" failures="${summary.failed}" time="${(summary.durationMs / 1000).toFixed(3)}">`,
|
|
74
|
+
cases,
|
|
75
|
+
' </testsuite>',
|
|
76
|
+
'</testsuites>',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=reporters.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ScenarioSummary } from './reporters';
|
|
2
|
+
import type { CaseResult, ScenarioRunOptions, Suite } from './schema';
|
|
3
|
+
export interface ScenarioRunResult {
|
|
4
|
+
suite: string;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
summary: ScenarioSummary;
|
|
7
|
+
results: CaseResult[];
|
|
8
|
+
artifactsDir: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runScenario(suite: Suite, suitePath: string, options?: ScenarioRunOptions): Promise<ScenarioRunResult>;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runScenario = runScenario;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const launcher_1 = require("../browser/launcher");
|
|
11
|
+
const frames_1 = require("../browser/frames");
|
|
12
|
+
const sampler_1 = require("../browser/sampler");
|
|
13
|
+
const interactions_1 = require("../browser/interactions");
|
|
14
|
+
const evalSerializer_1 = require("../engine/evalSerializer");
|
|
15
|
+
const screenshot_1 = require("../utils/screenshot");
|
|
16
|
+
const assertions_1 = require("./assertions");
|
|
17
|
+
const reporters_1 = require("./reporters");
|
|
18
|
+
async function evaluateValue(frame, expression) {
|
|
19
|
+
const handle = await frame.evaluateHandle(expression);
|
|
20
|
+
try {
|
|
21
|
+
const resolved = await handle.evaluateHandle((value) => Promise.resolve(value));
|
|
22
|
+
try {
|
|
23
|
+
return await resolved.evaluate(evalSerializer_1.serializeEvalResult);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await resolved.dispose();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await handle.dispose();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function readState(page) {
|
|
34
|
+
return { url: page.url(), title: await page.title(), frames: (0, frames_1.describeFrames)(page).length };
|
|
35
|
+
}
|
|
36
|
+
async function readGlobals(page, names) {
|
|
37
|
+
if (!names.length)
|
|
38
|
+
return {};
|
|
39
|
+
return page.evaluate((list) => {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const name of list) {
|
|
42
|
+
let current = window;
|
|
43
|
+
for (const part of name.split('.')) {
|
|
44
|
+
if (current === null || current === undefined) {
|
|
45
|
+
current = undefined;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
current = current[part];
|
|
49
|
+
}
|
|
50
|
+
out[name] = typeof current;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}, names);
|
|
54
|
+
}
|
|
55
|
+
function stepLabel(step) {
|
|
56
|
+
return step.do;
|
|
57
|
+
}
|
|
58
|
+
function resolveCaseState(suiteDir, caseState, defaultsState, cliState) {
|
|
59
|
+
if (caseState)
|
|
60
|
+
return path_1.default.resolve(suiteDir, caseState);
|
|
61
|
+
if (defaultsState)
|
|
62
|
+
return path_1.default.resolve(suiteDir, defaultsState);
|
|
63
|
+
return cliState ? path_1.default.resolve(process.cwd(), cliState) : undefined;
|
|
64
|
+
}
|
|
65
|
+
function caseDirectory(root, index, name) {
|
|
66
|
+
const dir = path_1.default.join(root, `${String(index + 1).padStart(2, '0')}-${(0, reporters_1.slugify)(name)}`);
|
|
67
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
async function runScenario(suite, suitePath, options = {}) {
|
|
71
|
+
const suiteDir = path_1.default.dirname(path_1.default.resolve(suitePath));
|
|
72
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
73
|
+
const artifactsDir = options.artifactsDir ??
|
|
74
|
+
path_1.default.join(os_1.default.homedir(), '.cssprobe-cli', 'scenario-runs', `${(0, reporters_1.slugify)(suite.name)}-${stamp}`);
|
|
75
|
+
fs_1.default.mkdirSync(artifactsDir, { recursive: true });
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const [caseIndex, scenarioCase] of suite.cases.entries()) {
|
|
78
|
+
const caseDir = caseDirectory(artifactsDir, caseIndex, scenarioCase.name);
|
|
79
|
+
const result = {
|
|
80
|
+
name: scenarioCase.name,
|
|
81
|
+
ok: false,
|
|
82
|
+
durationMs: 0,
|
|
83
|
+
steps: [],
|
|
84
|
+
artifactsDir: caseDir,
|
|
85
|
+
};
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const launcher = new launcher_1.BrowserLauncher({
|
|
88
|
+
browser: options.browser,
|
|
89
|
+
headed: options.headed,
|
|
90
|
+
viewport: options.viewport ?? suite.defaults?.viewport,
|
|
91
|
+
state: resolveCaseState(suiteDir, scenarioCase.state, suite.defaults?.state, options.state),
|
|
92
|
+
});
|
|
93
|
+
const vars = {};
|
|
94
|
+
try {
|
|
95
|
+
const url = scenarioCase.url ?? suite.defaults?.url ?? 'about:blank';
|
|
96
|
+
await launcher.open(url);
|
|
97
|
+
const page = launcher.getPage();
|
|
98
|
+
if (!page)
|
|
99
|
+
throw new Error('Browser page is not available');
|
|
100
|
+
for (const [stepIndex, step] of scenarioCase.steps.entries()) {
|
|
101
|
+
const stepStarted = Date.now();
|
|
102
|
+
const stepResult = {
|
|
103
|
+
index: stepIndex,
|
|
104
|
+
action: stepLabel(step),
|
|
105
|
+
ok: false,
|
|
106
|
+
durationMs: 0,
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
await executeStep(step, {
|
|
110
|
+
page,
|
|
111
|
+
vars,
|
|
112
|
+
frameSpec: scenarioCase.frame ?? suite.defaults?.frame,
|
|
113
|
+
caseDir,
|
|
114
|
+
stepIndex,
|
|
115
|
+
});
|
|
116
|
+
stepResult.ok = true;
|
|
117
|
+
if ('save' in step && step.save)
|
|
118
|
+
stepResult.saved = step.save;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
stepResult.error = error.message;
|
|
122
|
+
await page
|
|
123
|
+
.screenshot({ path: path_1.default.join(caseDir, `failure-step-${stepIndex + 1}.png`) })
|
|
124
|
+
.catch(() => { });
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
stepResult.durationMs = Date.now() - stepStarted;
|
|
129
|
+
result.steps.push(stepResult);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
result.ok = true;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
result.ok = false;
|
|
136
|
+
result.error = error.message;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
result.durationMs = Date.now() - started;
|
|
140
|
+
fs_1.default.writeFileSync(path_1.default.join(caseDir, 'case.json'), JSON.stringify(result, null, 2));
|
|
141
|
+
await launcher.close().catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
results.push(result);
|
|
144
|
+
if (!result.ok && options.bail)
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
const summary = (0, reporters_1.summarize)(results);
|
|
148
|
+
const runResult = { suite: suite.name, ok: summary.ok, summary, results, artifactsDir };
|
|
149
|
+
fs_1.default.writeFileSync(path_1.default.join(artifactsDir, 'summary.json'), JSON.stringify(runResult, null, 2));
|
|
150
|
+
if (options.junitPath) {
|
|
151
|
+
fs_1.default.mkdirSync(path_1.default.dirname(path_1.default.resolve(options.junitPath)), { recursive: true });
|
|
152
|
+
fs_1.default.writeFileSync(path_1.default.resolve(options.junitPath), require('./reporters').toJunit(suite.name, results), 'utf-8');
|
|
153
|
+
}
|
|
154
|
+
return runResult;
|
|
155
|
+
}
|
|
156
|
+
async function executeStep(step, context) {
|
|
157
|
+
const { page, vars, caseDir, stepIndex } = context;
|
|
158
|
+
switch (step.do) {
|
|
159
|
+
case 'goto': {
|
|
160
|
+
await page.goto(step.url, { waitUntil: step.waitUntil ?? 'domcontentloaded', timeout: 60000 });
|
|
161
|
+
if (step.save)
|
|
162
|
+
vars[step.save] = await readState(page);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
case 'reload': {
|
|
166
|
+
await page.reload({ waitUntil: step.waitUntil ?? 'domcontentloaded', timeout: 60000 });
|
|
167
|
+
if (step.save)
|
|
168
|
+
vars[step.save] = await readState(page);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
case 'eval': {
|
|
172
|
+
const frame = (0, frames_1.resolveFrame)(page, step.frame ?? context.frameSpec);
|
|
173
|
+
const value = await evaluateValue(frame, step.expression);
|
|
174
|
+
if (step.save)
|
|
175
|
+
vars[step.save] = value;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
case 'sample': {
|
|
179
|
+
const frame = (0, frames_1.resolveFrame)(page, step.frame ?? context.frameSpec);
|
|
180
|
+
const points = await (0, sampler_1.sampleFrame)(frame, step.expression, {
|
|
181
|
+
intervalMs: step.interval ?? 250,
|
|
182
|
+
durationMs: step.duration ?? 5000,
|
|
183
|
+
ignoreErrors: step.ignoreErrors,
|
|
184
|
+
});
|
|
185
|
+
const summary = (0, sampler_1.summarizeSamples)(points, step.key, 5000, step.tailFrom);
|
|
186
|
+
const value = { expression: step.expression, summary, series: points };
|
|
187
|
+
if (step.save)
|
|
188
|
+
vars[step.save] = value;
|
|
189
|
+
fs_1.default.writeFileSync(path_1.default.join(caseDir, `step-${stepIndex + 1}-${step.save ?? 'sample'}-series.json`), JSON.stringify(value, null, 2));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
case 'waitStable': {
|
|
193
|
+
const frame = (0, frames_1.resolveFrame)(page, step.frame ?? context.frameSpec);
|
|
194
|
+
const value = await (0, sampler_1.waitForStableValue)(frame, step.expression, {
|
|
195
|
+
tolerance: step.tolerance,
|
|
196
|
+
windowMs: step.window,
|
|
197
|
+
timeoutMs: step.timeout,
|
|
198
|
+
intervalMs: step.interval,
|
|
199
|
+
});
|
|
200
|
+
if (step.save)
|
|
201
|
+
vars[step.save] = value;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
case 'click': {
|
|
205
|
+
await (0, interactions_1.clickElement)(page, step.selector, { nth: step.nth, timeout: step.timeout });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case 'waitFor': {
|
|
209
|
+
const timeout = step.timeout ?? 30000;
|
|
210
|
+
if (step.networkIdle) {
|
|
211
|
+
await (0, interactions_1.waitForCondition)(page, { kind: 'networkIdle' }, timeout);
|
|
212
|
+
}
|
|
213
|
+
else if (step.js) {
|
|
214
|
+
await (0, interactions_1.waitForCondition)(page, { kind: 'js', expression: step.js }, timeout);
|
|
215
|
+
}
|
|
216
|
+
else if (step.selector) {
|
|
217
|
+
await (0, interactions_1.waitForCondition)(page, { kind: 'selector', selector: step.selector, state: step.state ?? 'visible' }, timeout);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
throw new Error('waitFor requires selector, js, or networkIdle');
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case 'screenshot': {
|
|
225
|
+
const buffer = await page.screenshot({ fullPage: step.fullPage });
|
|
226
|
+
const out = path_1.default.resolve(caseDir, step.path ?? `step-${stepIndex + 1}-screenshot.png`);
|
|
227
|
+
(0, screenshot_1.saveScreenshotFile)(`data:image/png;base64,${buffer.toString('base64')}`, out);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
case 'assert': {
|
|
231
|
+
const names = [...Object.keys(step.globals ?? {}), ...(step.absent ?? [])];
|
|
232
|
+
const assertionContext = {
|
|
233
|
+
vars,
|
|
234
|
+
state: await readState(page),
|
|
235
|
+
globals: await readGlobals(page, names),
|
|
236
|
+
};
|
|
237
|
+
const error = (0, assertions_1.evaluateAssertion)(step, assertionContext);
|
|
238
|
+
if (error)
|
|
239
|
+
throw new Error(error);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=runner.js.map
|