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.
- package/NATIVE_BROWSER.md +40 -0
- package/NATIVE_CI.md +14 -2
- package/NATIVE_WORKFLOWS.md +44 -0
- package/README.md +8 -1
- package/dist/adapters/api/undici.d.ts +3 -1
- package/dist/adapters/api/undici.js +17 -3
- package/dist/adapters/web/playwright.js +23 -73
- package/dist/ci/runtime.js +9 -1
- package/dist/cli/commands/run.js +14 -4
- package/dist/cli/manual_server/workspace_server.js +90 -74
- package/dist/manual/security.d.ts +4 -4
- package/dist/native/accessibility.d.ts +2 -1
- package/dist/native/accessibility.js +22 -6
- package/dist/native/assertions.d.ts +2 -1
- package/dist/native/assertions.js +174 -91
- package/dist/native/deadline.d.ts +2 -0
- package/dist/native/deadline.js +13 -0
- package/dist/native/events.d.ts +3 -2
- package/dist/native/events.js +35 -8
- package/dist/native/functions.d.ts +2 -1
- package/dist/native/functions.js +34 -6
- package/dist/native/runtime.d.ts +3 -0
- package/dist/native/runtime.js +12 -6
- package/dist/parser/assertion_contract.d.ts +12 -0
- package/dist/parser/assertion_contract.js +56 -0
- package/dist/parser/parser.js +14 -6
- package/dist/reporting/assertion_result.d.ts +36 -0
- package/dist/reporting/assertion_result.js +104 -0
- package/dist/reporting/pdf_generator.js +25 -20
- package/dist/reporting/runtime_failure.d.ts +10 -0
- package/dist/reporting/runtime_failure.js +64 -0
- package/dist/reporting/step_status.d.ts +10 -0
- package/dist/reporting/step_status.js +44 -0
- package/dist/runner/flow_orchestrator.d.ts +1 -0
- package/dist/runner/flow_orchestrator.js +8 -3
- package/dist/runner/flow_worker.d.ts +8 -1
- package/dist/runner/flow_worker.js +119 -41
- package/dist/runner/plugin_manager.js +3 -0
- package/dist/runner/temporary_store.d.ts +1 -0
- package/dist/runner/temporary_store.js +3 -0
- package/dist/sdk/api_proxy.d.ts +3 -0
- package/dist/sdk/api_proxy.js +16 -0
- package/dist/security/localWorkspace.d.ts +2 -0
- package/dist/security/localWorkspace.js +17 -0
- package/dist/security/redaction.js +1 -1
- package/dist/types/index.d.ts +3 -0
- package/examples/native-workflows/README.md +144 -0
- package/examples/native-workflows/api-and-ui/README.md +80 -0
- package/examples/native-workflows/api-and-ui/flows/api-and-ui.flow +27 -0
- package/examples/native-workflows/api-and-ui/negative/wrong-backend-state.flow +27 -0
- package/examples/native-workflows/api-ui-otp/README.md +140 -0
- package/examples/native-workflows/api-ui-otp/templates/api-ui-otp.flow +31 -0
- package/examples/native-workflows/api-ui-otp/templates/existing-account-otp.flow +20 -0
- package/examples/native-workflows/audio-to-text/README.md +131 -0
- package/examples/native-workflows/audio-to-text/fixtures/microphone-tone.wav +0 -0
- package/examples/native-workflows/audio-to-text/flows/microphone-demo.flow +23 -0
- package/examples/native-workflows/audio-to-text/negative/wrong-transcript.flow +23 -0
- package/examples/native-workflows/audio-to-text/templates/real-transcription.flow +25 -0
- package/examples/native-workflows/iframes/README.md +116 -0
- package/examples/native-workflows/iframes/flows/explicit-frame-plugin.flow +20 -0
- package/examples/native-workflows/iframes/flows/readable-frame.flow +13 -0
- package/examples/native-workflows/iframes/negative/wrong-frame-result.flow +13 -0
- package/examples/native-workflows/iframes/plugins/frame-actions.ts +7 -0
- package/examples/native-workflows/manual-testing/README.md +85 -0
- package/examples/native-workflows/manual-testing/fixtures/profile-review-mismatch.json +93 -0
- package/examples/native-workflows/manual-testing/fixtures/profile-review.json +93 -0
- package/examples/native-workflows/shared/server.mjs +80 -0
- package/examples/native-workflows/tables-and-cells/README.md +118 -0
- package/examples/native-workflows/tables-and-cells/flows/coordinates.flow +17 -0
- package/examples/native-workflows/tables-and-cells/flows/search-extract-reuse.flow +27 -0
- package/examples/native-workflows/tables-and-cells/negative/wrong-cell-value.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/README.md +140 -0
- package/examples/native-workflows/variables-and-reuse/flows/reuse.flow +9 -0
- package/examples/native-workflows/variables-and-reuse/flows/setup.first.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/negative/wrong-reused-value.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/sessions/restore.flow +10 -0
- package/examples/native-workflows/variables-and-reuse/sessions/save.flow +29 -0
- package/package.json +11 -8
- package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
- package/src/ui/dist/index.html +1 -1
- 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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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.
|
|
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,
|
|
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
|
|
472
|
-
//
|
|
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
|
|
470
|
+
console.warn('[Session]: Could not remove the completed storage initializer.');
|
|
479
471
|
});
|
|
480
472
|
};
|
|
481
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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);
|
package/dist/ci/runtime.js
CHANGED
|
@@ -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
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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. (${
|
|
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
|
-
|
|
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]:",
|
|
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
|
}
|