cssprobe-cli 0.3.1 → 0.4.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/AGENTS.md +39 -7
- package/CHANGELOG.md +30 -0
- package/README.md +58 -5
- package/dist/browser/frames.d.ts +15 -0
- package/dist/browser/frames.js +60 -0
- package/dist/browser/interactions.d.ts +3 -2
- package/dist/browser/interactions.js +16 -4
- 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 +93 -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 +70 -20
- package/dist/daemon/expect.d.ts +8 -0
- package/dist/daemon/expect.js +47 -0
- package/dist/daemon/flowRunner.d.ts +13 -1
- package/dist/daemon/flowRunner.js +28 -3
- package/dist/engine/evalSerializer.js +2 -2
- package/dist/help.json +96 -17
- 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 +251 -0
- package/dist/scenario/schema.d.ts +1270 -0
- package/dist/scenario/schema.js +151 -0
- package/docs/guide/installation.md +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
const frameSpec = step.frame ?? context.frameSpec;
|
|
211
|
+
if (step.networkIdle) {
|
|
212
|
+
if (frameSpec)
|
|
213
|
+
throw new Error('networkIdle waits are page-level; remove frame');
|
|
214
|
+
await (0, interactions_1.waitForCondition)(page, { kind: 'networkIdle' }, timeout);
|
|
215
|
+
}
|
|
216
|
+
else if (step.js) {
|
|
217
|
+
await (0, interactions_1.waitForCondition)(frameSpec ? (0, frames_1.resolveFrame)(page, frameSpec) : page, { kind: 'js', expression: step.js }, timeout);
|
|
218
|
+
}
|
|
219
|
+
else if (step.selector) {
|
|
220
|
+
await (0, interactions_1.waitForCondition)(frameSpec ? (0, frames_1.resolveFrame)(page, frameSpec) : page, { kind: 'selector', selector: step.selector, state: step.state ?? 'visible' }, timeout);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
throw new Error('waitFor requires selector, js, or networkIdle');
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
case 'wait': {
|
|
228
|
+
await new Promise(resolve => setTimeout(resolve, step.ms));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
case 'screenshot': {
|
|
232
|
+
const buffer = await page.screenshot({ fullPage: step.fullPage });
|
|
233
|
+
const out = path_1.default.resolve(caseDir, step.path ?? `step-${stepIndex + 1}-screenshot.png`);
|
|
234
|
+
(0, screenshot_1.saveScreenshotFile)(`data:image/png;base64,${buffer.toString('base64')}`, out);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
case 'assert': {
|
|
238
|
+
const names = [...Object.keys(step.globals ?? {}), ...(step.absent ?? [])];
|
|
239
|
+
const assertionContext = {
|
|
240
|
+
vars,
|
|
241
|
+
state: await readState(page),
|
|
242
|
+
globals: await readGlobals(page, names),
|
|
243
|
+
};
|
|
244
|
+
const error = (0, assertions_1.evaluateAssertion)(step, assertionContext);
|
|
245
|
+
if (error)
|
|
246
|
+
throw new Error(error);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=runner.js.map
|