flowstride 1.0.18 → 1.0.20-rc.2

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.
Files changed (41) hide show
  1. package/NATIVE_CI.md +155 -0
  2. package/README.md +40 -33
  3. package/RELEASE_NOTES.md +40 -0
  4. package/dist/adapters/web/playwright.d.ts +4 -0
  5. package/dist/adapters/web/playwright.js +85 -38
  6. package/dist/ci/config.d.ts +3 -0
  7. package/dist/ci/config.js +76 -0
  8. package/dist/ci/deadline.d.ts +2 -0
  9. package/dist/ci/deadline.js +16 -0
  10. package/dist/ci/init.d.ts +1 -0
  11. package/dist/ci/init.js +21 -0
  12. package/dist/ci/runtime.d.ts +19 -0
  13. package/dist/ci/runtime.js +190 -0
  14. package/dist/cli/commands/open.d.ts +1 -0
  15. package/dist/cli/commands/open.js +12 -0
  16. package/dist/cli/commands/run.js +15 -34
  17. package/dist/cli/index.js +51 -5
  18. package/dist/cli/manual_server/workspace_server.d.ts +1 -0
  19. package/dist/cli/manual_server/workspace_server.js +751 -0
  20. package/dist/game/bridge.d.ts +13 -0
  21. package/dist/game/bridge.js +15 -0
  22. package/dist/game/sdk.d.ts +27 -0
  23. package/dist/game/sdk.js +255 -0
  24. package/dist/game/three-bridge.d.ts +18 -0
  25. package/dist/game/three-bridge.js +59 -0
  26. package/dist/parser/lexer.js +4 -0
  27. package/dist/parser/parser.js +19 -3
  28. package/dist/runner/flow_orchestrator.d.ts +5 -1
  29. package/dist/runner/flow_orchestrator.js +31 -7
  30. package/dist/runner/flow_worker.js +57 -13
  31. package/dist/runner/plugin_manager.d.ts +1 -0
  32. package/dist/runner/plugin_manager.js +33 -1
  33. package/dist/types/index.d.ts +36 -0
  34. package/examples/native-game/flows/canvas.flow +13 -0
  35. package/examples/native-game/flowstride.config.json +12 -0
  36. package/examples/native-game/server.mjs +12 -0
  37. package/package.json +14 -5
  38. package/src/ui/dist/assets/index-DIC22ZK_.js +638 -0
  39. package/src/ui/dist/assets/workspace_engine_bg-BVlpAZ1H.wasm +0 -0
  40. package/src/ui/dist/index.html +1 -1
  41. package/src/ui/dist/assets/index-BCdexgt5.js +0 -270
@@ -0,0 +1,13 @@
1
+ /** Register observable state and explicitly test-only fixtures in a browser game. */
2
+ export interface GameBridgeOptions {
3
+ readState?: () => unknown;
4
+ locate?: (id: string) => {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ testMode?: boolean;
9
+ fixtures?: Record<string, (data: unknown) => unknown>;
10
+ }
11
+ export declare function registerGame(options: GameBridgeOptions): {
12
+ dispose(): void;
13
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerGame = registerGame;
4
+ function registerGame(options) {
5
+ if (typeof window === 'undefined')
6
+ throw new Error('Game bridges must be registered in the browser.');
7
+ if (typeof options.readState !== 'function' && typeof options.locate !== 'function')
8
+ throw new Error('A game bridge requires readState or locate.');
9
+ const bridge = { readState: options.readState, locate: options.locate,
10
+ testMode: options.testMode === true,
11
+ fixtures: options.testMode === true ? options.fixtures || {} : {} };
12
+ window.__FLOWSTRIDE_GAME__ = bridge;
13
+ return { dispose() { if (window.__FLOWSTRIDE_GAME__ === bridge)
14
+ delete window.__FLOWSTRIDE_GAME__; } };
15
+ }
@@ -0,0 +1,27 @@
1
+ import type { Page } from 'playwright';
2
+ import type { FlowstrideConfig } from '../types';
3
+ export declare class GameSDK {
4
+ private page;
5
+ private config;
6
+ private snapshots;
7
+ private states;
8
+ constructor(page: () => Page, config: FlowstrideConfig);
9
+ private timeout;
10
+ private bridgeReady;
11
+ private point;
12
+ private touch;
13
+ tap(options: any): Promise<void>;
14
+ drag(options: any): Promise<void>;
15
+ swipe(options: any): Promise<void>;
16
+ key(options: any): Promise<void>;
17
+ checkpoint(options: any): Promise<string>;
18
+ snapshot(options: any): Promise<void>;
19
+ private difference;
20
+ expectVisual(options: any): Promise<number>;
21
+ state(options?: any): Promise<any>;
22
+ expectState(options: any): Promise<void>;
23
+ snapshotState(options: any): Promise<void>;
24
+ expectPreserved(options: any): Promise<void>;
25
+ fixture(options: any): Promise<any>;
26
+ select(options: any): Promise<void>;
27
+ }
@@ -0,0 +1,255 @@
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.GameSDK = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const strict_1 = __importDefault(require("node:assert/strict"));
10
+ const promises_1 = require("node:timers/promises");
11
+ class GameSDK {
12
+ page;
13
+ config;
14
+ snapshots = new Map();
15
+ states = new Map();
16
+ constructor(page, config) {
17
+ this.page = page;
18
+ this.config = config;
19
+ }
20
+ timeout() { return this.config.timeout; }
21
+ async bridgeReady(method) {
22
+ await this.page().waitForFunction(method => {
23
+ const bridge = window.__FLOWSTRIDE_GAME__;
24
+ return !!bridge && (!method || typeof bridge[method] === 'function');
25
+ }, method, { timeout: this.timeout() });
26
+ }
27
+ async point(options) {
28
+ const canvas = this.page().locator(options.canvas || 'canvas');
29
+ await canvas.waitFor({ state: 'visible', timeout: this.timeout() });
30
+ const box = await canvas.boundingBox();
31
+ if (!box || box.width <= 0 || box.height <= 0)
32
+ throw new Error('Canvas has no usable bounds.');
33
+ let { x, y } = options;
34
+ if (!Number.isFinite(x) || !Number.isFinite(y))
35
+ throw new Error('Canvas coordinates must be finite.');
36
+ if ((options.unit || 'relative') === 'relative') {
37
+ x *= box.width;
38
+ y *= box.height;
39
+ }
40
+ else if (options.unit !== 'pixels')
41
+ throw new Error('Coordinate unit must be relative or pixels.');
42
+ if (x < 0 || x >= box.width || y < 0 || y >= box.height)
43
+ throw new Error('Canvas coordinate is outside its bounds.');
44
+ const point = { x: box.x + x, y: box.y + y };
45
+ await canvas.evaluate((element, point) => {
46
+ if (document.elementFromPoint(point.x, point.y) !== element)
47
+ throw new Error('Canvas target is covered or outside the viewport.');
48
+ }, point);
49
+ return point;
50
+ }
51
+ touch(options) {
52
+ const input = options.input || (this.config.device === 'desktop' ? 'mouse' : 'touch');
53
+ if (!['mouse', 'touch'].includes(input))
54
+ throw new Error('input must be mouse or touch.');
55
+ return input === 'touch';
56
+ }
57
+ async tap(options) {
58
+ const point = await this.point(options);
59
+ if (this.touch(options))
60
+ await this.page().touchscreen.tap(point.x, point.y);
61
+ else
62
+ await this.page().mouse.click(point.x, point.y);
63
+ }
64
+ async drag(options) {
65
+ const from = await this.point({ ...options, ...options.from });
66
+ const to = await this.point({ ...options, ...options.to });
67
+ const duration = options.duration ?? 250;
68
+ if (!Number.isFinite(duration) || duration < 0 || duration > this.timeout())
69
+ throw new Error('Invalid gesture duration.');
70
+ if (this.touch(options)) {
71
+ const session = await this.page().context().newCDPSession(this.page());
72
+ try {
73
+ await session.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [from] });
74
+ for (let i = 1; i <= 10; i++) {
75
+ await session.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: from.x + (to.x - from.x) * i / 10, y: from.y + (to.y - from.y) * i / 10 }] });
76
+ await (0, promises_1.setTimeout)(duration / 10);
77
+ }
78
+ }
79
+ finally {
80
+ try {
81
+ await session.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
82
+ }
83
+ finally {
84
+ await session.detach();
85
+ }
86
+ }
87
+ }
88
+ else {
89
+ await this.page().mouse.move(from.x, from.y);
90
+ await this.page().mouse.down();
91
+ try {
92
+ for (let i = 1; i <= 10; i++) {
93
+ await this.page().mouse.move(from.x + (to.x - from.x) * i / 10, from.y + (to.y - from.y) * i / 10);
94
+ await (0, promises_1.setTimeout)(duration / 10);
95
+ }
96
+ }
97
+ finally {
98
+ await this.page().mouse.up();
99
+ }
100
+ }
101
+ }
102
+ async swipe(options) { return this.drag({ ...options, input: 'touch' }); }
103
+ async key(options) {
104
+ const key = typeof options === 'string' ? options : options.key;
105
+ const duration = typeof options === 'string' ? 0 : options.duration ?? 0;
106
+ if (typeof key !== 'string' || !key || !Number.isFinite(duration) || duration < 0 || duration > this.timeout())
107
+ throw new Error('Invalid key or duration.');
108
+ const keys = key === '+' ? [key] : key.split('+');
109
+ const pressed = [];
110
+ try {
111
+ for (const part of keys) {
112
+ await this.page().keyboard.down(part);
113
+ pressed.push(part);
114
+ }
115
+ if (duration)
116
+ await (0, promises_1.setTimeout)(duration);
117
+ }
118
+ finally {
119
+ const released = await Promise.allSettled(pressed.reverse().map(part => this.page().keyboard.up(part)));
120
+ if (released.some(result => result.status === 'rejected'))
121
+ throw new Error('Failed to release game keyboard input.');
122
+ }
123
+ }
124
+ async checkpoint(options) {
125
+ const name = typeof options === 'string' ? options : options.name;
126
+ if (typeof name !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(name))
127
+ throw new Error('Checkpoint name must use letters, digits, underscores or hyphens.');
128
+ const directory = this.config.artifactsDir || node_path_1.default.resolve('flowstride-artifacts');
129
+ node_fs_1.default.mkdirSync(directory, { recursive: true });
130
+ const prefix = this.config.testFile ? this.config.testFile.replace(/[^a-zA-Z0-9_.-]/g, '_') + '-' : '';
131
+ const file = node_path_1.default.join(directory, prefix + name + '.png');
132
+ if (typeof options === 'object' && options.canvas)
133
+ await this.page().locator(options.canvas).screenshot({ path: file });
134
+ else
135
+ await this.page().screenshot({ path: file });
136
+ return file;
137
+ }
138
+ async snapshot(options) {
139
+ if (!options?.name || typeof options.name !== 'string')
140
+ throw new Error('snapshot requires a name.');
141
+ const buffer = await this.page().locator(options.canvas || 'canvas').screenshot();
142
+ this.snapshots.set(options.name, buffer);
143
+ }
144
+ async difference(before, after) {
145
+ return this.page().evaluate(async ({ before, after }) => {
146
+ async function pixels(data) {
147
+ const image = new Image();
148
+ image.src = 'data:image/png;base64,' + data;
149
+ await image.decode();
150
+ const canvas = document.createElement('canvas');
151
+ canvas.width = image.width;
152
+ canvas.height = image.height;
153
+ const context = canvas.getContext('2d');
154
+ context.drawImage(image, 0, 0);
155
+ return { data: context.getImageData(0, 0, canvas.width, canvas.height).data, width: canvas.width, height: canvas.height };
156
+ }
157
+ const a = await pixels(before), b = await pixels(after);
158
+ if (a.width !== b.width || a.height !== b.height)
159
+ throw new Error('Visual comparison dimensions differ.');
160
+ let changed = 0;
161
+ for (let i = 0; i < a.data.length; i += 4)
162
+ if (Math.abs(a.data[i] - b.data[i]) + Math.abs(a.data[i + 1] - b.data[i + 1]) + Math.abs(a.data[i + 2] - b.data[i + 2]) + Math.abs(a.data[i + 3] - b.data[i + 3]) > 60)
163
+ changed++;
164
+ return changed / (a.width * a.height);
165
+ }, { before: before.toString('base64'), after: after.toString('base64') });
166
+ }
167
+ async expectVisual(options) {
168
+ let before;
169
+ if (options.baseline)
170
+ before = node_fs_1.default.readFileSync(node_path_1.default.resolve(options.baseline));
171
+ else
172
+ before = this.snapshots.get(options.name);
173
+ if (!before)
174
+ throw new Error('Visual snapshot or baseline is missing.');
175
+ const ratio = await this.difference(before, await this.page().locator(options.canvas || 'canvas').screenshot());
176
+ const min = options.minChange ?? 0, max = options.maxChange ?? (options.minChange === undefined ? 0 : 1);
177
+ if (![min, max].every(Number.isFinite) || min < 0 || max > 1 || min > max)
178
+ throw new Error('Visual change thresholds must satisfy 0 <= min <= max <= 1.');
179
+ (0, strict_1.default)(ratio >= min && ratio <= max, `Canvas changed by ${ratio}; expected between ${min} and ${max}.`);
180
+ return ratio;
181
+ }
182
+ async state(options = {}) {
183
+ await this.bridgeReady('readState');
184
+ return this.page().evaluate(async (field) => {
185
+ const bridge = window.__FLOWSTRIDE_GAME__;
186
+ if (!bridge?.readState)
187
+ throw new Error('Game state bridge is not registered.');
188
+ let value = await bridge.readState();
189
+ for (const part of field?.split('.') || []) {
190
+ if (['__proto__', 'constructor', 'prototype'].includes(part) || value == null || !Object.hasOwn(value, part))
191
+ throw new Error(`Game state field is unavailable: ${field}`);
192
+ value = value[part];
193
+ }
194
+ return value;
195
+ }, options.path);
196
+ }
197
+ async expectState(options) {
198
+ if (!options || !Object.hasOwn(options, 'equals'))
199
+ throw new Error('expectState requires equals.');
200
+ const deadline = Date.now() + this.timeout();
201
+ let actual;
202
+ let observed = false;
203
+ do {
204
+ try {
205
+ actual = await this.state(options);
206
+ observed = true;
207
+ strict_1.default.deepStrictEqual(actual, options.equals);
208
+ return;
209
+ }
210
+ catch { }
211
+ await (0, promises_1.setTimeout)(50);
212
+ } while (Date.now() < deadline);
213
+ if (!observed)
214
+ throw new Error('Game state was not readable before the timeout.');
215
+ strict_1.default.deepStrictEqual(actual, options.equals, `Game state did not reach the expected value: ${options.path || 'root'}`);
216
+ }
217
+ async snapshotState(options) {
218
+ if (!options?.name)
219
+ throw new Error('snapshotState requires name.');
220
+ this.states.set(options.name, { path: options.path, value: await this.state(options) });
221
+ }
222
+ async expectPreserved(options) {
223
+ if (!this.states.has(options.name))
224
+ throw new Error('State snapshot missing.');
225
+ const snapshot = this.states.get(options.name);
226
+ strict_1.default.deepStrictEqual(await this.state({ path: options.path ?? snapshot.path }), snapshot.value, 'Game state changed.');
227
+ }
228
+ async fixture(options) {
229
+ await this.bridgeReady();
230
+ return this.page().evaluate(async ({ name, data }) => {
231
+ const bridge = window.__FLOWSTRIDE_GAME__;
232
+ if (!bridge?.testMode || !Object.hasOwn(bridge.fixtures || {}, name) || typeof bridge.fixtures[name] !== 'function')
233
+ throw new Error('Named fixture is unavailable in this environment.');
234
+ return bridge.fixtures[name](data);
235
+ }, { name: options.name, data: options.data });
236
+ }
237
+ async select(options) {
238
+ if (typeof options?.id !== 'string' || !options.id.trim())
239
+ throw new Error('select requires a nonempty game object ID.');
240
+ await this.bridgeReady('locate');
241
+ const point = await this.page().evaluate(async (id) => {
242
+ const bridge = window.__FLOWSTRIDE_GAME__;
243
+ if (!bridge?.locate)
244
+ throw new Error('Game object bridge is not registered.');
245
+ return bridge.locate(id);
246
+ }, options.id);
247
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y))
248
+ throw new Error('Invalid object coordinates.');
249
+ if (this.touch(options))
250
+ await this.page().touchscreen.tap(point.x, point.y);
251
+ else
252
+ await this.page().mouse.click(point.x, point.y);
253
+ }
254
+ }
255
+ exports.GameSDK = GameSDK;
@@ -0,0 +1,18 @@
1
+ /** Browser-side integration. Import from flowstride/dist/game/three-bridge.
2
+ * The application supplies its own Three.js constructors; no second Three.js
3
+ * runtime, scene reconstruction, renderer or animation loop is created. */
4
+ export interface ThreeGameBridgeOptions {
5
+ scene: any;
6
+ camera: any;
7
+ canvas: HTMLCanvasElement;
8
+ Vector3: new (...args: any[]) => any;
9
+ Raycaster: new (...args: any[]) => any;
10
+ objects?: Record<string, any>;
11
+ readState?: () => unknown;
12
+ testMode?: boolean;
13
+ fixtures?: Record<string, (data: unknown) => unknown>;
14
+ }
15
+ export declare function registerThreeGame(options: ThreeGameBridgeOptions): {
16
+ registerObject(id: string, object: any): void;
17
+ dispose(): void;
18
+ };
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerThreeGame = registerThreeGame;
4
+ function registerThreeGame(options) {
5
+ if (!(options.canvas instanceof HTMLCanvasElement))
6
+ throw new Error('A live game canvas is required.');
7
+ const objects = new Map(Object.entries(options.objects || {}));
8
+ const locate = (id) => {
9
+ let object = objects.get(id);
10
+ if (!object)
11
+ options.scene.traverse((candidate) => { if (candidate.userData?.flowstrideId === id) {
12
+ if (object)
13
+ throw new Error(`Duplicate game object ID: ${id}`);
14
+ object = candidate;
15
+ } });
16
+ if (!object)
17
+ throw new Error(`Game object not found: ${id}`);
18
+ let cursor = object;
19
+ while (cursor) {
20
+ if (cursor.visible === false)
21
+ throw new Error(`Game object is hidden: ${id}`);
22
+ cursor = cursor.parent;
23
+ }
24
+ options.scene.updateMatrixWorld(true);
25
+ options.camera.updateMatrixWorld(true);
26
+ options.camera.updateProjectionMatrix();
27
+ const local = object.userData?.flowstridePickPoint;
28
+ const point = local ? object.localToWorld(new options.Vector3(...local)) : object.getWorldPosition(new options.Vector3());
29
+ point.project(options.camera);
30
+ if (![point.x, point.y, point.z].every(Number.isFinite) || Math.abs(point.x) > 1 || Math.abs(point.y) > 1 || Math.abs(point.z) > 1)
31
+ throw new Error(`Game object is outside the camera: ${id}`);
32
+ const ray = new options.Raycaster();
33
+ ray.layers.mask = options.camera.layers.mask;
34
+ ray.setFromCamera({ x: point.x, y: point.y }, options.camera);
35
+ const hits = ray.intersectObjects(options.scene.children, true);
36
+ const front = hits.find((hit) => { let node = hit.object; while (node) {
37
+ if (node.visible === false)
38
+ return false;
39
+ node = node.parent;
40
+ } return true; });
41
+ cursor = front?.object;
42
+ while (cursor && cursor !== object)
43
+ cursor = cursor.parent;
44
+ if (!cursor)
45
+ throw new Error(`Game object is occluded or not pickable: ${id}`);
46
+ const rect = options.canvas.getBoundingClientRect();
47
+ const x = rect.left + (point.x + 1) * rect.width / 2, y = rect.top + (1 - point.y) * rect.height / 2;
48
+ if (!rect.width || !rect.height || x < 0 || y < 0 || x >= innerWidth || y >= innerHeight || document.elementFromPoint(x, y) !== options.canvas)
49
+ throw new Error(`Game object is covered or outside the viewport: ${id}`);
50
+ return { x, y };
51
+ };
52
+ const bridge = { locate, readState: options.readState, testMode: options.testMode === true,
53
+ fixtures: options.testMode === true ? options.fixtures || {} : {} };
54
+ window.__FLOWSTRIDE_GAME__ = bridge;
55
+ return { registerObject(id, object) { if (!id || objects.has(id))
56
+ throw new Error('Game object ID must be unique and nonempty.'); objects.set(id, object); },
57
+ dispose() { if (window.__FLOWSTRIDE_GAME__ === bridge)
58
+ delete window.__FLOWSTRIDE_GAME__; } };
59
+ }
@@ -331,6 +331,10 @@ class Lexer {
331
331
  "img",
332
332
  "burger",
333
333
  "hamburger",
334
+ "table",
335
+ "tr",
336
+ "th",
337
+ "td",
334
338
  ].includes(lowerIdent)) {
335
339
  tokens.push({
336
340
  type: TokenType.ELEMENT,
@@ -319,11 +319,27 @@ class Parser {
319
319
  extTarget !== "reqHeader") {
320
320
  throw new Error(`Syntax Error on line ${extTargetToken.line}: Invalid keyword '${extTarget}'. Flowstride uses strict camelCase. Did you mean 'reqHeader'?`);
321
321
  }
322
- if (!["resBody", "resHeader", "cookie"].includes(extTarget)) {
323
- throw new Error(`Line ${extTargetToken.line}: Expected 'resBody', 'resHeader', or 'cookie' after extract`);
322
+ if (![
323
+ "resBody",
324
+ "resHeader",
325
+ "cookie",
326
+ "text",
327
+ "value",
328
+ "attribute",
329
+ ].includes(extTarget)) {
330
+ throw new Error(`Line ${extTargetToken.line}: Expected 'resBody', 'resHeader', 'cookie', 'text', 'value', or 'attribute' after extract`);
324
331
  }
325
332
  payload.target = extTarget;
326
- payload.jsonPath = this.consume(lexer_1.TokenType.STRING, "JSONPath or Key expected").value;
333
+ if (["resBody", "resHeader", "cookie"].includes(extTarget)) {
334
+ payload.jsonPath = this.consume(lexer_1.TokenType.STRING, "JSONPath or Key expected").value;
335
+ }
336
+ else {
337
+ this.consumeOptionalElement(payload);
338
+ payload.selector = this.parseSmartSelectorPayload();
339
+ if (extTarget === "attribute") {
340
+ payload.attribute = this.consume(lexer_1.TokenType.STRING, "Attribute name expected").value;
341
+ }
342
+ }
327
343
  const asToken = this.advance();
328
344
  if (asToken.value.toLowerCase() !== "as") {
329
345
  throw new Error(`Line ${asToken.line}: Expected 'as' after extraction target`);
@@ -4,6 +4,8 @@ export declare class FlowOrchestrator {
4
4
  private worker;
5
5
  private workerCount;
6
6
  private config?;
7
+ static readonly nativeCIProtocol = 1;
8
+ readonly nativeEventProtocol = 1;
7
9
  private wss;
8
10
  private httpServer;
9
11
  private globalTestScenarios;
@@ -13,6 +15,8 @@ export declare class FlowOrchestrator {
13
15
  constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
14
16
  private forwardToDashboard;
15
17
  private executeTestRun;
16
- start(targetPath: string): Promise<void>;
18
+ start(targetPath: string): Promise<boolean>;
19
+ abort(): void;
20
+ close(): Promise<void>;
17
21
  private handleExportPdfRequest;
18
22
  }
@@ -48,6 +48,8 @@ class FlowOrchestrator {
48
48
  worker;
49
49
  workerCount;
50
50
  config;
51
+ static nativeCIProtocol = 1;
52
+ nativeEventProtocol = 1;
51
53
  wss = null;
52
54
  httpServer = null;
53
55
  globalTestScenarios = [];
@@ -64,6 +66,7 @@ class FlowOrchestrator {
64
66
  });
65
67
  }
66
68
  forwardToDashboard(type, payload) {
69
+ this.config?.onRunEvent?.({ type, payload });
67
70
  if (this.wss) {
68
71
  this.wss.clients.forEach((client) => {
69
72
  if (client.readyState === 1)
@@ -76,10 +79,18 @@ class FlowOrchestrator {
76
79
  let filesToProcess = [];
77
80
  const stats = fs.statSync(targetPath);
78
81
  if (stats.isDirectory()) {
79
- filesToProcess = fs
82
+ const allFlowFiles = fs
80
83
  .readdirSync(targetPath)
81
- .filter((f) => f.endsWith(".flow"))
84
+ .filter((f) => f.endsWith(".flow"));
85
+ const priorityQueue = allFlowFiles
86
+ .filter((f) => f.endsWith(".first.flow"))
87
+ .sort()
82
88
  .map((f) => path.join(targetPath, f));
89
+ const standardQueue = allFlowFiles
90
+ .filter((f) => !f.endsWith(".first.flow"))
91
+ .sort()
92
+ .map((f) => path.join(targetPath, f));
93
+ filesToProcess = [...priorityQueue, ...standardQueue];
83
94
  }
84
95
  else {
85
96
  filesToProcess = [targetPath];
@@ -103,10 +114,14 @@ class FlowOrchestrator {
103
114
  this.worker.on("worker_event", listener);
104
115
  await this.worker.runBatch(filesToProcess);
105
116
  this.worker.off("worker_event", listener);
106
- this.forwardToDashboard("RUN_COMPLETE", { status: "success" });
117
+ const passed = ![...this.stepStatusRegistry.values()].some(status => ['failed', 'running', 'not_tested'].includes(status));
118
+ this.forwardToDashboard("RUN_COMPLETE", { status: passed ? 'success' : 'failed' });
119
+ return passed;
107
120
  }
108
121
  async start(targetPath) {
109
122
  try {
123
+ if (this.config?.ci)
124
+ return await this.executeTestRun(targetPath);
110
125
  this.httpServer = http.createServer((req, res) => {
111
126
  res.setHeader("Access-Control-Allow-Origin", "*");
112
127
  if (req.url && req.url.startsWith("/videos/")) {
@@ -304,8 +319,8 @@ class FlowOrchestrator {
304
319
  else {
305
320
  console.log(chalk_1.default.gray("[Dashboard]: Headless mode active. Bypassing UI auto-launch..."));
306
321
  }
307
- await this.executeTestRun(targetPath);
308
- resolve(undefined);
322
+ const result = await this.executeTestRun(targetPath);
323
+ resolve(result);
309
324
  }
310
325
  catch (err) {
311
326
  reject(err);
@@ -317,11 +332,20 @@ class FlowOrchestrator {
317
332
  throw e;
318
333
  }
319
334
  }
335
+ abort() {
336
+ this.worker.abort();
337
+ }
338
+ async close() {
339
+ if (this.disconnectTimer)
340
+ clearTimeout(this.disconnectTimer);
341
+ this.wss?.clients.forEach(client => client.terminate());
342
+ this.wss?.close();
343
+ this.httpServer?.close();
344
+ }
320
345
  async handleExportPdfRequest(testScenariosFromUI) {
321
346
  try {
322
- const spinner = (0, ora_1.default)(chalk_1.default.blue("Compiling high-fidelity PDF report...")).start();
347
+ const spinner = (0, ora_1.default)(chalk_1.default.blue("Compiling high fidelity PDF report...")).start();
323
348
  const pdfPath = await this.pdfGenerator.generateTestcasePdf(testScenariosFromUI);
324
- // Convert the saved PDF to base64 to trigger the native browser download UI
325
349
  const pdfBuffer = fs.readFileSync(pdfPath);
326
350
  const base64Data = pdfBuffer.toString("base64");
327
351
  this.forwardToDashboard("EXPORT_PDF_RES", { base64: base64Data });