flowstride 1.0.20 → 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 +381 -0
- package/NATIVE_CI.md +17 -5
- package/NATIVE_WORKFLOWS.md +44 -0
- package/README.md +11 -4
- package/dist/adapters/api/undici.d.ts +11 -1
- package/dist/adapters/api/undici.js +28 -3
- package/dist/adapters/web/playwright.d.ts +22 -2
- package/dist/adapters/web/playwright.js +254 -107
- package/dist/ci/config.js +14 -1
- package/dist/ci/distribution.d.ts +38 -0
- package/dist/ci/distribution.js +134 -0
- package/dist/ci/init.d.ts +4 -1
- package/dist/ci/init.js +13 -2
- package/dist/ci/runtime.js +33 -6
- package/dist/cli/commands/run.js +17 -4
- package/dist/cli/index.js +24 -5
- package/dist/cli/manual_server/workspace_server.js +90 -74
- package/dist/manual/security.d.ts +6 -6
- package/dist/native/accessibility.d.ts +4 -0
- package/dist/native/accessibility.js +84 -0
- package/dist/native/assertions.d.ts +3 -0
- package/dist/native/assertions.js +223 -0
- package/dist/native/compatibility.d.ts +3 -0
- package/dist/native/compatibility.js +16 -0
- package/dist/native/components.d.ts +12 -0
- package/dist/native/components.js +122 -0
- package/dist/native/dashboard.d.ts +7 -0
- package/dist/native/dashboard.js +32 -0
- package/dist/native/deadline.d.ts +2 -0
- package/dist/native/deadline.js +13 -0
- package/dist/native/events.d.ts +35 -0
- package/dist/native/events.js +308 -0
- package/dist/native/evidence.d.ts +3 -0
- package/dist/native/evidence.js +31 -0
- package/dist/native/functions.d.ts +10 -0
- package/dist/native/functions.js +131 -0
- package/dist/native/runtime.d.ts +17 -0
- package/dist/native/runtime.js +160 -0
- package/dist/native/validation.d.ts +107 -0
- package/dist/native/validation.js +84 -0
- package/dist/parser/assertion_contract.d.ts +12 -0
- package/dist/parser/assertion_contract.js +56 -0
- package/dist/parser/lexer.js +15 -4
- package/dist/parser/parser.js +15 -7
- 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 +5 -0
- package/dist/runner/flow_orchestrator.js +30 -8
- package/dist/runner/flow_worker.d.ts +15 -1
- package/dist/runner/flow_worker.js +219 -42
- 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 +13 -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 +23 -9
- package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/index-B7MuBTWm.js +0 -460
|
@@ -0,0 +1,160 @@
|
|
|
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.NativeRuntime = void 0;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const validation_1 = require("./validation");
|
|
11
|
+
const projectPaths_1 = require("../utils/projectPaths");
|
|
12
|
+
const events_1 = require("./events");
|
|
13
|
+
const functions_1 = require("./functions");
|
|
14
|
+
const components_1 = require("./components");
|
|
15
|
+
const assertions_1 = require("./assertions");
|
|
16
|
+
const accessibility_1 = require("./accessibility");
|
|
17
|
+
class NativeRuntime {
|
|
18
|
+
web;
|
|
19
|
+
config;
|
|
20
|
+
events;
|
|
21
|
+
lastAssertion;
|
|
22
|
+
assertionGeneration = 0;
|
|
23
|
+
functions = new functions_1.NativeFunctions();
|
|
24
|
+
components;
|
|
25
|
+
constructor(web, config) {
|
|
26
|
+
this.web = web;
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.events = new events_1.NativeEvents(web, config);
|
|
29
|
+
this.components = new components_1.NativeComponents(config);
|
|
30
|
+
}
|
|
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; };
|
|
36
|
+
const [area, action, extra] = command.split('.');
|
|
37
|
+
if (!action || extra)
|
|
38
|
+
throw new Error(`Unknown native command: flow.${command}`);
|
|
39
|
+
if (area === 'browser') {
|
|
40
|
+
if (action === 'create') {
|
|
41
|
+
const data = zod_1.z.object({ name: validation_1.name, options: zod_1.z.unknown().optional() }).strict().parse(input);
|
|
42
|
+
(0, validation_1.validateContextOptions)(data.options, this.config.browser, this.config.projectRoot);
|
|
43
|
+
await this.web.createProfile(data.name, data.options);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (action === 'use' || action === 'close') {
|
|
47
|
+
const data = zod_1.z.object({ name: validation_1.name }).strict().parse(input);
|
|
48
|
+
if (action === 'use')
|
|
49
|
+
await this.web.useProfile(data.name);
|
|
50
|
+
else {
|
|
51
|
+
if (data.name === 'default')
|
|
52
|
+
throw new Error('The default profile stays available until file completion.');
|
|
53
|
+
await this.events.disposeProfile(data.name);
|
|
54
|
+
await this.web.closeProfile(data.name);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (action === 'list') {
|
|
59
|
+
zod_1.z.object({}).strict().parse(input);
|
|
60
|
+
return this.web.profileNames();
|
|
61
|
+
}
|
|
62
|
+
if (action === 'offline') {
|
|
63
|
+
const data = zod_1.z.object({ value: zod_1.z.boolean() }).strict().parse(input);
|
|
64
|
+
await this.web.getContext().setOffline(data.value);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (action === 'permissions') {
|
|
68
|
+
const data = zod_1.z.object({ permissions: zod_1.z.array(zod_1.z.string()), origin: zod_1.z.string().url().optional() }).strict().parse(input);
|
|
69
|
+
await this.web.getContext().grantPermissions(data.permissions, { origin: data.origin });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (action === 'clearPermissions') {
|
|
73
|
+
zod_1.z.object({}).strict().parse(input);
|
|
74
|
+
await this.web.getContext().clearPermissions();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (action === 'geolocation') {
|
|
78
|
+
const data = zod_1.z.object({ latitude: zod_1.z.number().min(-90).max(90), longitude: zod_1.z.number().min(-180).max(180), accuracy: zod_1.z.number().nonnegative().optional() }).strict().parse(input);
|
|
79
|
+
await this.web.getContext().setGeolocation(data);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Unknown browser action: ${action}`);
|
|
83
|
+
}
|
|
84
|
+
if (area === 'assert') {
|
|
85
|
+
if (action !== 'that')
|
|
86
|
+
throw new Error(`Unknown assertion action: ${action}`);
|
|
87
|
+
return (0, assertions_1.assertBrowser)(this.web.getPage(), input, this.config.timeout, report);
|
|
88
|
+
}
|
|
89
|
+
if (area === 'network')
|
|
90
|
+
return this.events.network(action, input, report);
|
|
91
|
+
if (area === 'download')
|
|
92
|
+
return this.events.download(action, input, report);
|
|
93
|
+
if (area === 'stub')
|
|
94
|
+
return this.functions.execute(this.web.getPage(), action, input, this.config.timeout, report);
|
|
95
|
+
if (area === 'component')
|
|
96
|
+
return this.components.execute(this.web.getPage(), action, input);
|
|
97
|
+
if (area === 'a11y')
|
|
98
|
+
return (0, accessibility_1.accessibility)(this.web.getPage(), this.config, action, input, report);
|
|
99
|
+
if (area === 'clock') {
|
|
100
|
+
const clock = this.web.getPage().clock;
|
|
101
|
+
if (action === 'install') {
|
|
102
|
+
const data = zod_1.z.object({ time: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional() }).strict().parse(input);
|
|
103
|
+
await clock.install(data);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (action === 'resume') {
|
|
107
|
+
zod_1.z.object({}).strict().parse(input);
|
|
108
|
+
await clock.resume();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (['pauseAt', 'setFixedTime', 'setSystemTime'].includes(action)) {
|
|
112
|
+
const data = zod_1.z.object({ time: zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number().finite()]) }).strict().parse(input);
|
|
113
|
+
if (typeof data.time === 'string' && !Number.isFinite(Date.parse(data.time)))
|
|
114
|
+
throw new Error('Clock time must be a valid date.');
|
|
115
|
+
await clock[action](data.time);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (['runFor', 'fastForward'].includes(action)) {
|
|
119
|
+
const data = zod_1.z.object({ milliseconds: zod_1.z.number().int().min(0).max(86400000) }).strict().parse(input);
|
|
120
|
+
await clock[action](data.milliseconds);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Unknown clock action: ${action}`);
|
|
124
|
+
}
|
|
125
|
+
if (area === 'trace') {
|
|
126
|
+
if (action === 'start') {
|
|
127
|
+
const data = zod_1.z.object({ name: validation_1.name.optional() }).strict().parse(input);
|
|
128
|
+
await this.web.beginTrace(undefined, data.name ? `${data.name}-trace.zip` : undefined);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (action === 'stop') {
|
|
132
|
+
zod_1.z.object({}).strict().parse(input);
|
|
133
|
+
return this.web.endTrace();
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Unknown trace action: ${action}`);
|
|
136
|
+
}
|
|
137
|
+
if (area === 'har') {
|
|
138
|
+
if (action === 'replay') {
|
|
139
|
+
const data = zod_1.z.object({ file: zod_1.z.string().min(1), url: zod_1.z.string().optional(), notFound: zod_1.z.enum(['abort', 'fallback']).default('abort') }).strict().parse(input);
|
|
140
|
+
(0, validation_1.readProjectFile)(this.config, data.file, 100 * 1024 * 1024);
|
|
141
|
+
await this.web.getContext().routeFromHAR((0, projectPaths_1.projectPath)(this.config.projectRoot || process.cwd(), data.file), { url: data.url, notFound: data.notFound, update: false });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Unknown HAR action: ${action}. Enable har in configuration before browser creation to record.`);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unknown native command: flow.${command}`);
|
|
147
|
+
}
|
|
148
|
+
async captureDOM(step) {
|
|
149
|
+
if (!this.config.domSnapshots)
|
|
150
|
+
return;
|
|
151
|
+
const page = this.web.getPage();
|
|
152
|
+
// Saved HTML is diagnostic data. It is never injected into an executable frame.
|
|
153
|
+
const html = await page.content();
|
|
154
|
+
const file = (0, validation_1.evidencePath)(this.config, `${validation_1.name.parse(step)}-${(0, node_crypto_1.randomUUID)()}.dom.html`);
|
|
155
|
+
node_fs_1.default.writeFileSync(file, html, { mode: 0o600 });
|
|
156
|
+
return file;
|
|
157
|
+
}
|
|
158
|
+
async dispose() { this.assertionGeneration++; this.lastAssertion = undefined; await this.events.dispose(); await this.functions.dispose(); await this.components.dispose(); }
|
|
159
|
+
}
|
|
160
|
+
exports.NativeRuntime = NativeRuntime;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { BrowserContextOptions } from 'playwright';
|
|
3
|
+
import type { FlowstrideConfig } from '../types';
|
|
4
|
+
export declare const name: z.ZodString;
|
|
5
|
+
export declare const positive: z.ZodNumber;
|
|
6
|
+
export declare const headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
7
|
+
export declare const contextOptions: z.ZodObject<{
|
|
8
|
+
acceptDownloads: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
strictSelectors: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
clientCertificates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
11
|
+
origin: z.ZodString;
|
|
12
|
+
certPath: z.ZodOptional<z.ZodString>;
|
|
13
|
+
keyPath: z.ZodOptional<z.ZodString>;
|
|
14
|
+
pfxPath: z.ZodOptional<z.ZodString>;
|
|
15
|
+
passphrase: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, z.core.$strict>>>;
|
|
17
|
+
storageState: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
18
|
+
cookies: z.ZodArray<z.ZodObject<{
|
|
19
|
+
name: z.ZodString;
|
|
20
|
+
value: z.ZodString;
|
|
21
|
+
domain: z.ZodString;
|
|
22
|
+
path: z.ZodString;
|
|
23
|
+
expires: z.ZodNumber;
|
|
24
|
+
httpOnly: z.ZodBoolean;
|
|
25
|
+
secure: z.ZodBoolean;
|
|
26
|
+
sameSite: z.ZodEnum<{
|
|
27
|
+
Strict: "Strict";
|
|
28
|
+
Lax: "Lax";
|
|
29
|
+
None: "None";
|
|
30
|
+
}>;
|
|
31
|
+
}, z.core.$strict>>;
|
|
32
|
+
origins: z.ZodArray<z.ZodObject<{
|
|
33
|
+
origin: z.ZodString;
|
|
34
|
+
localStorage: z.ZodArray<z.ZodObject<{
|
|
35
|
+
name: z.ZodString;
|
|
36
|
+
value: z.ZodString;
|
|
37
|
+
}, z.core.$strict>>;
|
|
38
|
+
indexedDB: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
}, z.core.$strict>]>>;
|
|
41
|
+
baseURL: z.ZodOptional<z.ZodString>;
|
|
42
|
+
bypassCSP: z.ZodOptional<z.ZodBoolean>;
|
|
43
|
+
colorScheme: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
44
|
+
light: "light";
|
|
45
|
+
dark: "dark";
|
|
46
|
+
"no-preference": "no-preference";
|
|
47
|
+
}>>>;
|
|
48
|
+
contrast: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
49
|
+
"no-preference": "no-preference";
|
|
50
|
+
more: "more";
|
|
51
|
+
}>>>;
|
|
52
|
+
deviceScaleFactor: z.ZodOptional<z.ZodNumber>;
|
|
53
|
+
extraHTTPHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
54
|
+
forcedColors: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
55
|
+
none: "none";
|
|
56
|
+
active: "active";
|
|
57
|
+
}>>>;
|
|
58
|
+
hasTouch: z.ZodOptional<z.ZodBoolean>;
|
|
59
|
+
httpCredentials: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
username: z.ZodString;
|
|
61
|
+
password: z.ZodString;
|
|
62
|
+
origin: z.ZodOptional<z.ZodString>;
|
|
63
|
+
send: z.ZodOptional<z.ZodEnum<{
|
|
64
|
+
always: "always";
|
|
65
|
+
unauthorized: "unauthorized";
|
|
66
|
+
}>>;
|
|
67
|
+
}, z.core.$strict>>;
|
|
68
|
+
geolocation: z.ZodOptional<z.ZodObject<{
|
|
69
|
+
latitude: z.ZodNumber;
|
|
70
|
+
longitude: z.ZodNumber;
|
|
71
|
+
accuracy: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
}, z.core.$strict>>;
|
|
73
|
+
ignoreHTTPSErrors: z.ZodOptional<z.ZodBoolean>;
|
|
74
|
+
isMobile: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
javaScriptEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
77
|
+
offline: z.ZodOptional<z.ZodBoolean>;
|
|
78
|
+
permissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
79
|
+
proxy: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
server: z.ZodString;
|
|
81
|
+
bypass: z.ZodOptional<z.ZodString>;
|
|
82
|
+
username: z.ZodOptional<z.ZodString>;
|
|
83
|
+
password: z.ZodOptional<z.ZodString>;
|
|
84
|
+
}, z.core.$strict>>;
|
|
85
|
+
reducedMotion: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
86
|
+
reduce: "reduce";
|
|
87
|
+
"no-preference": "no-preference";
|
|
88
|
+
}>>>;
|
|
89
|
+
screen: z.ZodOptional<z.ZodObject<{
|
|
90
|
+
width: z.ZodNumber;
|
|
91
|
+
height: z.ZodNumber;
|
|
92
|
+
}, z.core.$strict>>;
|
|
93
|
+
serviceWorkers: z.ZodOptional<z.ZodEnum<{
|
|
94
|
+
allow: "allow";
|
|
95
|
+
block: "block";
|
|
96
|
+
}>>;
|
|
97
|
+
timezoneId: z.ZodOptional<z.ZodString>;
|
|
98
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
99
|
+
viewport: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
100
|
+
width: z.ZodNumber;
|
|
101
|
+
height: z.ZodNumber;
|
|
102
|
+
}, z.core.$strict>>>;
|
|
103
|
+
}, z.core.$strict>;
|
|
104
|
+
export declare function validateContextOptions(value: unknown, browser?: string, root?: string): BrowserContextOptions;
|
|
105
|
+
export declare function evidencePath(config: FlowstrideConfig, file: string): string;
|
|
106
|
+
export declare function readProjectFile(config: FlowstrideConfig, file: string, maxSize?: number): Buffer;
|
|
107
|
+
export declare function poll(read: () => Promise<boolean>, timeout: number, label: string): Promise<void>;
|
|
@@ -0,0 +1,84 @@
|
|
|
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.contextOptions = exports.headers = exports.positive = exports.name = void 0;
|
|
7
|
+
exports.validateContextOptions = validateContextOptions;
|
|
8
|
+
exports.evidencePath = evidencePath;
|
|
9
|
+
exports.readProjectFile = readProjectFile;
|
|
10
|
+
exports.poll = poll;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const zod_1 = require("zod");
|
|
14
|
+
const projectPaths_1 = require("../utils/projectPaths");
|
|
15
|
+
exports.name = zod_1.z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,79}$/, 'Use a simple name without path separators.');
|
|
16
|
+
exports.positive = zod_1.z.number().int().min(1).max(2147483647);
|
|
17
|
+
exports.headers = zod_1.z.record(zod_1.z.string(), zod_1.z.string());
|
|
18
|
+
const size = zod_1.z.object({ width: exports.positive, height: exports.positive }).strict();
|
|
19
|
+
// File-bearing options are handled explicitly; evidence paths cannot bypass project containment.
|
|
20
|
+
exports.contextOptions = zod_1.z.object({
|
|
21
|
+
acceptDownloads: zod_1.z.boolean().optional(), strictSelectors: zod_1.z.boolean().optional(),
|
|
22
|
+
clientCertificates: zod_1.z.array(zod_1.z.object({ origin: zod_1.z.string().url(), certPath: zod_1.z.string().optional(), keyPath: zod_1.z.string().optional(), pfxPath: zod_1.z.string().optional(), passphrase: zod_1.z.string().optional() }).strict()).optional(),
|
|
23
|
+
storageState: zod_1.z.union([zod_1.z.string(), zod_1.z.object({ cookies: zod_1.z.array(zod_1.z.object({ name: zod_1.z.string(), value: zod_1.z.string(), domain: zod_1.z.string(), path: zod_1.z.string(), expires: zod_1.z.number(), httpOnly: zod_1.z.boolean(), secure: zod_1.z.boolean(), sameSite: zod_1.z.enum(['Strict', 'Lax', 'None']) }).strict()), origins: zod_1.z.array(zod_1.z.object({ origin: zod_1.z.string().url(), localStorage: zod_1.z.array(zod_1.z.object({ name: zod_1.z.string(), value: zod_1.z.string() }).strict()), indexedDB: zod_1.z.array(zod_1.z.unknown()).optional() }).strict()) }).strict()]).optional(),
|
|
24
|
+
baseURL: zod_1.z.string().url().optional(),
|
|
25
|
+
bypassCSP: zod_1.z.boolean().optional(), colorScheme: zod_1.z.enum(['light', 'dark', 'no-preference']).nullable().optional(),
|
|
26
|
+
contrast: zod_1.z.enum(['no-preference', 'more']).nullable().optional(),
|
|
27
|
+
deviceScaleFactor: zod_1.z.number().positive().optional(), extraHTTPHeaders: exports.headers.optional(),
|
|
28
|
+
forcedColors: zod_1.z.enum(['active', 'none']).nullable().optional(), hasTouch: zod_1.z.boolean().optional(),
|
|
29
|
+
httpCredentials: zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string(), origin: zod_1.z.string().url().optional(), send: zod_1.z.enum(['always', 'unauthorized']).optional() }).strict().optional(),
|
|
30
|
+
geolocation: zod_1.z.object({ latitude: zod_1.z.number().min(-90).max(90), longitude: zod_1.z.number().min(-180).max(180), accuracy: zod_1.z.number().nonnegative().optional() }).strict().optional(),
|
|
31
|
+
ignoreHTTPSErrors: zod_1.z.boolean().optional(), isMobile: zod_1.z.boolean().optional(), javaScriptEnabled: zod_1.z.boolean().optional(),
|
|
32
|
+
locale: zod_1.z.string().min(1).optional(), offline: zod_1.z.boolean().optional(), permissions: zod_1.z.array(zod_1.z.string()).optional(),
|
|
33
|
+
proxy: zod_1.z.object({ server: zod_1.z.string().min(1), bypass: zod_1.z.string().optional(), username: zod_1.z.string().optional(), password: zod_1.z.string().optional() }).strict().optional(),
|
|
34
|
+
reducedMotion: zod_1.z.enum(['reduce', 'no-preference']).nullable().optional(), screen: size.optional(),
|
|
35
|
+
serviceWorkers: zod_1.z.enum(['allow', 'block']).optional(), timezoneId: zod_1.z.string().optional(),
|
|
36
|
+
userAgent: zod_1.z.string().optional(), viewport: size.nullable().optional(),
|
|
37
|
+
}).strict();
|
|
38
|
+
function validateContextOptions(value, browser = 'chromium', root = process.cwd()) {
|
|
39
|
+
const parsed = exports.contextOptions.parse(value ?? {});
|
|
40
|
+
if (typeof parsed.storageState === 'string')
|
|
41
|
+
parsed.storageState = (0, projectPaths_1.projectPath)(root, parsed.storageState);
|
|
42
|
+
for (const certificate of parsed.clientCertificates || [])
|
|
43
|
+
for (const key of ['certPath', 'keyPath', 'pfxPath'])
|
|
44
|
+
if (certificate[key])
|
|
45
|
+
certificate[key] = (0, projectPaths_1.projectPath)(root, certificate[key]);
|
|
46
|
+
if (browser === 'firefox' && parsed.isMobile)
|
|
47
|
+
throw new Error('Firefox does not support mobile emulation; use viewport and touch options instead.');
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
function evidencePath(config, file) {
|
|
51
|
+
const safe = exports.name.parse(file);
|
|
52
|
+
const root = config.projectRoot || process.cwd();
|
|
53
|
+
const dir = (0, projectPaths_1.projectPath)(root, config.artifactsDir || 'flowstride-artifacts');
|
|
54
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
55
|
+
return (0, projectPaths_1.projectPath)(root, node_path_1.default.join(dir, (0, projectPaths_1.artifactPrefix)(config.testFile) + safe));
|
|
56
|
+
}
|
|
57
|
+
function readProjectFile(config, file, maxSize = 20 * 1024 * 1024) {
|
|
58
|
+
const absolute = (0, projectPaths_1.projectPath)(config.projectRoot || process.cwd(), file);
|
|
59
|
+
if (!node_fs_1.default.statSync(absolute).isFile() || node_fs_1.default.statSync(absolute).size > maxSize)
|
|
60
|
+
throw new Error('Fixture must be a regular project file within the size limit.');
|
|
61
|
+
return node_fs_1.default.readFileSync(absolute);
|
|
62
|
+
}
|
|
63
|
+
async function poll(read, timeout, label) {
|
|
64
|
+
const deadline = Date.now() + exports.positive.parse(timeout);
|
|
65
|
+
let last;
|
|
66
|
+
for (;;) {
|
|
67
|
+
const remaining = deadline - Date.now();
|
|
68
|
+
if (remaining <= 0)
|
|
69
|
+
throw new Error(`Assertion timed out: ${label}`, { cause: last });
|
|
70
|
+
let timer;
|
|
71
|
+
try {
|
|
72
|
+
const ok = await Promise.race([read(), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Read timed out')), remaining); })]);
|
|
73
|
+
if (ok)
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
last = error;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -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/lexer.js
CHANGED
|
@@ -53,11 +53,22 @@ class Lexer {
|
|
|
53
53
|
let jsonStr = "";
|
|
54
54
|
let depth = 0;
|
|
55
55
|
let startLine = this.line;
|
|
56
|
+
let inString = false, escaped = false;
|
|
56
57
|
while (this.position < this.input.length) {
|
|
57
58
|
const c = this.input[this.position];
|
|
58
|
-
if (
|
|
59
|
+
if (inString) {
|
|
60
|
+
if (escaped)
|
|
61
|
+
escaped = false;
|
|
62
|
+
else if (c === "\\")
|
|
63
|
+
escaped = true;
|
|
64
|
+
else if (c === '"')
|
|
65
|
+
inString = false;
|
|
66
|
+
}
|
|
67
|
+
else if (c === '"')
|
|
68
|
+
inString = true;
|
|
69
|
+
else if (c === "{")
|
|
59
70
|
depth++;
|
|
60
|
-
if (c === "}")
|
|
71
|
+
else if (c === "}")
|
|
61
72
|
depth--;
|
|
62
73
|
if (c === "\n")
|
|
63
74
|
this.line++;
|
|
@@ -217,7 +228,7 @@ class Lexer {
|
|
|
217
228
|
});
|
|
218
229
|
continue;
|
|
219
230
|
}
|
|
220
|
-
else if (ident.includes(".") && !lowerIdent.startsWith("flow.")) {
|
|
231
|
+
else if (/^flow\.(browser|assert|network|download|clock|stub|trace|har|a11y|component)\.[a-z]+$/i.test(ident) || (ident.includes(".") && !lowerIdent.startsWith("flow."))) {
|
|
221
232
|
tokens.push({
|
|
222
233
|
type: TokenType.ALIAS_COMMAND,
|
|
223
234
|
value: ident,
|
|
@@ -392,7 +403,7 @@ class Lexer {
|
|
|
392
403
|
line: this.line,
|
|
393
404
|
});
|
|
394
405
|
}
|
|
395
|
-
else if (ident.includes(".") && !lowerIdent.startsWith("flow.")) {
|
|
406
|
+
else if (/^flow\.(browser|assert|network|download|clock|stub|trace|har|a11y|component)\.[a-z]+$/i.test(ident) || (ident.includes(".") && !lowerIdent.startsWith("flow."))) {
|
|
396
407
|
tokens.push({
|
|
397
408
|
type: TokenType.ALIAS_COMMAND,
|
|
398
409
|
value: ident,
|
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);
|
|
@@ -242,7 +242,7 @@ class Parser {
|
|
|
242
242
|
return {
|
|
243
243
|
id: `scenario_${this.scenarioCounter}_S${this.stepCounter++}`,
|
|
244
244
|
blockDescription: this.currentBlockDescription,
|
|
245
|
-
action: "plugin",
|
|
245
|
+
action: alias === "flow" ? "native" : "plugin",
|
|
246
246
|
payload: {
|
|
247
247
|
pluginAlias: alias,
|
|
248
248
|
pluginMethod: method,
|
|
@@ -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;
|