flowstride 1.0.21 → 1.0.22
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/NATIVE_BROWSER.md +40 -0
- package/NATIVE_CI.md +14 -2
- package/NATIVE_WORKFLOWS.md +44 -0
- package/README.md +8 -1
- package/dist/adapters/api/undici.d.ts +3 -1
- package/dist/adapters/api/undici.js +17 -3
- package/dist/adapters/web/playwright.js +23 -73
- package/dist/ci/runtime.js +9 -1
- package/dist/cli/commands/run.js +14 -4
- package/dist/cli/manual_server/workspace_server.js +90 -74
- package/dist/manual/security.d.ts +4 -4
- package/dist/native/accessibility.d.ts +2 -1
- package/dist/native/accessibility.js +22 -6
- package/dist/native/assertions.d.ts +2 -1
- package/dist/native/assertions.js +174 -91
- package/dist/native/deadline.d.ts +2 -0
- package/dist/native/deadline.js +13 -0
- package/dist/native/events.d.ts +3 -2
- package/dist/native/events.js +35 -8
- package/dist/native/functions.d.ts +2 -1
- package/dist/native/functions.js +34 -6
- package/dist/native/runtime.d.ts +3 -0
- package/dist/native/runtime.js +12 -6
- package/dist/parser/assertion_contract.d.ts +12 -0
- package/dist/parser/assertion_contract.js +56 -0
- package/dist/parser/parser.js +14 -6
- package/dist/reporting/assertion_result.d.ts +36 -0
- package/dist/reporting/assertion_result.js +104 -0
- package/dist/reporting/pdf_generator.js +25 -20
- package/dist/reporting/runtime_failure.d.ts +10 -0
- package/dist/reporting/runtime_failure.js +64 -0
- package/dist/reporting/step_status.d.ts +10 -0
- package/dist/reporting/step_status.js +44 -0
- package/dist/runner/flow_orchestrator.d.ts +1 -0
- package/dist/runner/flow_orchestrator.js +8 -3
- package/dist/runner/flow_worker.d.ts +8 -1
- package/dist/runner/flow_worker.js +119 -41
- package/dist/runner/plugin_manager.js +3 -0
- package/dist/runner/temporary_store.d.ts +1 -0
- package/dist/runner/temporary_store.js +3 -0
- package/dist/sdk/api_proxy.d.ts +3 -0
- package/dist/sdk/api_proxy.js +16 -0
- package/dist/security/localWorkspace.d.ts +2 -0
- package/dist/security/localWorkspace.js +17 -0
- package/dist/security/redaction.js +1 -1
- package/dist/types/index.d.ts +3 -0
- package/examples/native-workflows/README.md +144 -0
- package/examples/native-workflows/api-and-ui/README.md +80 -0
- package/examples/native-workflows/api-and-ui/flows/api-and-ui.flow +27 -0
- package/examples/native-workflows/api-and-ui/negative/wrong-backend-state.flow +27 -0
- package/examples/native-workflows/api-ui-otp/README.md +140 -0
- package/examples/native-workflows/api-ui-otp/templates/api-ui-otp.flow +31 -0
- package/examples/native-workflows/api-ui-otp/templates/existing-account-otp.flow +20 -0
- package/examples/native-workflows/audio-to-text/README.md +131 -0
- package/examples/native-workflows/audio-to-text/fixtures/microphone-tone.wav +0 -0
- package/examples/native-workflows/audio-to-text/flows/microphone-demo.flow +23 -0
- package/examples/native-workflows/audio-to-text/negative/wrong-transcript.flow +23 -0
- package/examples/native-workflows/audio-to-text/templates/real-transcription.flow +25 -0
- package/examples/native-workflows/iframes/README.md +116 -0
- package/examples/native-workflows/iframes/flows/explicit-frame-plugin.flow +20 -0
- package/examples/native-workflows/iframes/flows/readable-frame.flow +13 -0
- package/examples/native-workflows/iframes/negative/wrong-frame-result.flow +13 -0
- package/examples/native-workflows/iframes/plugins/frame-actions.ts +7 -0
- package/examples/native-workflows/manual-testing/README.md +85 -0
- package/examples/native-workflows/manual-testing/fixtures/profile-review-mismatch.json +93 -0
- package/examples/native-workflows/manual-testing/fixtures/profile-review.json +93 -0
- package/examples/native-workflows/shared/server.mjs +80 -0
- package/examples/native-workflows/tables-and-cells/README.md +118 -0
- package/examples/native-workflows/tables-and-cells/flows/coordinates.flow +17 -0
- package/examples/native-workflows/tables-and-cells/flows/search-extract-reuse.flow +27 -0
- package/examples/native-workflows/tables-and-cells/negative/wrong-cell-value.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/README.md +140 -0
- package/examples/native-workflows/variables-and-reuse/flows/reuse.flow +9 -0
- package/examples/native-workflows/variables-and-reuse/flows/setup.first.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/negative/wrong-reused-value.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/sessions/restore.flow +10 -0
- package/examples/native-workflows/variables-and-reuse/sessions/save.flow +29 -0
- package/package.json +11 -8
- package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/index-DglsL_J2.js +0 -636
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nativeStepTimeout = nativeStepTimeout;
|
|
4
|
+
const validation_1 = require("./validation");
|
|
5
|
+
/** Only commands whose validated contract accepts a timeout may override it. */
|
|
6
|
+
function nativeStepTimeout(command, input, fallback) {
|
|
7
|
+
if (['assert.that', 'a11y.match', 'a11y.snapshot', 'stub.expect'].includes(command) &&
|
|
8
|
+
input !== null && typeof input === 'object' && !Array.isArray(input) &&
|
|
9
|
+
Object.hasOwn(input, 'timeout')) {
|
|
10
|
+
return validation_1.positive.parse(input.timeout);
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
package/dist/native/events.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AssertionReporter } from '../reporting/assertion_result';
|
|
1
2
|
import type { FlowstrideConfig } from '../types';
|
|
2
3
|
import { WebAdapter } from '../adapters/web/playwright';
|
|
3
4
|
export declare class ArmedEvent<T> {
|
|
@@ -27,8 +28,8 @@ export declare class NativeEvents {
|
|
|
27
28
|
private savedDownloads;
|
|
28
29
|
constructor(web: WebAdapter, config: FlowstrideConfig);
|
|
29
30
|
private key;
|
|
30
|
-
network(action: string, input: unknown): Promise<any>;
|
|
31
|
-
download(action: string, input: unknown): Promise<any>;
|
|
31
|
+
network(action: string, input: unknown, reporter?: AssertionReporter): Promise<any>;
|
|
32
|
+
download(action: string, input: unknown, reporter?: AssertionReporter): Promise<any>;
|
|
32
33
|
disposeProfile(profile: string): Promise<void>;
|
|
33
34
|
dispose(): Promise<void>;
|
|
34
35
|
}
|
package/dist/native/events.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.NativeEvents = exports.ArmedEvent = void 0;
|
|
7
7
|
exports.matchesURL = matchesURL;
|
|
8
|
+
const assertion_result_1 = require("../reporting/assertion_result");
|
|
8
9
|
const zod_1 = require("zod");
|
|
9
10
|
const validation_1 = require("./validation");
|
|
10
11
|
const projectPaths_1 = require("../utils/projectPaths");
|
|
@@ -60,7 +61,7 @@ class NativeEvents {
|
|
|
60
61
|
this.config = config;
|
|
61
62
|
}
|
|
62
63
|
key(value) { return `${this.web.activeProfile}:${value}`; }
|
|
63
|
-
async network(action, input) {
|
|
64
|
+
async network(action, input, reporter) {
|
|
64
65
|
if (this.routeErrors.length)
|
|
65
66
|
throw new Error(`Network route failed: ${this.routeErrors.shift()}`);
|
|
66
67
|
if (action === 'route') {
|
|
@@ -180,19 +181,26 @@ class NativeEvents {
|
|
|
180
181
|
throw new Error('No captured event; call network.wait first.');
|
|
181
182
|
if (!['status', 'method', 'equals', 'contains'].some(key => key in data))
|
|
182
183
|
throw new Error('Provide a network expectation.');
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
const checks = [];
|
|
185
|
+
if (data.status !== undefined)
|
|
186
|
+
checks.push((0, assertion_result_1.assertionCheck)('status', 'status', result.status === data.status, data.status, result.status));
|
|
187
|
+
if (data.method)
|
|
188
|
+
checks.push((0, assertion_result_1.assertionCheck)('method', 'method', result.method === data.method.toUpperCase(), data.method.toUpperCase(), result.method));
|
|
185
189
|
let actual = data.header ? result.headers[data.header.toLowerCase()] : result.body;
|
|
186
190
|
if (data.path)
|
|
187
191
|
for (const key of data.path.split('.'))
|
|
188
192
|
actual = actual?.[key];
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
const protect = (value) => /password|passcode|secret|token|authorization|cookie|api.?key|otp/i.test((data.header || '') + ' ' + (data.path || '')) ? '[REDACTED]' : value;
|
|
194
|
+
if ('equals' in data)
|
|
195
|
+
checks.push((0, assertion_result_1.assertionCheck)('equals', (data.header ? 'header ' + data.header : data.path ? 'body path ' + data.path : 'body') + ' equals', (0, node_util_1.isDeepStrictEqual)(actual, data.equals), protect(data.equals), protect(actual)));
|
|
196
|
+
if (data.contains !== undefined)
|
|
197
|
+
checks.push((0, assertion_result_1.assertionCheck)('contains', 'content contains', String(actual).includes(data.contains), protect(data.contains), protect(actual)));
|
|
198
|
+
(0, assertion_result_1.requireAssertion)('network.expect', 'captured network event ' + JSON.stringify(data.name), checks, reporter, { reason: checks.some(check => check.status === 'failed' && ['status', 'method'].includes(check.key)) ? 'Network status/method assertion failed.' : checks.some(check => check.status === 'failed') ? 'Network content assertion failed.' : undefined });
|
|
191
199
|
return;
|
|
192
200
|
}
|
|
193
201
|
throw new Error(`Unknown network action: ${action}`);
|
|
194
202
|
}
|
|
195
|
-
async download(action, input) {
|
|
203
|
+
async download(action, input, reporter) {
|
|
196
204
|
if (action === 'arm') {
|
|
197
205
|
const data = zod_1.z.object({ name: validation_1.name, timeout: validation_1.positive.optional() }).strict().parse(input), key = this.key(data.name), page = this.web.getPage();
|
|
198
206
|
if (this.downloads.has(key))
|
|
@@ -240,8 +248,27 @@ class NativeEvents {
|
|
|
240
248
|
hash.update(chunk);
|
|
241
249
|
sha256 = hash.digest('hex');
|
|
242
250
|
}
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
const checks = [];
|
|
252
|
+
if (data.filename !== undefined)
|
|
253
|
+
checks.push((0, assertion_result_1.assertionCheck)('filename', 'filename', saved.suggested === data.filename, data.filename, saved.suggested));
|
|
254
|
+
if (data.size !== undefined)
|
|
255
|
+
checks.push((0, assertion_result_1.assertionCheck)('size', 'size', length === data.size, data.size, length));
|
|
256
|
+
if (data.minSize !== undefined)
|
|
257
|
+
checks.push((0, assertion_result_1.assertionCheck)('minSize', 'minimum size', length >= data.minSize, data.minSize, length));
|
|
258
|
+
if (data.contains !== undefined)
|
|
259
|
+
checks.push((0, assertion_result_1.assertionCheck)('contains', 'content contains', body.toString('utf8').includes(data.contains), data.contains, body.toString('utf8')));
|
|
260
|
+
if (data.sha256 !== undefined)
|
|
261
|
+
checks.push((0, assertion_result_1.assertionCheck)('sha256', 'SHA-256', sha256 === data.sha256, data.sha256, sha256));
|
|
262
|
+
if ('json' in data) {
|
|
263
|
+
try {
|
|
264
|
+
const actual = JSON.parse(body.toString('utf8'));
|
|
265
|
+
checks.push((0, assertion_result_1.assertionCheck)('json', 'JSON content', (0, node_util_1.isDeepStrictEqual)(actual, data.json), data.json, actual));
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
checks.push({ ...(0, assertion_result_1.assertionCheck)('json', 'JSON content', false, data.json, body.toString('utf8')), reason: 'Downloaded content is not valid JSON: ' + (0, assertion_result_1.diagnosticValue)(error instanceof Error ? error.message : String(error)) });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
(0, assertion_result_1.requireAssertion)('download.expect', 'saved download ' + JSON.stringify(data.name), checks, reporter, { reason: checks.some(check => check.status === 'failed') ? 'Download assertion failed.' : undefined });
|
|
245
272
|
return;
|
|
246
273
|
}
|
|
247
274
|
throw new Error(`Unknown download action: ${action}`);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type AssertionReporter } from '../reporting/assertion_result';
|
|
1
2
|
import type { Page } from 'playwright';
|
|
2
3
|
export declare class NativeFunctions {
|
|
3
4
|
private installed;
|
|
4
5
|
private pages;
|
|
5
6
|
private counter;
|
|
6
7
|
private key;
|
|
7
|
-
execute(page: Page, action: string, input: unknown, timeout: number): Promise<void>;
|
|
8
|
+
execute(page: Page, action: string, input: unknown, timeout: number, reporter?: AssertionReporter): Promise<void>;
|
|
8
9
|
dispose(): Promise<void>;
|
|
9
10
|
}
|
package/dist/native/functions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NativeFunctions = void 0;
|
|
4
|
+
const assertion_result_1 = require("../reporting/assertion_result");
|
|
4
5
|
const zod_1 = require("zod");
|
|
5
6
|
const validation_1 = require("./validation");
|
|
6
7
|
const node_util_1 = require("node:util");
|
|
@@ -50,7 +51,7 @@ class NativeFunctions {
|
|
|
50
51
|
counter = 0;
|
|
51
52
|
key(page, name) { if (!this.pages.has(page))
|
|
52
53
|
this.pages.set(page, ++this.counter); return `${this.pages.get(page)}:${name}`; }
|
|
53
|
-
async execute(page, action, input, timeout) {
|
|
54
|
+
async execute(page, action, input, timeout, reporter) {
|
|
54
55
|
if (action === 'install') {
|
|
55
56
|
const data = schema.parse(input);
|
|
56
57
|
if (this.installed.has(this.key(page, data.name)))
|
|
@@ -64,7 +65,7 @@ class NativeFunctions {
|
|
|
64
65
|
await script?.dispose();
|
|
65
66
|
throw error;
|
|
66
67
|
}
|
|
67
|
-
this.installed.set(this.key(page, data.name), { name: data.name, page, script });
|
|
68
|
+
this.installed.set(this.key(page, data.name), { name: data.name, target: data.target, page, script });
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
71
|
if (action === 'restore') {
|
|
@@ -81,10 +82,37 @@ class NativeFunctions {
|
|
|
81
82
|
const data = zod_1.z.object({ name: validation_1.name, count: zod_1.z.number().int().nonnegative().optional(), minCount: zod_1.z.number().int().nonnegative().optional(), args: zod_1.z.array(zod_1.z.unknown()).optional(), timeout: validation_1.positive.optional() }).strict().parse(input);
|
|
82
83
|
if (!['count', 'minCount', 'args'].some(key => key in data))
|
|
83
84
|
throw new Error('Provide a call count or expected arguments.');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const started = Date.now(), limit = data.timeout ?? timeout;
|
|
86
|
+
const sensitive = /auth|login|password|passcode|secret|token|cookie|api.?key|otp/i.test(this.installed.get(this.key(page, data.name))?.target || data.name);
|
|
87
|
+
let latest = (0, assertion_result_1.reportAssertion)('stub.expect', 'function double ' + JSON.stringify(data.name), [{ key: 'calls', label: 'Recorded calls available', status: 'running' }], reporter, { status: 'running', timeout: limit, elapsed: 0 });
|
|
88
|
+
let closed = false;
|
|
89
|
+
try {
|
|
90
|
+
await (0, validation_1.poll)(async () => {
|
|
91
|
+
const calls = await page.evaluate(name => window[Symbol.for('flowstride.function-doubles')]?.[name]?.calls, data.name);
|
|
92
|
+
const checks = [(0, assertion_result_1.assertionCheck)('calls', 'Recorded calls available', Array.isArray(calls), 'recorded calls', Array.isArray(calls) ? calls.length : 'No function double found on this page')];
|
|
93
|
+
if (Array.isArray(calls)) {
|
|
94
|
+
if (data.count !== undefined)
|
|
95
|
+
checks.push((0, assertion_result_1.assertionCheck)('count', 'call count', calls.length === data.count, data.count, calls.length));
|
|
96
|
+
if (data.minCount !== undefined)
|
|
97
|
+
checks.push((0, assertion_result_1.assertionCheck)('minCount', 'minimum call count', calls.length >= data.minCount, data.minCount, calls.length));
|
|
98
|
+
if (data.args !== undefined)
|
|
99
|
+
checks.push((0, assertion_result_1.assertionCheck)('args', 'matching call arguments', calls.some(call => (0, node_util_1.isDeepStrictEqual)(call, data.args)), sensitive ? "[REDACTED]" : data.args, sensitive ? "[REDACTED]" : calls));
|
|
100
|
+
}
|
|
101
|
+
if (!closed)
|
|
102
|
+
latest = (0, assertion_result_1.reportAssertion)('stub.expect', 'function double ' + JSON.stringify(data.name), checks, reporter, { status: 'running', timeout: limit, elapsed: Date.now() - started });
|
|
103
|
+
return checks.every(check => check.status === 'passed');
|
|
104
|
+
}, limit, `function double ${data.name}`);
|
|
105
|
+
closed = true;
|
|
106
|
+
reporter?.({ ...latest, status: 'passed', elapsed: Date.now() - started });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
closed = true;
|
|
110
|
+
const result = { ...latest, status: 'failed', elapsed: Date.now() - started, reason: (0, assertion_result_1.diagnosticValue)(error instanceof Error ? error.message : String(error)) };
|
|
111
|
+
if (!result.checks.some(check => check.status === 'failed'))
|
|
112
|
+
result.checks.push({ key: 'read', label: 'Recorded calls could be read', status: 'failed', reason: result.reason });
|
|
113
|
+
reporter?.(result);
|
|
114
|
+
throw new assertion_result_1.AssertionFailure(result);
|
|
115
|
+
}
|
|
88
116
|
return;
|
|
89
117
|
}
|
|
90
118
|
throw new Error(`Unknown stub action: ${action}`);
|
package/dist/native/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssertionResult } from '../reporting/assertion_result';
|
|
1
2
|
import type { FlowstrideConfig } from '../types';
|
|
2
3
|
import { WebAdapter } from '../adapters/web/playwright';
|
|
3
4
|
import { NativeEvents } from './events';
|
|
@@ -5,6 +6,8 @@ export declare class NativeRuntime {
|
|
|
5
6
|
private web;
|
|
6
7
|
private config;
|
|
7
8
|
readonly events: NativeEvents;
|
|
9
|
+
lastAssertion?: AssertionResult;
|
|
10
|
+
private assertionGeneration;
|
|
8
11
|
private functions;
|
|
9
12
|
private components;
|
|
10
13
|
constructor(web: WebAdapter, config: FlowstrideConfig);
|
package/dist/native/runtime.js
CHANGED
|
@@ -18,6 +18,8 @@ class NativeRuntime {
|
|
|
18
18
|
web;
|
|
19
19
|
config;
|
|
20
20
|
events;
|
|
21
|
+
lastAssertion;
|
|
22
|
+
assertionGeneration = 0;
|
|
21
23
|
functions = new functions_1.NativeFunctions();
|
|
22
24
|
components;
|
|
23
25
|
constructor(web, config) {
|
|
@@ -27,6 +29,10 @@ class NativeRuntime {
|
|
|
27
29
|
this.components = new components_1.NativeComponents(config);
|
|
28
30
|
}
|
|
29
31
|
async execute(command, input = {}) {
|
|
32
|
+
this.lastAssertion = undefined;
|
|
33
|
+
const generation = ++this.assertionGeneration;
|
|
34
|
+
const report = result => { if (generation === this.assertionGeneration)
|
|
35
|
+
this.lastAssertion = result; };
|
|
30
36
|
const [area, action, extra] = command.split('.');
|
|
31
37
|
if (!action || extra)
|
|
32
38
|
throw new Error(`Unknown native command: flow.${command}`);
|
|
@@ -78,18 +84,18 @@ class NativeRuntime {
|
|
|
78
84
|
if (area === 'assert') {
|
|
79
85
|
if (action !== 'that')
|
|
80
86
|
throw new Error(`Unknown assertion action: ${action}`);
|
|
81
|
-
return (0, assertions_1.assertBrowser)(this.web.getPage(), input, this.config.timeout);
|
|
87
|
+
return (0, assertions_1.assertBrowser)(this.web.getPage(), input, this.config.timeout, report);
|
|
82
88
|
}
|
|
83
89
|
if (area === 'network')
|
|
84
|
-
return this.events.network(action, input);
|
|
90
|
+
return this.events.network(action, input, report);
|
|
85
91
|
if (area === 'download')
|
|
86
|
-
return this.events.download(action, input);
|
|
92
|
+
return this.events.download(action, input, report);
|
|
87
93
|
if (area === 'stub')
|
|
88
|
-
return this.functions.execute(this.web.getPage(), action, input, this.config.timeout);
|
|
94
|
+
return this.functions.execute(this.web.getPage(), action, input, this.config.timeout, report);
|
|
89
95
|
if (area === 'component')
|
|
90
96
|
return this.components.execute(this.web.getPage(), action, input);
|
|
91
97
|
if (area === 'a11y')
|
|
92
|
-
return (0, accessibility_1.accessibility)(this.web.getPage(), this.config, action, input);
|
|
98
|
+
return (0, accessibility_1.accessibility)(this.web.getPage(), this.config, action, input, report);
|
|
93
99
|
if (area === 'clock') {
|
|
94
100
|
const clock = this.web.getPage().clock;
|
|
95
101
|
if (action === 'install') {
|
|
@@ -149,6 +155,6 @@ class NativeRuntime {
|
|
|
149
155
|
node_fs_1.default.writeFileSync(file, html, { mode: 0o600 });
|
|
150
156
|
return file;
|
|
151
157
|
}
|
|
152
|
-
async dispose() { await this.events.dispose(); await this.functions.dispose(); await this.components.dispose(); }
|
|
158
|
+
async dispose() { this.assertionGeneration++; this.lastAssertion = undefined; await this.events.dispose(); await this.functions.dispose(); await this.components.dispose(); }
|
|
153
159
|
}
|
|
154
160
|
exports.NativeRuntime = NativeRuntime;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const expectationTypes: Set<string>;
|
|
2
|
+
export declare const apiAssertionConditions: Set<string>;
|
|
3
|
+
export declare function validateExpectationType(type: unknown): void;
|
|
4
|
+
/** Templates are checked after interpolation; literal thresholds fail at parse time. */
|
|
5
|
+
export declare function validateResponseTimeThreshold(value: unknown, allowTemplate?: boolean): number | undefined;
|
|
6
|
+
export declare function validateApiAssertions(assertions: unknown): asserts assertions is {
|
|
7
|
+
condition: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}[];
|
|
10
|
+
/** And remains limited to assertions and existing custom assertion plugins. */
|
|
11
|
+
export declare const nativeAssertionCommands: Set<string>;
|
|
12
|
+
export declare function isAssertionStep(action: string, payload: any): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nativeAssertionCommands = exports.apiAssertionConditions = exports.expectationTypes = void 0;
|
|
4
|
+
exports.validateExpectationType = validateExpectationType;
|
|
5
|
+
exports.validateResponseTimeThreshold = validateResponseTimeThreshold;
|
|
6
|
+
exports.validateApiAssertions = validateApiAssertions;
|
|
7
|
+
exports.isAssertionStep = isAssertionStep;
|
|
8
|
+
exports.expectationTypes = new Set([
|
|
9
|
+
'status', 'responseTime', 'resBody', 'resHeader', 'cookie',
|
|
10
|
+
'visible', 'text', 'value', 'attribute', 'transcript',
|
|
11
|
+
]);
|
|
12
|
+
exports.apiAssertionConditions = new Set([
|
|
13
|
+
'equals', 'contains', 'to.be', 'to.not.be', 'is', 'is.not',
|
|
14
|
+
'type.of', 'format', 'matches', 'lessthan', 'greaterthan',
|
|
15
|
+
'length', 'length.greaterthan', 'length.lessthan',
|
|
16
|
+
'includes', 'does.not.include', 'has.key', 'does.not.have.key',
|
|
17
|
+
'is.empty', 'not.empty',
|
|
18
|
+
]);
|
|
19
|
+
function validateExpectationType(type) {
|
|
20
|
+
if (typeof type !== 'string' || !exports.expectationTypes.has(type)) {
|
|
21
|
+
throw new Error(`Unsupported flow.expect modifier: ${String(type)}.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Templates are checked after interpolation; literal thresholds fail at parse time. */
|
|
25
|
+
function validateResponseTimeThreshold(value, allowTemplate = false) {
|
|
26
|
+
if (allowTemplate && typeof value === 'string' &&
|
|
27
|
+
/\{\{env\.[^}]+\}\}|(?:@|\$)(?:global\[[^\]]+\]|[a-zA-Z_]\w*)/.test(value))
|
|
28
|
+
return;
|
|
29
|
+
if ((typeof value !== 'string' && typeof value !== 'number') ||
|
|
30
|
+
(typeof value === 'string' && value.trim() === '')) {
|
|
31
|
+
throw new Error('API responseTime threshold must be a finite nonnegative number.');
|
|
32
|
+
}
|
|
33
|
+
const threshold = Number(value);
|
|
34
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
35
|
+
throw new Error('API responseTime threshold must be a finite nonnegative number.');
|
|
36
|
+
}
|
|
37
|
+
return threshold;
|
|
38
|
+
}
|
|
39
|
+
function validateApiAssertions(assertions) {
|
|
40
|
+
if (!Array.isArray(assertions) || assertions.length === 0) {
|
|
41
|
+
throw new Error('API assertion requires at least one supported condition.');
|
|
42
|
+
}
|
|
43
|
+
for (const assertion of assertions) {
|
|
44
|
+
if (!assertion || !exports.apiAssertionConditions.has(assertion.condition)) {
|
|
45
|
+
throw new Error(`Unsupported API assertion condition: ${String(assertion?.condition)}.`);
|
|
46
|
+
}
|
|
47
|
+
if (!['is.empty', 'not.empty'].includes(assertion.condition) && typeof assertion.value !== 'string') {
|
|
48
|
+
throw new Error(`API assertion ${assertion.condition} requires an expected string.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** And remains limited to assertions and existing custom assertion plugins. */
|
|
53
|
+
exports.nativeAssertionCommands = new Set(['assert.that', 'network.expect', 'download.expect', 'stub.expect', 'a11y.scan', 'a11y.match']);
|
|
54
|
+
function isAssertionStep(action, payload) {
|
|
55
|
+
return action === 'expect' || action === 'plugin' || action === 'native' && exports.nativeAssertionCommands.has(payload?.pluginMethod);
|
|
56
|
+
}
|
package/dist/parser/parser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Parser = void 0;
|
|
4
|
+
const assertion_contract_1 = require("./assertion_contract");
|
|
4
5
|
const lexer_1 = require("./lexer");
|
|
5
6
|
class Parser {
|
|
6
7
|
tokens;
|
|
@@ -99,8 +100,8 @@ class Parser {
|
|
|
99
100
|
declarations.push(this.parseDeclaration());
|
|
100
101
|
}
|
|
101
102
|
else if (token.type === lexer_1.TokenType.BLOCK) {
|
|
102
|
-
if (isLocked)
|
|
103
|
-
throw new Error(`Line ${token.line}: Terminal Lock Violation.
|
|
103
|
+
if (isLocked && token.value.toLowerCase().replace(/^stop\./, '') !== 'and')
|
|
104
|
+
throw new Error(`Line ${token.line}: Terminal Lock Violation. Only assertion And blocks are allowed after Then.`);
|
|
104
105
|
const blockToken = this.advance();
|
|
105
106
|
let rawBlockType = blockToken.value.toLowerCase();
|
|
106
107
|
if (rawBlockType.startsWith("stop.")) {
|
|
@@ -149,7 +150,7 @@ class Parser {
|
|
|
149
150
|
}
|
|
150
151
|
else if (token.type === lexer_1.TokenType.COMMAND ||
|
|
151
152
|
token.type === lexer_1.TokenType.ALIAS_COMMAND) {
|
|
152
|
-
if (isLocked && currentBlockType !== "then") {
|
|
153
|
+
if (isLocked && currentBlockType !== "then" && currentBlockType !== "and") {
|
|
153
154
|
throw new Error(`Line ${token.line}: Terminal Lock Violation.`);
|
|
154
155
|
}
|
|
155
156
|
const step = this.parseCommand();
|
|
@@ -157,8 +158,7 @@ class Parser {
|
|
|
157
158
|
step.isStop = true;
|
|
158
159
|
}
|
|
159
160
|
if (currentBlockType === "and" &&
|
|
160
|
-
step.action
|
|
161
|
-
step.action !== "plugin") {
|
|
161
|
+
!(0, assertion_contract_1.isAssertionStep)(step.action, step.payload)) {
|
|
162
162
|
throw new Error(`Line ${token.line}: And blocks are strictly for assertions or custom assertion plugins.`);
|
|
163
163
|
}
|
|
164
164
|
steps.push(step);
|
|
@@ -371,7 +371,13 @@ class Parser {
|
|
|
371
371
|
const viaToken = this.consume(lexer_1.TokenType.PREPOSITION, 'Expected "via"');
|
|
372
372
|
if (viaToken.value.toLowerCase() !== "via")
|
|
373
373
|
throw new Error(`Line ${viaToken.line}: Expected 'via'`);
|
|
374
|
-
payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase();
|
|
374
|
+
payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase().replace(/^flow\./, '');
|
|
375
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(payload.verb)) {
|
|
376
|
+
throw new Error('Authentication refresh requires an HTTP verb: get, post, put, patch or delete.');
|
|
377
|
+
}
|
|
378
|
+
if (!['401', '403'].includes(payload.targetStatus)) {
|
|
379
|
+
throw new Error('Authentication refresh status must be 401 or 403.');
|
|
380
|
+
}
|
|
375
381
|
payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
|
|
376
382
|
payload.headers = [];
|
|
377
383
|
this.parseApiModifiers(payload);
|
|
@@ -507,6 +513,7 @@ class Parser {
|
|
|
507
513
|
if (mod.toLowerCase() === "responsetime" && mod !== "responseTime") {
|
|
508
514
|
throw new Error(`Syntax Error on line ${modToken.line}: Invalid keyword '${mod}'. Flowstride uses strict camelCase. Did you mean 'responseTime'?`);
|
|
509
515
|
}
|
|
516
|
+
(0, assertion_contract_1.validateExpectationType)(mod);
|
|
510
517
|
payload.type = mod;
|
|
511
518
|
if (mod === "status") {
|
|
512
519
|
payload.statusCode = this.consume(lexer_1.TokenType.STRING, 'Status code expected (e.g., "200")').value;
|
|
@@ -517,6 +524,7 @@ class Parser {
|
|
|
517
524
|
throw new Error(`Line ${modToken.line}: Expected 'lessThan' after responseTime`);
|
|
518
525
|
}
|
|
519
526
|
payload.threshold = this.consume(lexer_1.TokenType.STRING, "Time threshold expected").value;
|
|
527
|
+
(0, assertion_contract_1.validateResponseTimeThreshold)(payload.threshold, true);
|
|
520
528
|
}
|
|
521
529
|
else if (mod === "resBody" ||
|
|
522
530
|
mod === "resHeader" ||
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type AssertionCheckStatus = 'passed' | 'failed' | 'not_evaluated' | 'running';
|
|
2
|
+
export interface AssertionCheck {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
status: AssertionCheckStatus;
|
|
6
|
+
expected?: string;
|
|
7
|
+
actual?: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
children?: AssertionCheck[];
|
|
10
|
+
}
|
|
11
|
+
export interface AssertionResult {
|
|
12
|
+
command: string;
|
|
13
|
+
target: string;
|
|
14
|
+
status: 'passed' | 'failed' | 'running' | 'not_evaluated';
|
|
15
|
+
checks: AssertionCheck[];
|
|
16
|
+
timeout?: number;
|
|
17
|
+
elapsed?: number;
|
|
18
|
+
reason?: string;
|
|
19
|
+
sourceFile?: string;
|
|
20
|
+
sourceLine?: number;
|
|
21
|
+
profile?: string;
|
|
22
|
+
}
|
|
23
|
+
export type AssertionReporter = (result: AssertionResult) => void;
|
|
24
|
+
/** Diagnostic text is bounded data, including in terminals and exported reports. */
|
|
25
|
+
export declare function diagnosticValue(value: unknown): string;
|
|
26
|
+
export declare function assertionCheck(key: string, label: string, matched: boolean, expected?: unknown, actual?: unknown): AssertionCheck;
|
|
27
|
+
export declare function assertionText(result: AssertionResult): string;
|
|
28
|
+
export declare class AssertionFailure extends Error {
|
|
29
|
+
readonly assertion: AssertionResult;
|
|
30
|
+
constructor(assertion: AssertionResult);
|
|
31
|
+
}
|
|
32
|
+
export declare function reportAssertion(command: string, target: string, checks: AssertionCheck[], reporter?: AssertionReporter, metadata?: Partial<AssertionResult>): AssertionResult;
|
|
33
|
+
export declare function requireAssertion(command: string, target: string, checks: AssertionCheck[], reporter?: AssertionReporter, metadata?: Partial<AssertionResult>): void;
|
|
34
|
+
export declare function failedAssertion(result: AssertionResult | undefined, reason: string): AssertionResult | undefined;
|
|
35
|
+
export declare function commandLabel(action: string, payload: any): string;
|
|
36
|
+
export declare function unexecutedAssertion(action: string, payload: any, reason?: string): AssertionResult | undefined;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AssertionFailure = void 0;
|
|
4
|
+
exports.diagnosticValue = diagnosticValue;
|
|
5
|
+
exports.assertionCheck = assertionCheck;
|
|
6
|
+
exports.assertionText = assertionText;
|
|
7
|
+
exports.reportAssertion = reportAssertion;
|
|
8
|
+
exports.requireAssertion = requireAssertion;
|
|
9
|
+
exports.failedAssertion = failedAssertion;
|
|
10
|
+
exports.commandLabel = commandLabel;
|
|
11
|
+
exports.unexecutedAssertion = unexecutedAssertion;
|
|
12
|
+
const redaction_1 = require("../security/redaction");
|
|
13
|
+
/** Diagnostic text is bounded data, including in terminals and exported reports. */
|
|
14
|
+
function diagnosticValue(value) {
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
const safe = (0, redaction_1.redactPayload)(value);
|
|
18
|
+
text = typeof safe === 'string' ? safe : JSON.stringify(safe);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
text = '[unavailable]';
|
|
22
|
+
}
|
|
23
|
+
if (text === undefined)
|
|
24
|
+
text = '(not observed)';
|
|
25
|
+
return text.replace(/[\x00-\x1f\x7f-\x9f]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')).slice(0, 1500);
|
|
26
|
+
}
|
|
27
|
+
function assertionCheck(key, label, matched, expected, actual) {
|
|
28
|
+
return { key, label, status: matched ? 'passed' : 'failed',
|
|
29
|
+
...(expected === undefined ? {} : { expected: diagnosticValue(expected) }),
|
|
30
|
+
...(actual === undefined ? {} : { actual: diagnosticValue(actual) }) };
|
|
31
|
+
}
|
|
32
|
+
function assertionText(result) {
|
|
33
|
+
const lines = [`${result.status === 'passed' ? '✔' : result.status === 'not_evaluated' ? '○' : result.status === 'running' ? '◌' : '✖'} flow.${result.command}`];
|
|
34
|
+
const visit = (check, depth) => {
|
|
35
|
+
const prefix = ' '.repeat(depth);
|
|
36
|
+
lines.push(`${prefix}${check.status === 'passed' ? '✔' : check.status === 'failed' ? '✖' : '○'} ${diagnosticValue(check.label)}${check.status === 'not_evaluated' ? ' — Not evaluated' : ''}`);
|
|
37
|
+
if (check.status === 'failed') {
|
|
38
|
+
if (check.expected !== undefined)
|
|
39
|
+
lines.push(`${prefix} Expected: ${diagnosticValue(check.expected)}`);
|
|
40
|
+
if (check.actual !== undefined)
|
|
41
|
+
lines.push(`${prefix} Actual: ${diagnosticValue(check.actual)}`);
|
|
42
|
+
if (check.reason)
|
|
43
|
+
lines.push(`${prefix} Reason: ${diagnosticValue(check.reason)}`);
|
|
44
|
+
}
|
|
45
|
+
for (const child of check.children || [])
|
|
46
|
+
visit(child, depth + 1);
|
|
47
|
+
};
|
|
48
|
+
for (const check of result.checks)
|
|
49
|
+
visit(check, 1);
|
|
50
|
+
if (result.status === 'failed' && result.reason)
|
|
51
|
+
lines.push(` Reason: ${diagnosticValue(result.reason)}`);
|
|
52
|
+
if (result.timeout !== undefined)
|
|
53
|
+
lines.push(` Timeout: ${result.timeout} ms; elapsed: ${result.elapsed || 0} ms`);
|
|
54
|
+
if (result.sourceFile)
|
|
55
|
+
lines.push(` Location: ${diagnosticValue(result.sourceFile)}${result.sourceLine ? ':' + result.sourceLine : ''}`);
|
|
56
|
+
if (result.profile)
|
|
57
|
+
lines.push(` Profile: ${diagnosticValue(result.profile)}`);
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
class AssertionFailure extends Error {
|
|
61
|
+
assertion;
|
|
62
|
+
constructor(assertion) {
|
|
63
|
+
super(`${assertion.reason?.includes('timed out') ? 'Assertion timed out' : 'Assertion failed'}: ${assertion.target}\n${assertionText(assertion)}`);
|
|
64
|
+
this.assertion = assertion;
|
|
65
|
+
this.name = 'AssertionFailure';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.AssertionFailure = AssertionFailure;
|
|
69
|
+
function reportAssertion(command, target, checks, reporter, metadata = {}) {
|
|
70
|
+
const result = { command, target, status: checks.some(check => check.status === 'failed') ? 'failed' : 'passed', checks, ...metadata };
|
|
71
|
+
reporter?.(result);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function requireAssertion(command, target, checks, reporter, metadata = {}) {
|
|
75
|
+
const result = reportAssertion(command, target, checks, reporter, metadata);
|
|
76
|
+
if (result.status === 'failed')
|
|
77
|
+
throw new AssertionFailure(result);
|
|
78
|
+
}
|
|
79
|
+
function failedAssertion(result, reason) {
|
|
80
|
+
if (!result)
|
|
81
|
+
return;
|
|
82
|
+
const copy = structuredClone(result);
|
|
83
|
+
copy.status = 'failed';
|
|
84
|
+
copy.reason ||= diagnosticValue(reason.split("\n")[0]);
|
|
85
|
+
if (/exceeded|timed out|timeout/i.test(reason))
|
|
86
|
+
copy.elapsed = Math.max(copy.elapsed || 0, copy.timeout || 0);
|
|
87
|
+
for (const check of copy.checks)
|
|
88
|
+
if (check.status === 'running')
|
|
89
|
+
check.status = 'failed';
|
|
90
|
+
if (!copy.checks.some(check => check.status === 'failed'))
|
|
91
|
+
copy.checks.push({ key: 'completion', label: 'Command completed within its deadline', status: 'failed', reason: diagnosticValue(reason) });
|
|
92
|
+
return copy;
|
|
93
|
+
}
|
|
94
|
+
function commandLabel(action, payload) {
|
|
95
|
+
return action === 'native' && typeof payload?.pluginMethod === 'string' ? 'flow.' + payload.pluginMethod
|
|
96
|
+
: action === 'plugin' && typeof payload?.pluginAlias === 'string' ? payload.pluginAlias + '.' + payload.pluginMethod : action;
|
|
97
|
+
}
|
|
98
|
+
function unexecutedAssertion(action, payload, reason = 'A previous required step failed.') {
|
|
99
|
+
if (action !== 'native' || !['assert.that', 'network.expect', 'download.expect', 'stub.expect', 'a11y.scan', 'a11y.match', 'a11y.snapshot'].includes(payload?.pluginMethod))
|
|
100
|
+
return;
|
|
101
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
102
|
+
const fields = payload.pluginMethod === 'assert.that' ? ['role', 'name', 'selector', 'condition'] : Object.keys(data).filter(key => !['timeout', 'name'].includes(key));
|
|
103
|
+
return { command: payload.pluginMethod, target: payload.pluginMethod, status: 'not_evaluated', reason, checks: fields.slice(0, 100).map(key => ({ key, label: key + ': ' + diagnosticValue(data[key]), status: 'not_evaluated', reason })) };
|
|
104
|
+
}
|
|
@@ -34,9 +34,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.PdfGenerator = void 0;
|
|
37
|
+
const assertion_result_1 = require("./assertion_result");
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const playwright_1 = require("playwright");
|
|
41
|
+
const step_status_1 = require("./step_status");
|
|
42
|
+
function escapeHtml(value) {
|
|
43
|
+
return String(value ?? "").replace(/[&<>"']/g, (character) => ({
|
|
44
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
45
|
+
})[character]);
|
|
46
|
+
}
|
|
40
47
|
class PdfGenerator {
|
|
41
48
|
async generateTestcasePdf(data, customOutputPath) {
|
|
42
49
|
let outputDir = "";
|
|
@@ -79,7 +86,7 @@ class PdfGenerator {
|
|
|
79
86
|
else if (Array.isArray(data)) {
|
|
80
87
|
scenariosToProcess = data;
|
|
81
88
|
}
|
|
82
|
-
const groupedScenarios =
|
|
89
|
+
const groupedScenarios = Object.create(null);
|
|
83
90
|
scenariosToProcess.forEach((scenario) => {
|
|
84
91
|
const key = scenario.fileName || "General";
|
|
85
92
|
if (!groupedScenarios[key])
|
|
@@ -95,31 +102,29 @@ class PdfGenerator {
|
|
|
95
102
|
let fileRows = "";
|
|
96
103
|
flatSteps.forEach((step, idx) => {
|
|
97
104
|
totalSteps++;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const isFail = status === "FAILED" || status === "FAIL";
|
|
101
|
-
if (isSuccess)
|
|
105
|
+
const result = (0, step_status_1.stepStatusPresentation)(step.status);
|
|
106
|
+
if (result.isPassed)
|
|
102
107
|
passedSteps++;
|
|
103
|
-
else if (
|
|
108
|
+
else if (result.isFailed)
|
|
104
109
|
failedSteps++;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
?
|
|
108
|
-
:
|
|
109
|
-
const actualResultColor =
|
|
110
|
+
const actionText = step.commandLabel || step.command || step.action || "-";
|
|
111
|
+
const expectedText = step.description
|
|
112
|
+
? `${step.type ? `${step.type} ` : ""}${step.description}`
|
|
113
|
+
: step.expected || "-";
|
|
114
|
+
const actualResultColor = result.isFailed ? "#ef4444" : "#64748b";
|
|
110
115
|
fileRows += `
|
|
111
|
-
<tr>
|
|
116
|
+
<tr data-step-status="${result.status}">
|
|
112
117
|
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: #64748b; font-weight: 500; width: 50px;">${idx + 1}</td>
|
|
113
|
-
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 700; color: #1e293b;">${
|
|
114
|
-
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: #475569; font-style: italic;">${
|
|
115
|
-
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: ${actualResultColor};">${
|
|
116
|
-
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 900; font-size: 10px; color: ${color};">${
|
|
118
|
+
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 700; color: #1e293b;">${escapeHtml(actionText)}</td>
|
|
119
|
+
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: #475569; font-style: italic;">${escapeHtml(expectedText)}</td>
|
|
120
|
+
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: ${actualResultColor};">${step.assertion ? `<pre style="white-space:pre-wrap;overflow-wrap:anywhere">${escapeHtml((0, assertion_result_1.assertionText)(step.assertion))}</pre>` : step.error ? `<pre style="white-space:pre-wrap;overflow-wrap:anywhere">${escapeHtml(step.error)}</pre>` : result.actualResult}</td>
|
|
121
|
+
<td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 900; font-size: 10px; color: ${result.color};">${result.label}</td>
|
|
117
122
|
</tr>
|
|
118
123
|
`;
|
|
119
124
|
});
|
|
120
125
|
tablesHtml += `
|
|
121
126
|
<div class="file-separator">
|
|
122
|
-
<span>📄 ${fileName.toUpperCase()}</span>
|
|
127
|
+
<span>📄 ${escapeHtml(fileName.toUpperCase())}</span>
|
|
123
128
|
</div>
|
|
124
129
|
<table>
|
|
125
130
|
<thead>
|
|
@@ -164,15 +169,15 @@ class PdfGenerator {
|
|
|
164
169
|
</div>
|
|
165
170
|
<div class="summary">
|
|
166
171
|
<div class="stat-box">
|
|
167
|
-
<div class="stat-value" style="color: #1e293b;">${totalSteps}</div>
|
|
172
|
+
<div class="stat-value" data-report-count="total" style="color: #1e293b;">${totalSteps}</div>
|
|
168
173
|
<div style="font-size:10px;color:#64748b;text-transform:uppercase;margin-top:4px;font-weight:700;">Total Steps</div>
|
|
169
174
|
</div>
|
|
170
175
|
<div class="stat-box">
|
|
171
|
-
<div class="stat-value" style="color:#10b981;">${passedSteps}</div>
|
|
176
|
+
<div class="stat-value" data-report-count="passed" style="color:#10b981;">${passedSteps}</div>
|
|
172
177
|
<div style="font-size:10px;color:#64748b;text-transform:uppercase;margin-top:4px;font-weight:700;">Passed</div>
|
|
173
178
|
</div>
|
|
174
179
|
<div class="stat-box">
|
|
175
|
-
<div class="stat-value" style="color:#ef4444;">${failedSteps}</div>
|
|
180
|
+
<div class="stat-value" data-report-count="failed" style="color:#ef4444;">${failedSteps}</div>
|
|
176
181
|
<div style="font-size:10px;color:#64748b;text-transform:uppercase;margin-top:4px;font-weight:700;">Failed</div>
|
|
177
182
|
</div>
|
|
178
183
|
</div>
|