flowstride 1.0.21 → 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 (81) hide show
  1. package/NATIVE_BROWSER.md +40 -0
  2. package/NATIVE_CI.md +14 -2
  3. package/NATIVE_WORKFLOWS.md +44 -0
  4. package/README.md +8 -1
  5. package/dist/adapters/api/undici.d.ts +3 -1
  6. package/dist/adapters/api/undici.js +17 -3
  7. package/dist/adapters/web/playwright.js +23 -73
  8. package/dist/ci/runtime.js +9 -1
  9. package/dist/cli/commands/run.js +14 -4
  10. package/dist/cli/manual_server/workspace_server.js +90 -74
  11. package/dist/manual/security.d.ts +4 -4
  12. package/dist/native/accessibility.d.ts +2 -1
  13. package/dist/native/accessibility.js +22 -6
  14. package/dist/native/assertions.d.ts +2 -1
  15. package/dist/native/assertions.js +174 -91
  16. package/dist/native/deadline.d.ts +2 -0
  17. package/dist/native/deadline.js +13 -0
  18. package/dist/native/events.d.ts +3 -2
  19. package/dist/native/events.js +35 -8
  20. package/dist/native/functions.d.ts +2 -1
  21. package/dist/native/functions.js +34 -6
  22. package/dist/native/runtime.d.ts +3 -0
  23. package/dist/native/runtime.js +12 -6
  24. package/dist/parser/assertion_contract.d.ts +12 -0
  25. package/dist/parser/assertion_contract.js +56 -0
  26. package/dist/parser/parser.js +14 -6
  27. package/dist/reporting/assertion_result.d.ts +36 -0
  28. package/dist/reporting/assertion_result.js +104 -0
  29. package/dist/reporting/pdf_generator.js +25 -20
  30. package/dist/reporting/runtime_failure.d.ts +10 -0
  31. package/dist/reporting/runtime_failure.js +64 -0
  32. package/dist/reporting/step_status.d.ts +10 -0
  33. package/dist/reporting/step_status.js +44 -0
  34. package/dist/runner/flow_orchestrator.d.ts +1 -0
  35. package/dist/runner/flow_orchestrator.js +8 -3
  36. package/dist/runner/flow_worker.d.ts +8 -1
  37. package/dist/runner/flow_worker.js +119 -41
  38. package/dist/runner/plugin_manager.js +3 -0
  39. package/dist/runner/temporary_store.d.ts +1 -0
  40. package/dist/runner/temporary_store.js +3 -0
  41. package/dist/sdk/api_proxy.d.ts +3 -0
  42. package/dist/sdk/api_proxy.js +16 -0
  43. package/dist/security/localWorkspace.d.ts +2 -0
  44. package/dist/security/localWorkspace.js +17 -0
  45. package/dist/security/redaction.js +1 -1
  46. package/dist/types/index.d.ts +3 -0
  47. package/examples/native-workflows/README.md +144 -0
  48. package/examples/native-workflows/api-and-ui/README.md +80 -0
  49. package/examples/native-workflows/api-and-ui/flows/api-and-ui.flow +27 -0
  50. package/examples/native-workflows/api-and-ui/negative/wrong-backend-state.flow +27 -0
  51. package/examples/native-workflows/api-ui-otp/README.md +140 -0
  52. package/examples/native-workflows/api-ui-otp/templates/api-ui-otp.flow +31 -0
  53. package/examples/native-workflows/api-ui-otp/templates/existing-account-otp.flow +20 -0
  54. package/examples/native-workflows/audio-to-text/README.md +131 -0
  55. package/examples/native-workflows/audio-to-text/fixtures/microphone-tone.wav +0 -0
  56. package/examples/native-workflows/audio-to-text/flows/microphone-demo.flow +23 -0
  57. package/examples/native-workflows/audio-to-text/negative/wrong-transcript.flow +23 -0
  58. package/examples/native-workflows/audio-to-text/templates/real-transcription.flow +25 -0
  59. package/examples/native-workflows/iframes/README.md +116 -0
  60. package/examples/native-workflows/iframes/flows/explicit-frame-plugin.flow +20 -0
  61. package/examples/native-workflows/iframes/flows/readable-frame.flow +13 -0
  62. package/examples/native-workflows/iframes/negative/wrong-frame-result.flow +13 -0
  63. package/examples/native-workflows/iframes/plugins/frame-actions.ts +7 -0
  64. package/examples/native-workflows/manual-testing/README.md +85 -0
  65. package/examples/native-workflows/manual-testing/fixtures/profile-review-mismatch.json +93 -0
  66. package/examples/native-workflows/manual-testing/fixtures/profile-review.json +93 -0
  67. package/examples/native-workflows/shared/server.mjs +80 -0
  68. package/examples/native-workflows/tables-and-cells/README.md +118 -0
  69. package/examples/native-workflows/tables-and-cells/flows/coordinates.flow +17 -0
  70. package/examples/native-workflows/tables-and-cells/flows/search-extract-reuse.flow +27 -0
  71. package/examples/native-workflows/tables-and-cells/negative/wrong-cell-value.flow +27 -0
  72. package/examples/native-workflows/variables-and-reuse/README.md +140 -0
  73. package/examples/native-workflows/variables-and-reuse/flows/reuse.flow +9 -0
  74. package/examples/native-workflows/variables-and-reuse/flows/setup.first.flow +27 -0
  75. package/examples/native-workflows/variables-and-reuse/negative/wrong-reused-value.flow +27 -0
  76. package/examples/native-workflows/variables-and-reuse/sessions/restore.flow +10 -0
  77. package/examples/native-workflows/variables-and-reuse/sessions/save.flow +29 -0
  78. package/package.json +11 -8
  79. package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
  80. package/src/ui/dist/index.html +1 -1
  81. package/src/ui/dist/assets/index-DglsL_J2.js +0 -636
package/NATIVE_BROWSER.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Native browser workflows
2
2
 
3
+ For API/UI composition, email OTP, iframe examples, audio-to-text, tables, variables and manual testing, start with [Core Flowstride workflows](NATIVE_WORKFLOWS.md).
4
+
3
5
  OS and Cloud support these commands directly in `.flow` files. Existing commands, plugins and saved sessions continue to work. Use `flow.<area>.<action>` followed by a JSON object. Unknown actions and options fail explicitly.
4
6
 
5
7
  Keep both engine installations current when using these features. If an installed Cloud engine lacks the required capability protocol, the public runner warns and uses OS fallback for the run. Existing commands can still use an older compatible Cloud engine; Cloud-only services require a compatible Cloud installation.
@@ -72,6 +74,8 @@ Chromium remains the default. Browser installation must match the installed Flow
72
74
 
73
75
  Validated settings include viewport/screen, device scale, touch/mobile emulation, locale/timezone, color/contrast/motion, JavaScript, HTTPS handling, CSP bypass, permissions/geolocation, offline mode, headers, HTTP credentials, proxy, downloads, strict selectors, storage state and project-local client certificate paths. Use `browserOptions` for the default profile and `options` in `flow.browser.create` for a separate profile. Secrets belong in environment variables or private local files.
74
76
 
77
+ Set `BASE_URL` directly in a project `.env` file when you want a common application URL. String values in native `.flow` command payloads support environment templates. The JSON configuration loader does not generally expand `{{env.NAME}}` throughout arbitrary `browserOptions`; use supported native profile options for command-time credential interpolation.
78
+
75
79
  Runtime commands include `flow.browser.offline` with `value`, `flow.browser.permissions` with `permissions` and optional `origin`, `flow.browser.clearPermissions`, and `flow.browser.geolocation` with latitude/longitude. Recording paths are controlled by evidence settings, rather than arbitrary browser path options. JavaScript logger callbacks are not JSON DSL settings.
76
80
 
77
81
  ## Assertions that wait for a condition
@@ -339,3 +343,39 @@ For later runs, use `--shard-history path/to/timings.json` from a previous compl
339
343
  Chromium's current stealth behavior can override an explicitly requested locale. Use Firefox or WebKit for locale-sensitive checks and verify the observed language. For HAR playback in Firefox/WebKit, leave forced browser-offline mode unset and stop the real server to verify the recording; combining forced offline mode and HAR can reject navigation before fulfillment.
340
344
 
341
345
  The maintained command reference, browser compatibility guide and API-to-UI examples are available in the [Flowstride documentation](https://docs.flowstride.io/guide/native-browser-workflows).
346
+
347
+ ## Assertion results and `And`
348
+
349
+ Native checks `flow.assert.that`, `flow.network.expect`, `flow.download.expect`,
350
+ `flow.stub.expect`, `flow.a11y.scan` and `flow.a11y.match` can appear under
351
+ `Then` or `And`. State-changing browser, route and clock commands belong under
352
+ `Given` or `When`.
353
+
354
+ ```flow
355
+ Then "The login button is ready"
356
+ flow.assert.that {
357
+ "role": "button",
358
+ "name": "Submit",
359
+ "condition": "enabled",
360
+ "timeout": 10000
361
+ };
362
+ And "The heading is visible"
363
+ flow.assert.that {
364
+ "role": "heading",
365
+ "name": "Welcome",
366
+ "condition": "visible"
367
+ };
368
+ ```
369
+
370
+ Terminal, CI JSON/JUnit and dashboard evidence retain individual lookup and
371
+ condition results. A failing condition does not erase successful target checks.
372
+ Missing prerequisites and later steps are marked not evaluated / not tested.
373
+ Expected and observed values, timeout, profile and source location explain a
374
+ failure. These child diagnostics do not count as extra test steps.
375
+
376
+ All command failures retain the original adapter error, call log and nested
377
+ causes, plus command, file/line and relevant target/resource. This applies to
378
+ browser actions, APIs, plugins, sessions, games, audio and email workflows.
379
+ Timeouts are reported as observed timeouts, without inventing a root cause when
380
+ the application does not provide one. Secrets and terminal control characters
381
+ are protected in diagnostics.
package/NATIVE_CI.md CHANGED
@@ -85,9 +85,9 @@ flow.use session "browser";
85
85
 
86
86
  Attribute comparisons wait up to the configured timeout. Exact matching is case-sensitive; `contains` checks a substring. Missing attributes fail even when the expected value is empty.
87
87
 
88
- Sessions capture cookies, local storage, session storage and the current HTTP(S) storage origin. Session storage is restored immediately on the matching origin or before its first matching navigation. It is not seeded into unrelated origins. The initial seed is then removed so later application changes and reloads are preserved.
88
+ Sessions capture cookies, local storage, session storage and the current HTTP(S) storage origin. Local storage and session storage are restored immediately on the matching origin or before its first matching navigation. It is not seeded into unrelated origins. The initial seed is then removed so later application changes and reloads are preserved.
89
89
 
90
- Existing cookie/local-storage snapshots remain compatible. Older snapshots with session storage but no origin use the current application origin or configured `baseUrl`; open the application first or set `baseUrl` if neither is available. Restoration merges saved keys into storage. This is a single-origin storage snapshot.
90
+ Existing cookie/local-storage snapshots remain compatible. Older snapshots with Web Storage but no origin prefer the configured application `baseUrl` (including `BASE_URL`), then an available current HTTP(S) application page. Configure the intended base URL before restoring a legacy record. Restoration merges saved keys into storage. This is a single-origin storage snapshot.
91
91
 
92
92
  ## Interactive history and debugging
93
93
 
@@ -96,3 +96,15 @@ Each completed flow tab restores its own last captured screen. Selecting an earl
96
96
  The local cURL preview remains `[REDACTED]`. Copy uses the original captured request held in interactive debugger memory. Already-redacted historical data cannot be recovered. CI and Cloud reports retain redaction.
97
97
 
98
98
  Review artifact access and test data: credential redaction does not remove every possible application secret, and screenshots or videos can show sensitive information.
99
+
100
+ ## Runnable core workflow examples
101
+
102
+ [Core Flowstride workflows](NATIVE_WORKFLOWS.md) covers API + UI, email OTP, iFrames, audio/transcripts, tables/cells, variables and manual testing. The [shared setup](examples/native-workflows/README.md) explains the local demo application's lifecycle and CI configuration.
103
+
104
+ After `npm ci` and matching browser installation, a CI job can run a named local example with:
105
+
106
+ ```sh
107
+ npx --no-install flowstride run workflow-examples/api-and-ui/flows/api-and-ui.flow --ci
108
+ ```
109
+
110
+ In CI, `--no-install` makes a missing local Flowstride installation fail rather than fetching a package. Ordinary interactive commands use `npx flowstride`. Configure the local app through `webServer`, and exclude `negative/` examples and unconfigured service templates from your passing CI target.
@@ -0,0 +1,44 @@
1
+ # Core Flowstride workflows
2
+
3
+ Flowstride can call an API, extract data from its response, use that data in a browser, and verify the application outcome in the same `.flow` test. It also supports table/cell targeting, browser sessions, microphone input and visual manual testing.
4
+
5
+ The examples below use the normal Flowstride CLI. Their small HTTP server is a local demonstration application; it does not replace the test runner.
6
+
7
+ ## Choose a workflow
8
+
9
+ | Workflow | What you will do | Availability |
10
+ | --- | --- | --- |
11
+ | [API + UI in one test](examples/native-workflows/api-and-ui/README.md) | Create a user through an API, sign in through the browser, then verify backend state | OS and Cloud; Chromium, Firefox, WebKit |
12
+ | [API + UI + email OTP](examples/native-workflows/api-ui-otp/README.md) | Use FlowMail to retrieve a delivered code and complete application authentication | Cloud; a supported inbox and an application that sends OTP email |
13
+ | [iFrames](examples/native-workflows/iframes/README.md) | Find a readable child-frame target, assert inside an explicit frame, and extend an action with a focused plugin | OS and Cloud; Chromium, Firefox, WebKit |
14
+ | [Audio-to-text workflows](examples/native-workflows/audio-to-text/README.md) | Inject microphone audio, drive recording controls and check the resulting transcript | OS and Cloud; audio injection requires Chromium |
15
+ | [Tables and cells](examples/native-workflows/tables-and-cells/README.md) | Search a table, click a specific row action, extract a cell and reuse its value on another page | OS and Cloud; Chromium, Firefox, WebKit |
16
+ | [Variables, reuse and saved sessions](examples/native-workflows/variables-and-reuse/README.md) | Reuse generated/extracted values, understand scope, and restore saved browser state | OS and Cloud; Chromium, Firefox, WebKit |
17
+ | [Manual testing](examples/native-workflows/manual-testing/README.md) | Load a design, execute its steps, record defects/results and reopen saved history | OS and Cloud; local Manual Workspace |
18
+
19
+ Start with the [shared setup](examples/native-workflows/README.md). It installs Flowstride with `npm i flowstride`, copies the examples already included in the package, and starts a loopback-only demo application. No separate starter download is required.
20
+
21
+ ## OS and Cloud
22
+
23
+ OS works locally without an account. To connect Cloud, run:
24
+
25
+ ```sh
26
+ npx flowstride login
27
+ ```
28
+
29
+ Use your Cloud developer token when prompted and accept the authenticated engine installation when offered. Cloud has free accounts; availability and usage allowances depend on the workspace feature and plan. Email OTP retrieval uses the Cloud FlowMail service. The other local examples do not need a Cloud service connection.
30
+
31
+ The public CLI selects a compatible, authorized Cloud engine when available and retains OS fallback. A Cloud-only command still requires its Cloud service prerequisites.
32
+
33
+ ## Reading and running the examples
34
+
35
+ - `flows/` contains runnable examples for the shared demo application.
36
+ - `negative/` contains deliberate failures. Run them individually and expect a nonzero exit code.
37
+ - `templates/` contains application-specific workflows. Configure the documented application, credentials and selectors before using them.
38
+ - Manual designs are JSON files; a human performs their actions and records the result.
39
+
40
+ Run the named file from its chapter. Running the whole example tree also discovers intentional failures and service-dependent templates.
41
+
42
+ Each chapter explains its prerequisites, expected result, negative check and limitations. For browser profiles, native assertions, network mocking, downloads, traces/HAR, accessibility and component testing, use [NATIVE_BROWSER.md](NATIVE_BROWSER.md). For CI, reports and games, use [NATIVE_CI.md](NATIVE_CI.md).
43
+
44
+ The maintained full reference is at [docs.flowstride.io](https://docs.flowstride.io/).
package/README.md CHANGED
@@ -7,7 +7,7 @@ Flowstride OS is an open source framework for browser, API and manual testing. W
7
7
  ## Requirements
8
8
 
9
9
  - Node.js 22.12 or newer.
10
- - Chromium installed through Flowstride for browser tests.
10
+ - A matching Chromium, Firefox or WebKit installation for browser tests. Chromium is the default; audio-file microphone injection requires Chromium.
11
11
 
12
12
  ## Get started
13
13
 
@@ -41,6 +41,13 @@ Then "The sign-in button is visible"
41
41
 
42
42
  Replace the URL and assertion with values from your own application.
43
43
 
44
+ ## Workflow guides
45
+
46
+ - [Core workflows](./NATIVE_WORKFLOWS.md): API + UI, email OTP, iFrames, audio-to-text, tables/cells, variables and manual testing.
47
+ - [Runnable examples and local setup](./examples/native-workflows/README.md).
48
+ - [Native browser workflows](./NATIVE_BROWSER.md): profiles, assertions, network control, downloads, traces/HAR, accessibility and components.
49
+ - [Native CI and games](./NATIVE_CI.md).
50
+
44
51
  ## Native CI
45
52
 
46
53
  ```sh
@@ -9,13 +9,15 @@ export declare class ApiAdapter {
9
9
  private config;
10
10
  private globalHeaders;
11
11
  private onUnauthorized?;
12
+ private unauthorizedStatus?;
13
+ private refreshing;
12
14
  private lastStatus;
13
15
  private lastBody;
14
16
  private lastHeaders;
15
17
  private lastResponseTime;
16
18
  constructor(config: FlowstrideConfig);
17
19
  setGlobalHeader(name: string, value: string): void;
18
- setUnauthorizedHandler(handler: () => Promise<void>): void;
20
+ setUnauthorizedHandler(handler: () => Promise<void>, status?: number): void;
19
21
  getState(): ApiState;
20
22
  checkpointState(): {
21
23
  globalHeaders: Record<string, string>;
@@ -6,6 +6,8 @@ class ApiAdapter {
6
6
  config;
7
7
  globalHeaders = {};
8
8
  onUnauthorized;
9
+ unauthorizedStatus;
10
+ refreshing = false;
9
11
  lastStatus = null;
10
12
  lastBody = null;
11
13
  lastHeaders = null;
@@ -16,8 +18,12 @@ class ApiAdapter {
16
18
  setGlobalHeader(name, value) {
17
19
  this.globalHeaders[name] = value;
18
20
  }
19
- setUnauthorizedHandler(handler) {
21
+ setUnauthorizedHandler(handler, status) {
22
+ if (status !== undefined && status !== 401 && status !== 403) {
23
+ throw new Error('Authentication refresh status must be 401 or 403.');
24
+ }
20
25
  this.onUnauthorized = handler;
26
+ this.unauthorizedStatus = status;
21
27
  }
22
28
  getState() {
23
29
  if (this.lastStatus === null) {
@@ -115,8 +121,16 @@ class ApiAdapter {
115
121
  };
116
122
  };
117
123
  let res = await makeRequest();
118
- if ((res.status === 401 || res.status === 403) && this.onUnauthorized) {
119
- await this.onUnauthorized();
124
+ if ((res.status === 401 || res.status === 403) && this.onUnauthorized &&
125
+ !this.refreshing && (this.unauthorizedStatus === undefined || res.status === this.unauthorizedStatus)) {
126
+ // A refresh can use this adapter too. Never recursively refresh its own rejection.
127
+ this.refreshing = true;
128
+ try {
129
+ await this.onUnauthorized();
130
+ }
131
+ finally {
132
+ this.refreshing = false;
133
+ }
120
134
  Object.assign(requestHeaders, this.globalHeaders);
121
135
  res = await makeRequest();
122
136
  }
@@ -133,7 +133,8 @@ class WebAdapter {
133
133
  const profile = this.profiles.get(key);
134
134
  if (!profile)
135
135
  return;
136
- await this.clearSessionStorageRestore(profile.page);
136
+ for (const page of profile.context.pages())
137
+ await this.clearSessionStorageRestore(page);
137
138
  if (profile.tracing)
138
139
  await this.endTrace(key);
139
140
  const video = profile.page.video();
@@ -208,6 +209,7 @@ class WebAdapter {
208
209
  page = null;
209
210
  discoveredPages = [];
210
211
  dialogHandler = null;
212
+ // One-shot initializers for both Web Storage types, owned by their exact page.
211
213
  sessionStorageRestores = new Map();
212
214
  get pendingSessionStorageRestore() { return this.sessionStorageRestores.values().next().value || null; }
213
215
  async clearSessionStorageRestore(target) {
@@ -216,6 +218,7 @@ class WebAdapter {
216
218
  continue;
217
219
  this.sessionStorageRestores.delete(page);
218
220
  pending.page.off('framenavigated', pending.onNavigation);
221
+ pending.page.off('close', pending.onClose);
219
222
  try {
220
223
  await pending.script.dispose();
221
224
  }
@@ -404,10 +407,11 @@ class WebAdapter {
404
407
  return undefined;
405
408
  }
406
409
  };
410
+ const hasLocalStorage = !!localStorage && Object.keys(localStorage).length > 0;
407
411
  const hasSessionStorage = !!sessionStorage && Object.keys(sessionStorage).length > 0;
408
412
  const origin = storageOrigin !== undefined ? originOf(storageOrigin)
409
- : originOf(page.url()) || originOf(this.config.baseUrl);
410
- if (hasSessionStorage && !origin) {
413
+ : originOf(this.config.baseUrl) || originOf(page.url());
414
+ if ((hasLocalStorage || hasSessionStorage) && !origin) {
411
415
  throw new Error('Session storage restoration requires a saved HTTP(S) origin, an open application page, or baseUrl.');
412
416
  }
413
417
  if (cookies && cookies.length > 0) {
@@ -440,46 +444,36 @@ class WebAdapter {
440
444
  }
441
445
  await this.context.addCookies(formattedCookies);
442
446
  }
443
- if (localStorage && Object.keys(localStorage).length > 0) {
444
- await this.page.addInitScript((data) => {
445
- for (const [k, v] of Object.entries(data)) {
446
- window.localStorage.setItem(k, v);
447
- }
448
- }, localStorage);
449
- if (this.page.url() !== "about:blank") {
450
- await this.page.evaluate((data) => {
451
- try {
452
- for (const [k, v] of Object.entries(data))
453
- window.localStorage.setItem(k, v);
454
- }
455
- catch (e) { }
456
- }, localStorage);
457
- }
458
- }
459
- if (hasSessionStorage) {
447
+ if (hasLocalStorage || hasSessionStorage) {
460
448
  const restore = (data) => {
449
+ // A matching iframe inside another site must never receive saved secrets.
461
450
  if (window !== window.top || window.location.origin !== data.origin)
462
451
  return;
463
- for (const [key, value] of Object.entries(data.values))
452
+ for (const [key, value] of Object.entries(data.local))
453
+ window.localStorage.setItem(key, value);
454
+ for (const [key, value] of Object.entries(data.session))
464
455
  window.sessionStorage.setItem(key, value);
465
456
  };
466
- const data = { origin: origin, values: sessionStorage };
457
+ const data = { origin: origin, local: localStorage || {}, session: sessionStorage || {} };
467
458
  if (originOf(page.url()) === origin) {
459
+ // Already on the saved origin: restore now without a persistent initializer.
468
460
  await page.evaluate(restore, data);
469
461
  }
470
462
  else {
471
- // Seed before application scripts run; remove the seed after the first
472
- // matching document so reloads preserve later application changes.
463
+ // Seed before application scripts run; remove after the first matching
464
+ // main document so later reloads preserve application changes and logouts.
473
465
  const script = await page.addInitScript(restore, data);
474
466
  const onNavigation = (frame) => {
475
467
  if (frame !== page.mainFrame() || originOf(frame.url()) !== origin)
476
468
  return;
477
469
  void this.clearSessionStorageRestore(page).catch(() => {
478
- console.warn('[Session]: Could not remove the completed session storage initializer.');
470
+ console.warn('[Session]: Could not remove the completed storage initializer.');
479
471
  });
480
472
  };
481
- this.sessionStorageRestores.set(page, { page, script, onNavigation });
473
+ const onClose = () => { void this.clearSessionStorageRestore(page).catch(() => { }); };
474
+ this.sessionStorageRestores.set(page, { page, script, onNavigation, onClose });
482
475
  page.on('framenavigated', onNavigation);
476
+ page.on('close', onClose);
483
477
  }
484
478
  }
485
479
  }
@@ -678,53 +672,9 @@ class WebAdapter {
678
672
  const targetLoc = await this.checkAmbiguity(loc, selector);
679
673
  await targetLoc.waitFor({ state: "visible" });
680
674
  await targetLoc.scrollIntoViewIfNeeded();
681
- await targetLoc.focus({ timeout: 3000 }).catch(() => { });
682
- await targetLoc
683
- .evaluate((node) => {
684
- if (typeof node.focus === "function")
685
- node.focus();
686
- })
687
- .catch(() => { });
688
- let box = null;
689
- try {
690
- box = await targetLoc.boundingBox({ timeout: 3000 });
691
- }
692
- catch (e) { }
693
- if (box) {
694
- const centerX = box.x + box.width / 2;
695
- const centerY = box.y + box.height / 2;
696
- await this.page.mouse.move(centerX, centerY, { steps: 5 });
697
- }
698
- await targetLoc.hover({ force: true, timeout: 3000 }).catch(() => { });
699
- await targetLoc
700
- .evaluate((node) => {
701
- const rect = node.getBoundingClientRect();
702
- const x = rect.left + rect.width / 2;
703
- const y = rect.top + rect.height / 2;
704
- const events = [
705
- "pointerover",
706
- "pointerenter",
707
- "mouseover",
708
- "mouseenter",
709
- "mousemove",
710
- ];
711
- events.forEach((type) => {
712
- node.dispatchEvent(new MouseEvent(type, {
713
- view: window,
714
- bubbles: true,
715
- cancelable: true,
716
- clientX: x,
717
- clientY: y,
718
- screenX: x,
719
- screenY: y,
720
- }));
721
- });
722
- })
723
- .catch(() => { });
724
- await targetLoc
725
- .click({ delay: 50, force: true, timeout: 3000 })
726
- .catch(() => { });
727
- await this.page.waitForTimeout(600);
675
+ // A real pointer hover preserves browser actionability checks and must not
676
+ // focus, click, or synthesize duplicate application events.
677
+ await targetLoc.hover();
728
678
  }
729
679
  async type(selector, value, elementType) {
730
680
  const loc = await this.getLocator(selector, elementType, false, false);
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runNativeCI = runNativeCI;
7
+ const assertion_result_1 = require("../reporting/assertion_result");
7
8
  const distribution_1 = require("./distribution");
8
9
  const node_crypto_1 = require("node:crypto");
9
10
  const projectPaths_1 = require("../utils/projectPaths");
@@ -210,7 +211,7 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
210
211
  config.artifactsDir = artifactRoot;
211
212
  node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'timings.json'), JSON.stringify(timings, null, 2));
212
213
  node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'summary.json'), JSON.stringify(result, null, 2));
213
- const cases = results.map(step => `<testcase classname="${xml(step.fileName)}" name="${xml(step.description || step.id)}">${step.status === 'failed' ? `<failure message="${xml(step.error || 'Test step failed')}"/>` : ['skipped', 'not_tested'].includes(step.status) ? '<skipped/>' : ''}</testcase>`);
214
+ const cases = results.map(step => `<testcase classname="${xml(step.fileName)}" name="${xml(step.description || step.id)}">${step.status === 'failed' ? `<failure message="${xml(step.error || 'Test step failed')}"/>` : ['skipped', 'not_tested'].includes(step.status) ? '<skipped/>' : ''}${step.assertion ? `<system-out>${xml((0, assertion_result_1.assertionText)(step.assertion))}</system-out>` : ''}</testcase>`);
214
215
  for (const error of errors)
215
216
  cases.push(`<testcase name="execution"><error message="${xml(error)}"/></testcase>`);
216
217
  if (incomplete)
@@ -218,5 +219,12 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
218
219
  node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'junit.xml'), `<?xml version="1.0"?><testsuite name="Flowstride" tests="${cases.length}" failures="${metrics.failed + (incomplete ? 1 : 0)}" errors="${errors.length}" skipped="${metrics.skipped}">${cases.join('')}</testsuite>`);
219
220
  if (process.env.GITHUB_STEP_SUMMARY)
220
221
  node_fs_1.default.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n### Flowstride · ${config.device}\nResult: **${result.status}** (${engine})\n\nPassed: ${metrics.passed}; failed: ${metrics.failed}; skipped: ${metrics.skipped}; incomplete: ${metrics.incomplete}.\n`);
222
+ if (process.env.GITHUB_STEP_SUMMARY) {
223
+ const failures = results.filter(step => step.status === 'failed').map(step => `<details><summary>${xml(step.commandLabel || step.command || 'Step')} · ${xml(step.fileName)}</summary><pre>${xml(step.error || (step.assertion ? (0, assertion_result_1.assertionText)(step.assertion) : 'Failed without an adapter diagnostic.'))}</pre></details>`);
224
+ for (const error of errors)
225
+ failures.push(`<pre>${xml(error)}</pre>`);
226
+ if (failures.length)
227
+ node_fs_1.default.appendFileSync(process.env.GITHUB_STEP_SUMMARY, '\n' + failures.join('\n') + '\n');
228
+ }
221
229
  return result;
222
230
  }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runAction = runAction;
37
+ const runtime_failure_1 = require("../../reporting/runtime_failure");
37
38
  const compatibility_1 = require("../../native/compatibility");
38
39
  const projectPaths_1 = require("../../utils/projectPaths");
39
40
  const config_1 = require("../../ci/config");
@@ -124,12 +125,14 @@ async function runAction(targetPath, options) {
124
125
  throw new Error('Installed Pro engine does not support native CI protocol 1; install the matching Pro candidate.');
125
126
  if ((0, compatibility_1.needsNativeBrowserProtocol)(resolvedPath, config) && (proWorkerModule.FlowWorker.nativeBrowserProtocol !== 2 || proOrchModule.FlowOrchestrator.nativeBrowserProtocol !== 2))
126
127
  throw new Error('Installed Cloud engine does not support these native browser capabilities. Install the matching Cloud archive; this run will use OS fallback.');
128
+ if ((0, compatibility_1.needsNativeBrowserProtocol)(resolvedPath, config) && (proWorkerModule.FlowWorker.nativeAssertionProtocol !== 1 || proOrchModule.FlowOrchestrator.nativeAssertionProtocol !== 1))
129
+ throw new Error('Installed Cloud engine predates structured assertion reporting; this run will use OS fallback. Install the matching Cloud archive to update.');
127
130
  ActiveFlowWorker = proWorkerModule.FlowWorker;
128
131
  ActiveFlowOrchestrator = proOrchModule.FlowOrchestrator;
129
132
  engineMode = "Enterprise (Pro)";
130
133
  }
131
134
  catch (err) {
132
- console.warn(`\n[Warning]: Found @flowstride/pro but failed to boot it. Falling back to Open Source engine. (${err.message})`);
135
+ console.warn(`\n[Warning]: Found @flowstride/pro but failed to boot it. Falling back to Open Source engine. (${(0, runtime_failure_1.runtimeFailure)("run.setup", { path: resolvedPath }, err, { file: resolvedPath })})`);
133
136
  }
134
137
  }
135
138
  console.log(`\n[Engine]: Booting Flowstride in ${engineMode} mode...`);
@@ -140,9 +143,16 @@ async function runAction(targetPath, options) {
140
143
  console.log(`[Flowstride]: ${result.status}; reports: ${config.artifactsDir}`);
141
144
  process.exit(result.status === 'passed' ? 0 : result.status === 'interrupted' ? 130 : 1);
142
145
  }
146
+ const previousRunEvent = config.onRunEvent;
147
+ config.onRunEvent = event => {
148
+ previousRunEvent?.(event);
149
+ if (event.type === 'RUN_COMPLETE')
150
+ process.exitCode = event.payload?.status === 'success' ? 0 : ['aborted', 'interrupted'].includes(event.payload?.status) ? 130 : 1;
151
+ };
143
152
  try {
144
- await orchestrator.start(resolvedPath);
145
- console.log("\n[Flowstride]: Execution suite completed successfully.");
153
+ const isSuccess = await orchestrator.start(resolvedPath);
154
+ process.exitCode = isSuccess ? 0 : 1;
155
+ console.log(isSuccess ? "\n[Flowstride]: Execution suite completed successfully." : "\n[Flowstride]: Execution suite failed.");
146
156
  if (isCI) {
147
157
  const artifactsDir = path.resolve(root, "flowstride-artifacts");
148
158
  if (!fs.existsSync(artifactsDir)) {
@@ -157,7 +167,7 @@ async function runAction(targetPath, options) {
157
167
  }
158
168
  }
159
169
  catch (error) {
160
- console.error("\n[Fatal Error]:", error.message);
170
+ console.error("\n[Fatal Error]:", (0, runtime_failure_1.runtimeFailure)("run.setup", { path: resolvedPath }, error, { file: resolvedPath }));
161
171
  if (isCI) {
162
172
  console.log(`[CI Alert]: Terminating pipeline with exit code 1 to block deployment.`);
163
173
  }