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.
Files changed (99) hide show
  1. package/NATIVE_BROWSER.md +381 -0
  2. package/NATIVE_CI.md +17 -5
  3. package/NATIVE_WORKFLOWS.md +44 -0
  4. package/README.md +11 -4
  5. package/dist/adapters/api/undici.d.ts +11 -1
  6. package/dist/adapters/api/undici.js +28 -3
  7. package/dist/adapters/web/playwright.d.ts +22 -2
  8. package/dist/adapters/web/playwright.js +254 -107
  9. package/dist/ci/config.js +14 -1
  10. package/dist/ci/distribution.d.ts +38 -0
  11. package/dist/ci/distribution.js +134 -0
  12. package/dist/ci/init.d.ts +4 -1
  13. package/dist/ci/init.js +13 -2
  14. package/dist/ci/runtime.js +33 -6
  15. package/dist/cli/commands/run.js +17 -4
  16. package/dist/cli/index.js +24 -5
  17. package/dist/cli/manual_server/workspace_server.js +90 -74
  18. package/dist/manual/security.d.ts +6 -6
  19. package/dist/native/accessibility.d.ts +4 -0
  20. package/dist/native/accessibility.js +84 -0
  21. package/dist/native/assertions.d.ts +3 -0
  22. package/dist/native/assertions.js +223 -0
  23. package/dist/native/compatibility.d.ts +3 -0
  24. package/dist/native/compatibility.js +16 -0
  25. package/dist/native/components.d.ts +12 -0
  26. package/dist/native/components.js +122 -0
  27. package/dist/native/dashboard.d.ts +7 -0
  28. package/dist/native/dashboard.js +32 -0
  29. package/dist/native/deadline.d.ts +2 -0
  30. package/dist/native/deadline.js +13 -0
  31. package/dist/native/events.d.ts +35 -0
  32. package/dist/native/events.js +308 -0
  33. package/dist/native/evidence.d.ts +3 -0
  34. package/dist/native/evidence.js +31 -0
  35. package/dist/native/functions.d.ts +10 -0
  36. package/dist/native/functions.js +131 -0
  37. package/dist/native/runtime.d.ts +17 -0
  38. package/dist/native/runtime.js +160 -0
  39. package/dist/native/validation.d.ts +107 -0
  40. package/dist/native/validation.js +84 -0
  41. package/dist/parser/assertion_contract.d.ts +12 -0
  42. package/dist/parser/assertion_contract.js +56 -0
  43. package/dist/parser/lexer.js +15 -4
  44. package/dist/parser/parser.js +15 -7
  45. package/dist/reporting/assertion_result.d.ts +36 -0
  46. package/dist/reporting/assertion_result.js +104 -0
  47. package/dist/reporting/pdf_generator.js +25 -20
  48. package/dist/reporting/runtime_failure.d.ts +10 -0
  49. package/dist/reporting/runtime_failure.js +64 -0
  50. package/dist/reporting/step_status.d.ts +10 -0
  51. package/dist/reporting/step_status.js +44 -0
  52. package/dist/runner/flow_orchestrator.d.ts +5 -0
  53. package/dist/runner/flow_orchestrator.js +30 -8
  54. package/dist/runner/flow_worker.d.ts +15 -1
  55. package/dist/runner/flow_worker.js +219 -42
  56. package/dist/runner/plugin_manager.js +3 -0
  57. package/dist/runner/temporary_store.d.ts +1 -0
  58. package/dist/runner/temporary_store.js +3 -0
  59. package/dist/sdk/api_proxy.d.ts +3 -0
  60. package/dist/sdk/api_proxy.js +16 -0
  61. package/dist/security/localWorkspace.d.ts +2 -0
  62. package/dist/security/localWorkspace.js +17 -0
  63. package/dist/security/redaction.js +1 -1
  64. package/dist/types/index.d.ts +13 -0
  65. package/examples/native-workflows/README.md +144 -0
  66. package/examples/native-workflows/api-and-ui/README.md +80 -0
  67. package/examples/native-workflows/api-and-ui/flows/api-and-ui.flow +27 -0
  68. package/examples/native-workflows/api-and-ui/negative/wrong-backend-state.flow +27 -0
  69. package/examples/native-workflows/api-ui-otp/README.md +140 -0
  70. package/examples/native-workflows/api-ui-otp/templates/api-ui-otp.flow +31 -0
  71. package/examples/native-workflows/api-ui-otp/templates/existing-account-otp.flow +20 -0
  72. package/examples/native-workflows/audio-to-text/README.md +131 -0
  73. package/examples/native-workflows/audio-to-text/fixtures/microphone-tone.wav +0 -0
  74. package/examples/native-workflows/audio-to-text/flows/microphone-demo.flow +23 -0
  75. package/examples/native-workflows/audio-to-text/negative/wrong-transcript.flow +23 -0
  76. package/examples/native-workflows/audio-to-text/templates/real-transcription.flow +25 -0
  77. package/examples/native-workflows/iframes/README.md +116 -0
  78. package/examples/native-workflows/iframes/flows/explicit-frame-plugin.flow +20 -0
  79. package/examples/native-workflows/iframes/flows/readable-frame.flow +13 -0
  80. package/examples/native-workflows/iframes/negative/wrong-frame-result.flow +13 -0
  81. package/examples/native-workflows/iframes/plugins/frame-actions.ts +7 -0
  82. package/examples/native-workflows/manual-testing/README.md +85 -0
  83. package/examples/native-workflows/manual-testing/fixtures/profile-review-mismatch.json +93 -0
  84. package/examples/native-workflows/manual-testing/fixtures/profile-review.json +93 -0
  85. package/examples/native-workflows/shared/server.mjs +80 -0
  86. package/examples/native-workflows/tables-and-cells/README.md +118 -0
  87. package/examples/native-workflows/tables-and-cells/flows/coordinates.flow +17 -0
  88. package/examples/native-workflows/tables-and-cells/flows/search-extract-reuse.flow +27 -0
  89. package/examples/native-workflows/tables-and-cells/negative/wrong-cell-value.flow +27 -0
  90. package/examples/native-workflows/variables-and-reuse/README.md +140 -0
  91. package/examples/native-workflows/variables-and-reuse/flows/reuse.flow +9 -0
  92. package/examples/native-workflows/variables-and-reuse/flows/setup.first.flow +27 -0
  93. package/examples/native-workflows/variables-and-reuse/negative/wrong-reused-value.flow +27 -0
  94. package/examples/native-workflows/variables-and-reuse/sessions/restore.flow +10 -0
  95. package/examples/native-workflows/variables-and-reuse/sessions/save.flow +29 -0
  96. package/package.json +23 -9
  97. package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
  98. package/src/ui/dist/index.html +1 -1
  99. package/src/ui/dist/assets/index-B7MuBTWm.js +0 -460
@@ -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
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
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 status = (step.status || "PENDING").toUpperCase();
99
- const isSuccess = status === "PASS" || status === "PASSED" || status === "SUCCESS";
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 (isFail)
108
+ else if (result.isFailed)
104
109
  failedSteps++;
105
- const color = isSuccess ? "#10b981" : isFail ? "#ef4444" : "#64748b";
106
- const actualResultText = isFail
107
- ? "Validation Failed"
108
- : "Action verified successfully.";
109
- const actualResultColor = isFail ? "#ef4444" : "#64748b";
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;">${step.command || "action"}</td>
114
- <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: #475569; font-style: italic;">${step.type || ""} ${step.description || "-"}</td>
115
- <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: ${actualResultColor};">${actualResultText}</td>
116
- <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 900; font-size: 10px; color: ${color};">${status}</td>
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>
@@ -0,0 +1,10 @@
1
+ export interface FailureContext {
2
+ file?: string;
3
+ line?: number;
4
+ profile?: string;
5
+ timeout?: number;
6
+ elapsed?: number;
7
+ }
8
+ /** Preserve the adapter's evidence and nested causes, without printing request bodies or typed values. */
9
+ export declare function commandSecrets(command: string, payload: any): string[];
10
+ export declare function runtimeFailure(command: string, payload: any, error: unknown, context: FailureContext): string;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commandSecrets = commandSecrets;
4
+ exports.runtimeFailure = runtimeFailure;
5
+ const redaction_1 = require("../security/redaction");
6
+ const assertion_result_1 = require("./assertion_result");
7
+ /** Preserve the adapter's evidence and nested causes, without printing request bodies or typed values. */
8
+ function commandSecrets(command, payload) {
9
+ const secrets = [], seen = new Set();
10
+ const visit = (value, depth) => {
11
+ if (depth > 8 || value == null || seen.has(value))
12
+ return;
13
+ if (typeof value === 'string') {
14
+ if (/^\s*[\[{]/.test(value)) {
15
+ try {
16
+ visit(JSON.parse(value), depth + 1);
17
+ }
18
+ catch { }
19
+ }
20
+ return;
21
+ }
22
+ if (typeof value !== 'object')
23
+ return;
24
+ seen.add(value);
25
+ for (const [key, item] of Object.entries(value).slice(0, 200)) {
26
+ if (/password|passcode|secret|token|authorization|cookie|api.?key|otp/i.test(key) && typeof item === 'string')
27
+ secrets.push(item);
28
+ else
29
+ visit(item, depth + 1);
30
+ }
31
+ };
32
+ visit(payload, 0);
33
+ if (/^(?:flow\.)?(?:type|forceType|passcode)$/i.test(command) && typeof payload?.text === 'string')
34
+ secrets.push(payload.text);
35
+ return secrets;
36
+ }
37
+ function runtimeFailure(command, payload, error, context) {
38
+ const label = command.includes('.') ? command : 'flow.' + command;
39
+ const lines = [`Failed command: ${(0, assertion_result_1.diagnosticValue)(label)}`];
40
+ if (context.file)
41
+ lines.push(`Location: ${(0, assertion_result_1.diagnosticValue)(context.file)}${context.line ? ':' + context.line : ''}`);
42
+ const input = payload?.data && typeof payload.data === 'object' ? payload.data : payload || {};
43
+ const fields = ['selector', 'role', 'name', 'text', 'url', 'path', 'file', 'filename', 'audioPath', 'sessionName', 'jsonPath', 'attribute', 'property', 'key', 'canvas', 'id', 'target'];
44
+ const targets = fields.filter(key => !(/^(?:flow\.)?(?:type|forceType|passcode)$/i.test(command) && key === 'text') && (['string', 'number'].includes(typeof input[key]) || key === 'selector' && input[key] && typeof input[key] === 'object')).map(key => `${key}: ${(0, assertion_result_1.diagnosticValue)(input[key])}`);
45
+ if (targets.length)
46
+ lines.push('Target / resource: ' + targets.join('; '));
47
+ if (context.profile)
48
+ lines.push('Profile: ' + (0, assertion_result_1.diagnosticValue)(context.profile));
49
+ if (context.timeout !== undefined)
50
+ lines.push(`Timeout: ${context.timeout} ms${context.elapsed !== undefined ? '; elapsed: ' + context.elapsed + ' ms' : ''}`);
51
+ const seen = new Set();
52
+ let current = error;
53
+ for (let depth = 0; current != null && depth < 5 && !seen.has(current); depth++) {
54
+ seen.add(current);
55
+ const message = typeof current === 'object' ? current.message || String(current) : String(current);
56
+ // Keep line breaks in Playwright call logs, but escape terminal control sequences.
57
+ const reason = String(message).slice(0, 12000).split('\n').map(line => (0, assertion_result_1.diagnosticValue)(line)).join('\n');
58
+ lines.push((depth ? 'Caused by: ' : 'Reason: ') + reason);
59
+ if (current.code)
60
+ lines.push('Error code: ' + (0, assertion_result_1.diagnosticValue)(current.code));
61
+ current = current.cause;
62
+ }
63
+ return (0, redaction_1.redactText)(lines.join('\n'), commandSecrets(command, payload));
64
+ }
@@ -0,0 +1,10 @@
1
+ /** Presentation shared by the interactive TestCase view and exported PDF. */
2
+ export interface StepStatusPresentation {
3
+ status: string;
4
+ label: string;
5
+ actualResult: string;
6
+ color: string;
7
+ isPassed: boolean;
8
+ isFailed: boolean;
9
+ }
10
+ export declare function stepStatusPresentation(value: unknown): StepStatusPresentation;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stepStatusPresentation = stepStatusPresentation;
4
+ function stepStatusPresentation(value) {
5
+ const status = typeof value === "string"
6
+ ? value.trim().toLowerCase().replace(/[\s-]+/g, "_")
7
+ : value == null ? "pending" : "unknown";
8
+ const result = (state, label, actualResult, color = "#64748b") => ({
9
+ status: state, label, actualResult, color,
10
+ isPassed: state === "passed",
11
+ isFailed: state === "failed" || state === "error",
12
+ });
13
+ switch (status) {
14
+ case "pass":
15
+ case "passed":
16
+ case "success":
17
+ return result("passed", "PASSED", "Action verified successfully.", "#10b981");
18
+ case "fail":
19
+ case "failed":
20
+ return result("failed", "FAILED", "Validation failed.", "#ef4444");
21
+ case "error":
22
+ return result("error", "ERROR", "Step execution failed.", "#ef4444");
23
+ case "skip":
24
+ case "skipped":
25
+ return result("skipped", "SKIPPED", "Step skipped; not executed.");
26
+ case "not_tested":
27
+ return result("not_tested", "NOT TESTED", "Step not tested.");
28
+ case "":
29
+ case "pending":
30
+ return result("pending", "PENDING", "Awaiting execution.");
31
+ case "running":
32
+ case "in_progress":
33
+ return result("running", "RUNNING", "Execution in progress.");
34
+ case "cancelled":
35
+ case "canceled":
36
+ return result("cancelled", "CANCELLED", "Execution cancelled.");
37
+ case "interrupted":
38
+ return result("interrupted", "INTERRUPTED", "Execution interrupted.");
39
+ case "blocked":
40
+ return result("blocked", "BLOCKED", "Execution blocked.");
41
+ default:
42
+ return result("unknown", "UNKNOWN", "Result unavailable.");
43
+ }
44
+ }
@@ -4,6 +4,8 @@ export declare class FlowOrchestrator {
4
4
  private worker;
5
5
  private workerCount;
6
6
  private config?;
7
+ static readonly nativeBrowserProtocol = 2;
8
+ static readonly nativeAssertionProtocol = 1;
7
9
  static readonly nativeCIProtocol = 1;
8
10
  readonly nativeEventProtocol = 1;
9
11
  private wss;
@@ -12,6 +14,9 @@ export declare class FlowOrchestrator {
12
14
  private stepStatusRegistry;
13
15
  private pdfGenerator;
14
16
  private disconnectTimer;
17
+ private dashboardBrowser;
18
+ private closing;
19
+ private browserInfoByFile;
15
20
  constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
16
21
  private forwardToDashboard;
17
22
  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,8 @@ class FlowOrchestrator {
50
52
  worker;
51
53
  workerCount;
52
54
  config;
55
+ static nativeBrowserProtocol = 2;
56
+ static nativeAssertionProtocol = 1;
53
57
  static nativeCIProtocol = 1;
54
58
  nativeEventProtocol = 1;
55
59
  wss = null;
@@ -58,6 +62,9 @@ class FlowOrchestrator {
58
62
  stepStatusRegistry = new Map();
59
63
  pdfGenerator;
60
64
  disconnectTimer = null;
65
+ dashboardBrowser = new dashboard_1.DashboardBrowser();
66
+ closing = false;
67
+ browserInfoByFile = new Map();
61
68
  constructor(worker, workerCount = 1, config) {
62
69
  this.worker = worker;
63
70
  this.workerCount = workerCount;
@@ -68,6 +75,10 @@ class FlowOrchestrator {
68
75
  });
69
76
  }
70
77
  forwardToDashboard(type, payload) {
78
+ if (type === 'BROWSER_INFO')
79
+ this.browserInfoByFile.set(payload.fileName, payload);
80
+ if (type === 'SUITE_START')
81
+ this.browserInfoByFile.clear();
71
82
  this.config?.onRunEvent?.({ type, payload });
72
83
  if (this.wss) {
73
84
  this.wss.clients.forEach((client) => {
@@ -78,7 +89,7 @@ class FlowOrchestrator {
78
89
  }
79
90
  async executeTestRun(targetPath) {
80
91
  this.forwardToDashboard("SUITE_START", {});
81
- const filesToProcess = (0, projectPaths_1.discoverFlows)(targetPath);
92
+ const filesToProcess = (0, distribution_1.executionPlan)(targetPath, this.config).files;
82
93
  if (this.config)
83
94
  this.config.flowRoot = fs.statSync(targetPath).isDirectory() ? targetPath : path.dirname(targetPath);
84
95
  this.globalTestScenarios = [];
@@ -98,9 +109,13 @@ class FlowOrchestrator {
98
109
  }
99
110
  };
100
111
  this.worker.on("worker_event", listener);
101
- await this.worker.runBatch(filesToProcess);
102
- this.worker.off("worker_event", listener);
103
- const passed = ![...this.stepStatusRegistry.values()].some(status => ['failed', 'running', 'not_tested'].includes(status));
112
+ let passed = false;
113
+ try {
114
+ passed = await this.worker.runBatch(filesToProcess);
115
+ }
116
+ finally {
117
+ this.worker.off("worker_event", listener);
118
+ }
104
119
  this.forwardToDashboard("RUN_COMPLETE", { status: passed ? 'success' : 'failed' });
105
120
  return passed;
106
121
  }
@@ -115,6 +130,8 @@ class FlowOrchestrator {
115
130
  return;
116
131
  }
117
132
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
133
+ if ((0, evidence_1.serveDOMSnapshot)(req, res, this.config))
134
+ return;
118
135
  if (req.url && req.url.startsWith("/videos/")) {
119
136
  let urlObj;
120
137
  try {
@@ -229,6 +246,8 @@ class FlowOrchestrator {
229
246
  });
230
247
  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
248
  this.wss.on("connection", (ws) => {
249
+ for (const payload of this.browserInfoByFile.values())
250
+ ws.send(JSON.stringify({ type: 'BROWSER_INFO', payload }));
232
251
  if (this.disconnectTimer) {
233
252
  clearTimeout(this.disconnectTimer);
234
253
  this.disconnectTimer = null;
@@ -267,7 +286,7 @@ class FlowOrchestrator {
267
286
  }
268
287
  });
269
288
  ws.on("close", () => {
270
- if (this.wss && this.wss.clients.size === 0) {
289
+ if (!this.closing && this.wss && this.wss.clients.size === 0) {
271
290
  this.disconnectTimer = setTimeout(async () => {
272
291
  console.log(chalk_1.default.yellow("\n[Flowstride]: Dashboard disconnected. Shutting down framework..."));
273
292
  this.worker.abort();
@@ -300,8 +319,8 @@ class FlowOrchestrator {
300
319
  !!process.env.GITHUB_ACTIONS ||
301
320
  !!process.env.FLOWSTRIDE_API_KEY;
302
321
  if (!isStrictHeadless) {
303
- const { default: open } = await import("open");
304
- await open(localUrl);
322
+ await this.dashboardBrowser.open(localUrl, this.config?.browser);
323
+ 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
324
  console.log(chalk_1.default.yellow("[Dashboard]: Waiting for UI connection..."));
306
325
  while (this.wss.clients.size === 0)
307
326
  await new Promise((r) => setTimeout(r, 250));
@@ -314,6 +333,7 @@ class FlowOrchestrator {
314
333
  resolve(result);
315
334
  }
316
335
  catch (err) {
336
+ await this.dashboardBrowser.close();
317
337
  reject(err);
318
338
  }
319
339
  });
@@ -327,6 +347,8 @@ class FlowOrchestrator {
327
347
  this.worker.abort();
328
348
  }
329
349
  async close() {
350
+ this.closing = true;
351
+ await this.dashboardBrowser.close();
330
352
  if (this.disconnectTimer)
331
353
  clearTimeout(this.disconnectTimer);
332
354
  this.wss?.clients.forEach(client => client.terminate());
@@ -19,10 +19,20 @@ export declare class FlowWorker extends EventEmitter {
19
19
  private store;
20
20
  private sessions;
21
21
  private pluginManager?;
22
+ static readonly nativeBrowserProtocol = 2;
23
+ static readonly nativeAssertionProtocol = 1;
22
24
  private isSilent;
23
25
  private isAborted;
24
26
  private bailTriggered;
25
27
  private isDebugHalted;
28
+ private native;
29
+ private currentAssertion?;
30
+ private commandSecretValues;
31
+ private batchPassed;
32
+ private attempt;
33
+ private maxAttempts;
34
+ private fallbackStream?;
35
+ private interceptedContexts;
26
36
  private currentStepContext;
27
37
  private currentFileName;
28
38
  private activeAudioPath;
@@ -35,6 +45,9 @@ export declare class FlowWorker extends EventEmitter {
35
45
  private lastApiUrl;
36
46
  constructor(config: FlowstrideConfig, web: WebAdapter, api: ApiAdapter, store: any, sessions: SessionManager, pluginManager?: PluginManager | undefined);
37
47
  private broadcast;
48
+ private secretValues;
49
+ private assertionForStep;
50
+ private printAssertion;
38
51
  private emitLog;
39
52
  private translateError;
40
53
  private captureStepContext;
@@ -45,7 +58,8 @@ export declare class FlowWorker extends EventEmitter {
45
58
  private emitApiTelemetry;
46
59
  handleReplayRequest(payload: any): Promise<void>;
47
60
  abort(): void;
48
- runBatch(filesToProcess: string[]): Promise<void>;
61
+ runBatch(filesToProcess: string[]): Promise<boolean>;
62
+ private runScenarioWithRetries;
49
63
  private runScenario;
50
64
  private waitForPipeline;
51
65
  private executeStep;