flowstride 1.0.20-rc.6 → 1.0.21
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 +341 -0
- package/NATIVE_CI.md +88 -169
- package/README.md +46 -214
- package/dist/adapters/api/undici.d.ts +8 -0
- package/dist/adapters/api/undici.js +11 -0
- package/dist/adapters/web/playwright.d.ts +25 -2
- package/dist/adapters/web/playwright.js +285 -21
- 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 +24 -5
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/index.js +24 -5
- package/dist/manual/security.d.ts +2 -2
- package/dist/native/accessibility.d.ts +3 -0
- package/dist/native/accessibility.js +68 -0
- package/dist/native/assertions.d.ts +2 -0
- package/dist/native/assertions.js +140 -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/events.d.ts +34 -0
- package/dist/native/events.js +281 -0
- package/dist/native/evidence.d.ts +3 -0
- package/dist/native/evidence.js +31 -0
- package/dist/native/functions.d.ts +9 -0
- package/dist/native/functions.js +103 -0
- package/dist/native/runtime.d.ts +14 -0
- package/dist/native/runtime.js +154 -0
- package/dist/native/validation.d.ts +107 -0
- package/dist/native/validation.js +84 -0
- package/dist/parser/lexer.js +15 -4
- package/dist/parser/parser.js +1 -1
- package/dist/runner/flow_orchestrator.d.ts +4 -0
- package/dist/runner/flow_orchestrator.js +22 -5
- package/dist/runner/flow_worker.d.ts +7 -0
- package/dist/runner/flow_worker.js +141 -37
- package/dist/security/redaction.js +3 -1
- package/dist/sessions/manager.js +3 -1
- package/dist/types/index.d.ts +12 -0
- package/dist/utils/replayRequest.d.ts +8 -0
- package/dist/utils/replayRequest.js +77 -0
- package/package.json +22 -9
- package/src/ui/dist/assets/index-DglsL_J2.js +636 -0
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/index-DVwp0K-J.js +0 -462
|
@@ -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
|
+
}
|
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
|
@@ -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,
|
|
@@ -4,6 +4,7 @@ export declare class FlowOrchestrator {
|
|
|
4
4
|
private worker;
|
|
5
5
|
private workerCount;
|
|
6
6
|
private config?;
|
|
7
|
+
static readonly nativeBrowserProtocol = 2;
|
|
7
8
|
static readonly nativeCIProtocol = 1;
|
|
8
9
|
readonly nativeEventProtocol = 1;
|
|
9
10
|
private wss;
|
|
@@ -12,6 +13,9 @@ export declare class FlowOrchestrator {
|
|
|
12
13
|
private stepStatusRegistry;
|
|
13
14
|
private pdfGenerator;
|
|
14
15
|
private disconnectTimer;
|
|
16
|
+
private dashboardBrowser;
|
|
17
|
+
private closing;
|
|
18
|
+
private browserInfoByFile;
|
|
15
19
|
constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
|
|
16
20
|
private forwardToDashboard;
|
|
17
21
|
private executeTestRun;
|
|
@@ -37,8 +37,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.FlowOrchestrator = void 0;
|
|
40
|
+
const dashboard_1 = require("../native/dashboard");
|
|
41
|
+
const evidence_1 = require("../native/evidence");
|
|
42
|
+
const distribution_1 = require("../ci/distribution");
|
|
40
43
|
const localDashboard_1 = require("../security/localDashboard");
|
|
41
|
-
const projectPaths_1 = require("../utils/projectPaths");
|
|
42
44
|
const fs = __importStar(require("fs"));
|
|
43
45
|
const path = __importStar(require("path"));
|
|
44
46
|
const http = __importStar(require("http"));
|
|
@@ -50,6 +52,7 @@ class FlowOrchestrator {
|
|
|
50
52
|
worker;
|
|
51
53
|
workerCount;
|
|
52
54
|
config;
|
|
55
|
+
static nativeBrowserProtocol = 2;
|
|
53
56
|
static nativeCIProtocol = 1;
|
|
54
57
|
nativeEventProtocol = 1;
|
|
55
58
|
wss = null;
|
|
@@ -58,6 +61,9 @@ class FlowOrchestrator {
|
|
|
58
61
|
stepStatusRegistry = new Map();
|
|
59
62
|
pdfGenerator;
|
|
60
63
|
disconnectTimer = null;
|
|
64
|
+
dashboardBrowser = new dashboard_1.DashboardBrowser();
|
|
65
|
+
closing = false;
|
|
66
|
+
browserInfoByFile = new Map();
|
|
61
67
|
constructor(worker, workerCount = 1, config) {
|
|
62
68
|
this.worker = worker;
|
|
63
69
|
this.workerCount = workerCount;
|
|
@@ -68,6 +74,10 @@ class FlowOrchestrator {
|
|
|
68
74
|
});
|
|
69
75
|
}
|
|
70
76
|
forwardToDashboard(type, payload) {
|
|
77
|
+
if (type === 'BROWSER_INFO')
|
|
78
|
+
this.browserInfoByFile.set(payload.fileName, payload);
|
|
79
|
+
if (type === 'SUITE_START')
|
|
80
|
+
this.browserInfoByFile.clear();
|
|
71
81
|
this.config?.onRunEvent?.({ type, payload });
|
|
72
82
|
if (this.wss) {
|
|
73
83
|
this.wss.clients.forEach((client) => {
|
|
@@ -78,7 +88,7 @@ class FlowOrchestrator {
|
|
|
78
88
|
}
|
|
79
89
|
async executeTestRun(targetPath) {
|
|
80
90
|
this.forwardToDashboard("SUITE_START", {});
|
|
81
|
-
const filesToProcess = (0,
|
|
91
|
+
const filesToProcess = (0, distribution_1.executionPlan)(targetPath, this.config).files;
|
|
82
92
|
if (this.config)
|
|
83
93
|
this.config.flowRoot = fs.statSync(targetPath).isDirectory() ? targetPath : path.dirname(targetPath);
|
|
84
94
|
this.globalTestScenarios = [];
|
|
@@ -115,6 +125,8 @@ class FlowOrchestrator {
|
|
|
115
125
|
return;
|
|
116
126
|
}
|
|
117
127
|
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
128
|
+
if ((0, evidence_1.serveDOMSnapshot)(req, res, this.config))
|
|
129
|
+
return;
|
|
118
130
|
if (req.url && req.url.startsWith("/videos/")) {
|
|
119
131
|
let urlObj;
|
|
120
132
|
try {
|
|
@@ -229,6 +241,8 @@ class FlowOrchestrator {
|
|
|
229
241
|
});
|
|
230
242
|
this.wss = new ws_1.WebSocketServer({ server: this.httpServer, verifyClient: (info) => Boolean(info.origin) && (0, localDashboard_1.isLocalDashboardRequest)(info.req.headers.host, info.origin) });
|
|
231
243
|
this.wss.on("connection", (ws) => {
|
|
244
|
+
for (const payload of this.browserInfoByFile.values())
|
|
245
|
+
ws.send(JSON.stringify({ type: 'BROWSER_INFO', payload }));
|
|
232
246
|
if (this.disconnectTimer) {
|
|
233
247
|
clearTimeout(this.disconnectTimer);
|
|
234
248
|
this.disconnectTimer = null;
|
|
@@ -267,7 +281,7 @@ class FlowOrchestrator {
|
|
|
267
281
|
}
|
|
268
282
|
});
|
|
269
283
|
ws.on("close", () => {
|
|
270
|
-
if (this.wss && this.wss.clients.size === 0) {
|
|
284
|
+
if (!this.closing && this.wss && this.wss.clients.size === 0) {
|
|
271
285
|
this.disconnectTimer = setTimeout(async () => {
|
|
272
286
|
console.log(chalk_1.default.yellow("\n[Flowstride]: Dashboard disconnected. Shutting down framework..."));
|
|
273
287
|
this.worker.abort();
|
|
@@ -300,8 +314,8 @@ class FlowOrchestrator {
|
|
|
300
314
|
!!process.env.GITHUB_ACTIONS ||
|
|
301
315
|
!!process.env.FLOWSTRIDE_API_KEY;
|
|
302
316
|
if (!isStrictHeadless) {
|
|
303
|
-
|
|
304
|
-
|
|
317
|
+
await this.dashboardBrowser.open(localUrl, this.config?.browser);
|
|
318
|
+
console.log(`[Dashboard]: ${this.config?.browser === 'firefox' || this.config?.browser === 'webkit' ? this.config.browser : 'system default'} dashboard window; test browser reported after launch${this.config?.headless ? ' (headless tests)' : ''}.`);
|
|
305
319
|
console.log(chalk_1.default.yellow("[Dashboard]: Waiting for UI connection..."));
|
|
306
320
|
while (this.wss.clients.size === 0)
|
|
307
321
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -314,6 +328,7 @@ class FlowOrchestrator {
|
|
|
314
328
|
resolve(result);
|
|
315
329
|
}
|
|
316
330
|
catch (err) {
|
|
331
|
+
await this.dashboardBrowser.close();
|
|
317
332
|
reject(err);
|
|
318
333
|
}
|
|
319
334
|
});
|
|
@@ -327,6 +342,8 @@ class FlowOrchestrator {
|
|
|
327
342
|
this.worker.abort();
|
|
328
343
|
}
|
|
329
344
|
async close() {
|
|
345
|
+
this.closing = true;
|
|
346
|
+
await this.dashboardBrowser.close();
|
|
330
347
|
if (this.disconnectTimer)
|
|
331
348
|
clearTimeout(this.disconnectTimer);
|
|
332
349
|
this.wss?.clients.forEach(client => client.terminate());
|
|
@@ -19,10 +19,16 @@ export declare class FlowWorker extends EventEmitter {
|
|
|
19
19
|
private store;
|
|
20
20
|
private sessions;
|
|
21
21
|
private pluginManager?;
|
|
22
|
+
static readonly nativeBrowserProtocol = 2;
|
|
22
23
|
private isSilent;
|
|
23
24
|
private isAborted;
|
|
24
25
|
private bailTriggered;
|
|
25
26
|
private isDebugHalted;
|
|
27
|
+
private native;
|
|
28
|
+
private attempt;
|
|
29
|
+
private maxAttempts;
|
|
30
|
+
private fallbackStream?;
|
|
31
|
+
private interceptedContexts;
|
|
26
32
|
private currentStepContext;
|
|
27
33
|
private currentFileName;
|
|
28
34
|
private activeAudioPath;
|
|
@@ -46,6 +52,7 @@ export declare class FlowWorker extends EventEmitter {
|
|
|
46
52
|
handleReplayRequest(payload: any): Promise<void>;
|
|
47
53
|
abort(): void;
|
|
48
54
|
runBatch(filesToProcess: string[]): Promise<void>;
|
|
55
|
+
private runScenarioWithRetries;
|
|
49
56
|
private runScenario;
|
|
50
57
|
private waitForPipeline;
|
|
51
58
|
private executeStep;
|