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
@@ -61,6 +61,9 @@ export declare const manualRunSchema: z.ZodObject<{
61
61
  failed: "failed";
62
62
  skipped: "skipped";
63
63
  success: "success";
64
+ pending: "pending";
65
+ pass: "pass";
66
+ blocked: "blocked";
64
67
  Pending: "Pending";
65
68
  Current: "Current";
66
69
  Passed: "Passed";
@@ -70,9 +73,6 @@ export declare const manualRunSchema: z.ZodObject<{
70
73
  Pass: "Pass";
71
74
  Fail: "Fail";
72
75
  Success: "Success";
73
- pass: "pass";
74
- pending: "pending";
75
- blocked: "blocked";
76
76
  }>;
77
77
  group_title: z.ZodOptional<z.ZodString>;
78
78
  id: z.ZodNumber;
@@ -143,6 +143,9 @@ export declare const manualCanvasSchema: z.ZodObject<{
143
143
  failed: "failed";
144
144
  skipped: "skipped";
145
145
  success: "success";
146
+ pending: "pending";
147
+ pass: "pass";
148
+ blocked: "blocked";
146
149
  Pending: "Pending";
147
150
  Current: "Current";
148
151
  Passed: "Passed";
@@ -152,9 +155,6 @@ export declare const manualCanvasSchema: z.ZodObject<{
152
155
  Pass: "Pass";
153
156
  Fail: "Fail";
154
157
  Success: "Success";
155
- pass: "pass";
156
- pending: "pending";
157
- blocked: "blocked";
158
158
  }>;
159
159
  group_title: z.ZodOptional<z.ZodString>;
160
160
  id: z.ZodNumber;
@@ -0,0 +1,4 @@
1
+ import { type AssertionReporter } from '../reporting/assertion_result';
2
+ import type { Page } from 'playwright';
3
+ import type { FlowstrideConfig } from '../types';
4
+ export declare function accessibility(page: Page, config: FlowstrideConfig, action: string, input: unknown, reporter?: AssertionReporter): Promise<string | undefined>;
@@ -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.accessibility = accessibility;
7
+ const assertion_result_1 = require("../reporting/assertion_result");
8
+ const zod_1 = require("zod");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const playwright_1 = __importDefault(require("@axe-core/playwright"));
12
+ const validation_1 = require("./validation");
13
+ const projectPaths_1 = require("../utils/projectPaths");
14
+ async function accessibility(page, config, action, input, reporter) {
15
+ if (action === 'scan') {
16
+ const data = zod_1.z.object({ name: validation_1.name.default('accessibility'), include: zod_1.z.array(zod_1.z.string()).optional(), exclude: zod_1.z.array(zod_1.z.string()).optional(), tags: zod_1.z.array(zod_1.z.string()).optional(), rules: zod_1.z.array(zod_1.z.string()).optional(), disableRules: zod_1.z.array(zod_1.z.string()).optional(), impacts: zod_1.z.array(zod_1.z.enum(['minor', 'moderate', 'serious', 'critical'])).optional(), maxViolations: zod_1.z.number().int().nonnegative().default(0) }).strict().parse(input);
17
+ let builder = new playwright_1.default({ page });
18
+ for (const selector of data.include || [])
19
+ builder = builder.include(selector);
20
+ for (const selector of data.exclude || [])
21
+ builder = builder.exclude(selector);
22
+ if (data.tags)
23
+ builder = builder.withTags(data.tags);
24
+ if (data.rules)
25
+ builder = builder.withRules(data.rules);
26
+ if (data.disableRules)
27
+ builder = builder.disableRules(data.disableRules);
28
+ const result = await builder.analyze();
29
+ const violations = result.violations.filter(item => !data.impacts || item.impact && data.impacts.includes(item.impact));
30
+ const file = (0, validation_1.evidencePath)(config, `${data.name}-${Date.now()}.json`);
31
+ // Reports may contain application content. They remain in local evidence.
32
+ node_fs_1.default.writeFileSync(file, JSON.stringify({ ...result, violations }, null, 2), { mode: 0o600 });
33
+ (0, assertion_result_1.requireAssertion)('a11y.scan', 'accessibility scan ' + data.name, [(0, assertion_result_1.assertionCheck)('violations', 'Violation count within the allowed maximum', violations.length <= data.maxViolations, `at most ${data.maxViolations}`, violations.length)], reporter, { reason: violations.length > data.maxViolations ? `Accessibility assertion failed: ${violations.length} violations (${violations.map(item => item.id).slice(0, 30).join(', ')}). Report: ${file}` : undefined });
34
+ return file;
35
+ }
36
+ if (action === 'snapshot') {
37
+ const data = zod_1.z.object({ name: validation_1.name, selector: zod_1.z.string().default('body'), baseline: zod_1.z.string().optional(), update: zod_1.z.boolean().default(false), timeout: validation_1.positive.optional() }).strict().parse(input);
38
+ const snapshot = await page.locator(data.selector).ariaSnapshot({ timeout: data.timeout ?? config.timeout });
39
+ const file = (0, validation_1.evidencePath)(config, `${data.name}-${Date.now()}.aria.yml`);
40
+ node_fs_1.default.writeFileSync(file, snapshot, { mode: 0o600 });
41
+ if (!data.baseline) {
42
+ if (data.update)
43
+ throw new Error('Choose a baseline path.');
44
+ return file;
45
+ }
46
+ const baseline = (0, projectPaths_1.projectPath)(config.projectRoot || process.cwd(), data.baseline);
47
+ if (data.update || config.updateBaselines) {
48
+ if (config.ci || process.env.CI && process.env.CI !== 'false')
49
+ throw new Error('Baseline updates are not allowed in CI.');
50
+ if (!config.updateBaselines)
51
+ throw new Error('Baseline writing requires --update-baselines.');
52
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(baseline), { recursive: true });
53
+ node_fs_1.default.writeFileSync(baseline, snapshot, { mode: 0o600 });
54
+ }
55
+ else {
56
+ const expected = (0, validation_1.readProjectFile)(config, baseline).toString('utf8').replace(/\r\n/g, '\n').trim();
57
+ (0, assertion_result_1.requireAssertion)('a11y.snapshot', 'ARIA baseline ' + data.baseline, [(0, assertion_result_1.assertionCheck)('baseline', 'ARIA baseline matches', expected === snapshot.trim(), expected, snapshot.trim())], reporter, { reason: expected !== snapshot.trim() ? `ARIA baseline differs. Review the captured snapshot before updating: ${file}` : undefined });
58
+ }
59
+ return file;
60
+ }
61
+ if (action === 'match') {
62
+ const data = zod_1.z.object({ selector: zod_1.z.string().default('body'), yaml: zod_1.z.string().min(1), timeout: validation_1.positive.optional() }).strict().parse(input);
63
+ const started = Date.now(), timeout = data.timeout ?? config.timeout;
64
+ let latest = (0, assertion_result_1.reportAssertion)('a11y.match', 'ARIA snapshot ' + data.selector, [{ key: 'snapshot', label: 'ARIA snapshot matches', status: 'running' }], reporter, { status: 'running', timeout, elapsed: 0 });
65
+ let closed = false;
66
+ try {
67
+ await (0, validation_1.poll)(async () => { const actual = (await page.locator(data.selector).ariaSnapshot()).trim(), expected = data.yaml.replace(/\r\n/g, '\n').trim(); const check = (0, assertion_result_1.assertionCheck)('snapshot', 'ARIA snapshot matches', actual === expected, expected, actual); if (!closed)
68
+ latest = (0, assertion_result_1.reportAssertion)('a11y.match', 'ARIA snapshot ' + data.selector, [check], reporter, { status: 'running', timeout, elapsed: Date.now() - started }); return check.status === 'passed'; }, timeout, 'ARIA snapshot differs');
69
+ closed = true;
70
+ reporter?.({ ...latest, status: 'passed', elapsed: Date.now() - started });
71
+ }
72
+ catch (error) {
73
+ closed = true;
74
+ const result = { ...latest, status: 'failed', elapsed: Date.now() - started, reason: (0, assertion_result_1.diagnosticValue)(error instanceof Error ? error.message : String(error)) };
75
+ for (const check of result.checks)
76
+ if (check.status === 'running')
77
+ check.status = 'failed';
78
+ reporter?.(result);
79
+ throw new assertion_result_1.AssertionFailure(result);
80
+ }
81
+ return;
82
+ }
83
+ throw new Error(`Unknown accessibility action: ${action}`);
84
+ }
@@ -0,0 +1,3 @@
1
+ import type { Page } from 'playwright';
2
+ import { type AssertionReporter } from '../reporting/assertion_result';
3
+ export declare function assertBrowser(page: Page, input: unknown, defaultTimeout: number, reporter?: AssertionReporter): Promise<void>;
@@ -0,0 +1,223 @@
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.assertBrowser = assertBrowser;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const zod_1 = require("zod");
9
+ const validation_1 = require("./validation");
10
+ const node_util_1 = require("node:util");
11
+ const assertion_result_1 = require("../reporting/assertion_result");
12
+ let accessibilityBundle;
13
+ async function installAccessibilityNames(page) {
14
+ if (await page.evaluate(() => !!window.__FLOWSTRIDE_ACCESSIBILITY_NAMES__))
15
+ return;
16
+ accessibilityBundle ||= (async () => {
17
+ const { build } = await import('vite');
18
+ const result = await build({ configFile: false, root: node_path_1.default.dirname(require.resolve('dom-accessibility-api')), envDir: false, logLevel: 'silent', build: { write: false, minify: true, emptyOutDir: false,
19
+ lib: { entry: require.resolve('dom-accessibility-api'), formats: ['iife'], name: '__FLOWSTRIDE_ACCESSIBILITY_NAMES__' } } });
20
+ const code = (Array.isArray(result) ? result : [result]).flatMap(bundle => bundle.output).find((file) => file.type === 'chunk').code;
21
+ return code + '\nwindow.__FLOWSTRIDE_ACCESSIBILITY_NAMES__=__FLOWSTRIDE_ACCESSIBILITY_NAMES__;';
22
+ })();
23
+ const source = await accessibilityBundle;
24
+ for (const frame of page.frames())
25
+ await frame.evaluate(source);
26
+ }
27
+ const schema = zod_1.z.object({ selector: zod_1.z.string().optional(), role: zod_1.z.string().optional(), name: zod_1.z.string().optional(), frame: zod_1.z.string().optional(),
28
+ condition: zod_1.z.enum(['visible', 'hidden', 'attached', 'enabled', 'disabled', 'editable', 'checked', 'empty', 'focused', 'text', 'containsText', 'value', 'values', 'count', 'attribute', 'css', 'id', 'property', 'accessibleName', 'accessibleDescription', 'inViewport', 'url', 'title', 'localStorage', 'sessionStorage']),
29
+ value: zod_1.z.unknown().optional(), attribute: zod_1.z.string().optional(), property: zod_1.z.string().optional(), key: zod_1.z.string().optional(),
30
+ matches: zod_1.z.string().optional(), flags: zod_1.z.string().regex(/^[imsu]*$/).optional(), not: zod_1.z.boolean().default(false), timeout: validation_1.positive.optional(),
31
+ }).strict();
32
+ async function assertBrowser(page, input, defaultTimeout, reporter) {
33
+ const data = schema.parse(input), timeout = data.timeout ?? defaultTimeout, started = Date.now();
34
+ const expected = data.matches !== undefined ? new RegExp(data.matches, data.flags) : data.value;
35
+ const equals = (actual) => expected instanceof RegExp ? typeof actual === 'string' && expected.test(actual) : (0, node_util_1.isDeepStrictEqual)(actual, expected);
36
+ const scope = data.frame ? page.frameLocator(data.frame) : page;
37
+ const elementCondition = !['url', 'title', 'localStorage', 'sessionStorage'].includes(data.condition);
38
+ if (elementCondition && !!data.selector === !!data.role)
39
+ throw new Error('Choose one selector or role.');
40
+ const locator = data.role ? scope.getByRole(data.role, { name: data.name, exact: true }) : scope.locator(data.selector || 'body');
41
+ const booleanConditions = ['visible', 'hidden', 'attached', 'enabled', 'disabled', 'editable', 'checked', 'empty', 'focused', 'inViewport'];
42
+ if (!booleanConditions.includes(data.condition) && data.condition !== 'attribute' && expected === undefined)
43
+ throw new Error('This assertion requires value or matches.');
44
+ if (data.condition === 'attribute' && !data.attribute || ['css', 'property'].includes(data.condition) && !data.property || ['localStorage', 'sessionStorage'].includes(data.condition) && !data.key)
45
+ throw new Error('Assertion requires an attribute, property, or storage key.');
46
+ if (data.condition === 'count' && (!Number.isSafeInteger(expected) || expected < 0))
47
+ throw new Error('Count requires a nonnegative integer.');
48
+ const target = elementCondition ? (data.role ? `${data.role}${data.name === undefined ? '' : ' named ' + JSON.stringify(data.name)}` : data.selector) + (data.frame ? ` inside frame ${data.frame}` : '') : data.key ? `${data.condition} key ${JSON.stringify(data.key)}` : `page ${data.condition}`;
49
+ const needsSingle = elementCondition && !['count', 'attached'].includes(data.condition);
50
+ let latest = (0, assertion_result_1.reportAssertion)('assert.that', target, [{ key: 'target', label: 'Target lookup: ' + target, status: 'running' }, { key: 'condition', label: 'condition: ' + data.condition, status: 'not_evaluated' }], reporter, { status: 'running', timeout, elapsed: 0 });
51
+ let closed = false, lastReadError;
52
+ const progress = (checks) => {
53
+ if (closed)
54
+ return;
55
+ latest = (0, assertion_result_1.reportAssertion)('assert.that', target, checks, reporter, { status: 'running', timeout, elapsed: Date.now() - started });
56
+ };
57
+ try {
58
+ await (0, validation_1.poll)(async () => {
59
+ let count, roleCount, actual, matched = false, evaluated = false;
60
+ let sensitive = !!data.key && /password|passcode|token|secret|authorization|cookie|api.?key|otp/i.test(data.key);
61
+ lastReadError = undefined;
62
+ try {
63
+ if (elementCondition) {
64
+ count = await locator.count();
65
+ roleCount = data.role && data.name !== undefined && count === 0 ? await scope.getByRole(data.role).count() : count;
66
+ if (count === 1 && (['value', 'values'].includes(data.condition) || data.attribute === 'value' || data.property === 'value')) {
67
+ sensitive ||= await locator.evaluate(element => {
68
+ const hints = ['type', 'name', 'id', 'autocomplete'].map(key => element.getAttribute(key) || '').join(' ');
69
+ return /password|passcode|token|secret|authorization|cookie|api.?key|one-time-code|otp/i.test(hints);
70
+ });
71
+ }
72
+ }
73
+ if (!needsSingle || count === 1 || count === 0 && ['visible', 'hidden'].includes(data.condition)) {
74
+ evaluated = true;
75
+ if (['accessibleName', 'accessibleDescription'].includes(data.condition))
76
+ await installAccessibilityNames(page);
77
+ switch (data.condition) {
78
+ case 'url':
79
+ actual = page.url();
80
+ matched = equals(actual);
81
+ break;
82
+ case 'title':
83
+ actual = await page.title();
84
+ matched = equals(actual);
85
+ break;
86
+ case 'localStorage':
87
+ case 'sessionStorage':
88
+ actual = await page.evaluate(({ kind, key }) => window[kind].getItem(key), { kind: data.condition, key: data.key });
89
+ matched = equals(actual);
90
+ break;
91
+ case 'count':
92
+ actual = count;
93
+ matched = equals(actual);
94
+ break;
95
+ case 'attached':
96
+ actual = count > 0;
97
+ matched = actual === true;
98
+ break;
99
+ case 'hidden':
100
+ actual = count === 0 || await locator.isHidden();
101
+ matched = actual === true;
102
+ break;
103
+ case 'visible':
104
+ actual = await locator.isVisible();
105
+ matched = actual === true;
106
+ break;
107
+ case 'enabled':
108
+ actual = await locator.isEnabled();
109
+ matched = actual === true;
110
+ break;
111
+ case 'disabled':
112
+ actual = await locator.isDisabled();
113
+ matched = actual === true;
114
+ break;
115
+ case 'editable':
116
+ actual = await locator.isEditable();
117
+ matched = actual === true;
118
+ break;
119
+ case 'checked':
120
+ actual = await locator.isChecked();
121
+ matched = actual === true;
122
+ break;
123
+ case 'focused':
124
+ actual = await locator.evaluate(element => element === element.ownerDocument.activeElement);
125
+ matched = actual === true;
126
+ break;
127
+ case 'empty':
128
+ actual = await locator.evaluate(element => (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element.value : element.textContent || '').length === 0);
129
+ matched = actual === true;
130
+ break;
131
+ case 'text':
132
+ actual = (await locator.textContent())?.replace(/\s+/g, ' ').trim();
133
+ matched = equals(actual);
134
+ break;
135
+ case 'containsText':
136
+ actual = (await locator.textContent() || '').replace(/\s+/g, ' ').trim();
137
+ matched = expected instanceof RegExp ? expected.test(String(actual)) : String(actual).includes(String(expected));
138
+ break;
139
+ case 'value':
140
+ actual = await locator.inputValue();
141
+ matched = equals(actual);
142
+ break;
143
+ case 'values':
144
+ actual = await locator.evaluate(element => Array.from(element.selectedOptions).map(option => option.value));
145
+ matched = equals(actual);
146
+ break;
147
+ case 'attribute':
148
+ actual = await locator.getAttribute(data.attribute);
149
+ matched = expected === undefined ? actual !== null : equals(actual);
150
+ break;
151
+ case 'id':
152
+ actual = await locator.getAttribute('id');
153
+ matched = equals(actual);
154
+ break;
155
+ case 'css':
156
+ actual = await locator.evaluate((element, key) => getComputedStyle(element).getPropertyValue(key), data.property);
157
+ matched = equals(actual);
158
+ break;
159
+ case 'property':
160
+ actual = await locator.evaluate((element, key) => element[key], data.property);
161
+ matched = equals(actual);
162
+ break;
163
+ case 'accessibleName':
164
+ case 'accessibleDescription':
165
+ actual = await locator.evaluate((element, condition) => { const api = element.ownerDocument.defaultView.__FLOWSTRIDE_ACCESSIBILITY_NAMES__; return condition === 'accessibleName' ? api.computeAccessibleName(element) : api.computeAccessibleDescription(element); }, data.condition);
166
+ matched = equals(actual);
167
+ break;
168
+ case 'inViewport':
169
+ actual = await locator.evaluate(element => { const box = element.getBoundingClientRect(); return box.width > 0 && box.height > 0 && box.bottom > 0 && box.right > 0 && box.top < innerHeight && box.left < innerWidth; });
170
+ matched = actual === true;
171
+ break;
172
+ }
173
+ if (data.not)
174
+ matched = !matched;
175
+ }
176
+ }
177
+ catch (error) {
178
+ lastReadError = (0, assertion_result_1.diagnosticValue)(error instanceof Error ? error.message : String(error));
179
+ evaluated = false;
180
+ matched = false;
181
+ }
182
+ const checks = [];
183
+ if (elementCondition) {
184
+ const absenceAllowed = count === 0 && matched;
185
+ const lookup = { key: 'target', label: count === 0 && absenceAllowed ? 'Target absent as expected' : 'Target found: ' + target, status: count === undefined ? 'failed' : count > 0 || absenceAllowed ? 'passed' : data.condition === 'count' ? 'not_evaluated' : 'failed', actual: count === undefined ? 'Could not resolve the target' : `${count} matching target(s)` };
186
+ if (lastReadError && count === undefined)
187
+ lookup.reason = lastReadError;
188
+ const part = (key, label, found) => ({ key, label, status: found === true ? 'passed' : absenceAllowed || count === undefined || data.condition === 'count' ? 'not_evaluated' : found === false ? 'failed' : 'not_evaluated' });
189
+ lookup.children = [];
190
+ if (data.role)
191
+ lookup.children.push(part('role', 'role: ' + data.role, roleCount === undefined ? undefined : roleCount > 0));
192
+ if (data.name !== undefined)
193
+ lookup.children.push(part('name', 'name: ' + data.name, count === undefined || !roleCount ? undefined : count > 0));
194
+ if (data.selector)
195
+ lookup.children.push(part('selector', 'selector: ' + data.selector, count === undefined ? undefined : count > 0));
196
+ checks.push(lookup);
197
+ if (needsSingle && count !== 0)
198
+ checks.push(count === undefined ? { key: 'unique', label: 'Exactly one matching target', status: 'not_evaluated' } : (0, assertion_result_1.assertionCheck)('unique', 'Exactly one matching target', count === 1, 1, count));
199
+ else if (needsSingle)
200
+ checks.push({ key: 'unique', label: 'Exactly one matching target', status: 'not_evaluated', reason: absenceAllowed ? 'A matching element is not required for this condition.' : 'Target lookup did not find an element.' });
201
+ }
202
+ const opposites = { visible: 'not visible', hidden: 'visible', attached: 'absent', enabled: 'disabled', disabled: 'enabled', editable: 'not editable', checked: 'unchecked', empty: 'not empty', focused: 'not focused', inViewport: 'outside the viewport' };
203
+ const positiveLabel = { inViewport: 'inside the viewport' };
204
+ const wanted = sensitive ? '[REDACTED]' : booleanConditions.includes(data.condition) ? (data.not ? opposites[data.condition] : positiveLabel[data.condition] || data.condition) : data.condition === 'attribute' && expected === undefined ? 'present' : `${data.not ? 'not ' : ''}${expected instanceof RegExp ? expected.toString() : (0, assertion_result_1.diagnosticValue)(expected)}`;
205
+ const observed = sensitive ? '[REDACTED]' : booleanConditions.includes(data.condition) && typeof actual === 'boolean' ? (actual ? positiveLabel[data.condition] || data.condition : opposites[data.condition]) : (0, assertion_result_1.diagnosticValue)(actual);
206
+ checks.push(evaluated ? (0, assertion_result_1.assertionCheck)('condition', 'condition: ' + data.condition + (data.attribute ? ' ' + data.attribute : data.property ? ' ' + data.property : ''), matched, wanted, observed) : { key: 'condition', label: 'condition: ' + data.condition, status: 'not_evaluated', expected: wanted, reason: lastReadError || 'The target could not be resolved uniquely.' });
207
+ if (lastReadError && checks.at(-1))
208
+ checks.at(-1).reason = lastReadError;
209
+ progress(checks);
210
+ return evaluated && matched;
211
+ }, timeout, `${data.condition} ${target}`);
212
+ closed = true;
213
+ reporter?.({ ...latest, status: 'passed', elapsed: Date.now() - started });
214
+ }
215
+ catch (error) {
216
+ closed = true;
217
+ const result = { ...latest, status: 'failed', elapsed: Date.now() - started, reason: (0, assertion_result_1.diagnosticValue)(error instanceof Error ? error.message : String(error)) };
218
+ if (!result.checks.some(check => check.status === 'failed'))
219
+ result.checks.push({ key: 'read', label: 'Assertion could be evaluated', status: 'failed', reason: lastReadError || result.reason });
220
+ reporter?.(result);
221
+ throw new assertion_result_1.AssertionFailure(result);
222
+ }
223
+ }
@@ -0,0 +1,3 @@
1
+ import type { FlowstrideConfig } from '../types';
2
+ /** Old Cloud engines remain usable for their existing commands. */
3
+ export declare function needsNativeBrowserProtocol(target: string, config: FlowstrideConfig): boolean;
@@ -0,0 +1,16 @@
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.needsNativeBrowserProtocol = needsNativeBrowserProtocol;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const lexer_1 = require("../parser/lexer");
9
+ const projectPaths_1 = require("../utils/projectPaths");
10
+ /** Old Cloud engines remain usable for their existing commands. */
11
+ function needsNativeBrowserProtocol(target, config) {
12
+ if (config.browser && config.browser !== 'chromium' || Object.keys(config.browserOptions || {}).length ||
13
+ config.retries || config.failOnFlaky || config.trace || config.har || config.domSnapshots || config.shard)
14
+ return true;
15
+ return (0, projectPaths_1.discoverFlows)(target, config.projectRoot || process.cwd()).some(file => new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise().some(token => token.type === lexer_1.TokenType.ALIAS_COMMAND && /^flow\.(browser|assert|network|download|clock|stub|trace|har|a11y|component)\./i.test(token.value)));
16
+ }
@@ -0,0 +1,12 @@
1
+ import type { Page } from 'playwright';
2
+ import type { FlowstrideConfig } from '../types';
3
+ export declare class NativeComponents {
4
+ private config;
5
+ private server?;
6
+ private page?;
7
+ private mounted;
8
+ private framework?;
9
+ constructor(config: FlowstrideConfig);
10
+ execute(page: Page, action: string, input: unknown): Promise<void>;
11
+ dispose(): Promise<void>;
12
+ }
@@ -0,0 +1,122 @@
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.NativeComponents = void 0;
7
+ const zod_1 = require("zod");
8
+ const node_module_1 = require("node:module");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const projectPaths_1 = require("../utils/projectPaths");
11
+ const node_crypto_1 = require("node:crypto");
12
+ const schema = zod_1.z.object({ module: zod_1.z.string().min(1), framework: zod_1.z.enum(['vanilla', 'react', 'vue']), export: zod_1.z.string().regex(/^[a-zA-Z_$][\w$]*$/).default('default'), props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).default({}), css: zod_1.z.array(zod_1.z.string()).default([]) }).strict();
13
+ class NativeComponents {
14
+ config;
15
+ server;
16
+ page;
17
+ mounted = false;
18
+ framework;
19
+ constructor(config) {
20
+ this.config = config;
21
+ }
22
+ async execute(page, action, input) {
23
+ if (action === 'mount') {
24
+ const data = schema.parse(input);
25
+ await this.dispose();
26
+ const root = this.config.projectRoot || process.cwd(), module = (0, projectPaths_1.projectPath)(root, data.module);
27
+ const css = data.css.map(file => '/' + node_path_1.default.relative(root, (0, projectPaths_1.projectPath)(root, file)).split(node_path_1.default.sep).join('/'));
28
+ const projectRequire = (0, node_module_1.createRequire)(node_path_1.default.join(root, 'package.json'));
29
+ // Resolve project framework runtimes before starting any server.
30
+ if (data.framework === 'react') {
31
+ projectRequire.resolve('react');
32
+ projectRequire.resolve('react-dom/client');
33
+ }
34
+ if (data.framework === 'vue')
35
+ projectRequire.resolve('vue');
36
+ const { createServer } = await import('vite');
37
+ const plugins = [];
38
+ if (data.framework === 'react') {
39
+ const { default: react } = await import('@vitejs/plugin-react');
40
+ plugins.push(react({ jsxRuntime: 'automatic' }));
41
+ }
42
+ if (data.framework === 'vue') {
43
+ const { default: vue } = await import('@vitejs/plugin-vue');
44
+ plugins.push(vue());
45
+ }
46
+ const entry = `/__flowstride_component_${(0, node_crypto_1.randomUUID)()}`;
47
+ const moduleURL = '/' + node_path_1.default.relative(root, module).split(node_path_1.default.sep).join('/');
48
+ const importComponent = `import { ${data.export} as component } from ${JSON.stringify(moduleURL)};`;
49
+ const styles = css.map(file => `import ${JSON.stringify(file)};`).join('\n');
50
+ let kernel;
51
+ if (data.framework === 'react')
52
+ kernel = `import React from 'react'; import {createRoot} from 'react-dom/client'; const root = createRoot(target); export function update(props){root.render(React.createElement(component,props));} export function unmount(){root.unmount();}`;
53
+ else if (data.framework === 'vue')
54
+ kernel = `import {createApp,h,reactive} from 'vue'; const props=reactive({}); const app=createApp({render:()=>h(component,props)}); app.mount(target); export function update(next){for(const key of Object.keys(props))delete props[key];Object.assign(props,next);} export function unmount(){app.unmount();}`;
55
+ else
56
+ kernel = `let current; export function update(props){current?.unmount?.();target.replaceChildren();current=component(target,props);if(current instanceof Node)target.appendChild(current);} export function unmount(){current?.unmount?.();target.replaceChildren();}`;
57
+ const source = `${importComponent}\n${styles}\nconst target=document.getElementById('flowstride-component-root');\n${kernel}`;
58
+ plugins.push({ name: 'flowstride-component-module', resolveId(id) { if (id === entry + '.js')
59
+ return '\0' + entry; }, load(id) { if (id === '\0' + entry)
60
+ return source; } });
61
+ plugins.push({ name: 'flowstride-component-entry', configureServer(server) {
62
+ server.middlewares.use((req, res, next) => {
63
+ const host = `127.0.0.1:${server.httpServer.address()?.port}`;
64
+ if (req.headers.host !== host || req.headers.origin && req.headers.origin !== `http://${host}`) {
65
+ res.statusCode = 403;
66
+ res.end('Forbidden');
67
+ return;
68
+ }
69
+ if (req.url?.split('?')[0] !== entry)
70
+ return next();
71
+ res.setHeader('Content-Type', 'text/html');
72
+ res.setHeader('Cache-Control', 'no-store');
73
+ res.end('<!doctype html><html><head><title>Flowstride component test</title></head><body><div id="flowstride-component-root"></div></body></html>');
74
+ });
75
+ } });
76
+ this.server = await createServer({ configFile: false, root, plugins, logLevel: 'error', cacheDir: (0, projectPaths_1.projectPath)(root, '.flowstride/component-cache'),
77
+ server: { host: '127.0.0.1', port: 0, cors: false, hmr: false, watch: null, fs: { strict: true, allow: [root], deny: ['.env', '.env.*', '**/.flowstride/**', '**/.git/**', '*.{crt,pem,key}'] } },
78
+ optimizeDeps: { noDiscovery: true, include: data.framework === 'react' ? ['react', 'react-dom/client'] : data.framework === 'vue' ? ['vue'] : [] }, appType: 'custom' });
79
+ try {
80
+ await this.server.listen();
81
+ const address = this.server.httpServer.address();
82
+ await page.goto(`http://127.0.0.1:${address.port}${entry}`);
83
+ await page.evaluate(async ({ entry, props }) => {
84
+ const mod = await import(entry + '.js');
85
+ window.__FLOWSTRIDE_COMPONENT__ = mod;
86
+ mod.update(props);
87
+ }, { entry, props: data.props });
88
+ this.page = page;
89
+ this.mounted = true;
90
+ this.framework = data.framework;
91
+ }
92
+ catch (error) {
93
+ await this.dispose();
94
+ throw error;
95
+ }
96
+ return;
97
+ }
98
+ if (!this.mounted || page !== this.page)
99
+ throw new Error('Mount a component on this page first.');
100
+ if (action === 'update') {
101
+ const data = zod_1.z.object({ props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) }).strict().parse(input);
102
+ await page.evaluate(props => window.__FLOWSTRIDE_COMPONENT__.update(props), data.props);
103
+ return;
104
+ }
105
+ if (action === 'unmount') {
106
+ zod_1.z.object({}).strict().parse(input);
107
+ await page.evaluate(() => window.__FLOWSTRIDE_COMPONENT__.unmount());
108
+ this.mounted = false;
109
+ return;
110
+ }
111
+ throw new Error(`Unknown component action: ${action}`);
112
+ }
113
+ async dispose() {
114
+ if (this.mounted && this.page && !this.page.isClosed())
115
+ await this.page.evaluate(() => window.__FLOWSTRIDE_COMPONENT__?.unmount()).catch(() => { });
116
+ this.mounted = false;
117
+ this.page = undefined;
118
+ await this.server?.close();
119
+ this.server = undefined;
120
+ }
121
+ }
122
+ exports.NativeComponents = NativeComponents;
@@ -0,0 +1,7 @@
1
+ import type { FlowstrideConfig } from '../types';
2
+ /** The dashboard has its own browser; it never shares test cookies or hooks. */
3
+ export declare class DashboardBrowser {
4
+ private browser?;
5
+ open(url: string, engine?: FlowstrideConfig['browser']): Promise<void>;
6
+ close(): Promise<void>;
7
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DashboardBrowser = void 0;
4
+ const playwright_1 = require("playwright");
5
+ /** The dashboard has its own browser; it never shares test cookies or hooks. */
6
+ class DashboardBrowser {
7
+ browser;
8
+ async open(url, engine = 'chromium') {
9
+ if (engine === 'chromium') {
10
+ const { default: open } = await import('open');
11
+ await open(url);
12
+ return;
13
+ }
14
+ if (engine !== 'firefox' && engine !== 'webkit')
15
+ throw new Error('Unknown dashboard browser.');
16
+ try {
17
+ this.browser = await (engine === 'firefox' ? playwright_1.firefox : playwright_1.webkit).launch({ headless: false });
18
+ const page = await this.browser.newPage();
19
+ await page.goto(url);
20
+ }
21
+ catch (error) {
22
+ await this.close();
23
+ throw error;
24
+ }
25
+ }
26
+ async close() {
27
+ const browser = this.browser;
28
+ this.browser = undefined;
29
+ await browser?.close();
30
+ }
31
+ }
32
+ exports.DashboardBrowser = DashboardBrowser;
@@ -0,0 +1,2 @@
1
+ /** Only commands whose validated contract accepts a timeout may override it. */
2
+ export declare function nativeStepTimeout(command: string, input: unknown, fallback: number): number;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nativeStepTimeout = nativeStepTimeout;
4
+ const validation_1 = require("./validation");
5
+ /** Only commands whose validated contract accepts a timeout may override it. */
6
+ function nativeStepTimeout(command, input, fallback) {
7
+ if (['assert.that', 'a11y.match', 'a11y.snapshot', 'stub.expect'].includes(command) &&
8
+ input !== null && typeof input === 'object' && !Array.isArray(input) &&
9
+ Object.hasOwn(input, 'timeout')) {
10
+ return validation_1.positive.parse(input.timeout);
11
+ }
12
+ return fallback;
13
+ }
@@ -0,0 +1,35 @@
1
+ import { type AssertionReporter } from '../reporting/assertion_result';
2
+ import type { FlowstrideConfig } from '../types';
3
+ import { WebAdapter } from '../adapters/web/playwright';
4
+ export declare class ArmedEvent<T> {
5
+ private detach;
6
+ private settled;
7
+ private timer;
8
+ private resolve;
9
+ readonly promise: Promise<{
10
+ value?: T;
11
+ error?: Error;
12
+ }>;
13
+ constructor(timeout: number, detach: () => void);
14
+ capture(value: T): void;
15
+ fail(error: Error): void;
16
+ take(): Promise<T>;
17
+ }
18
+ export declare function matchesURL(pattern: string, url: string): boolean;
19
+ export declare class NativeEvents {
20
+ private web;
21
+ private config;
22
+ private routes;
23
+ private events;
24
+ private captured;
25
+ private routeErrors;
26
+ private generation;
27
+ private downloads;
28
+ private savedDownloads;
29
+ constructor(web: WebAdapter, config: FlowstrideConfig);
30
+ private key;
31
+ network(action: string, input: unknown, reporter?: AssertionReporter): Promise<any>;
32
+ download(action: string, input: unknown, reporter?: AssertionReporter): Promise<any>;
33
+ disposeProfile(profile: string): Promise<void>;
34
+ dispose(): Promise<void>;
35
+ }