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.
- package/NATIVE_BROWSER.md +381 -0
- package/NATIVE_CI.md +17 -5
- package/NATIVE_WORKFLOWS.md +44 -0
- package/README.md +11 -4
- package/dist/adapters/api/undici.d.ts +11 -1
- package/dist/adapters/api/undici.js +28 -3
- package/dist/adapters/web/playwright.d.ts +22 -2
- package/dist/adapters/web/playwright.js +254 -107
- package/dist/ci/config.js +14 -1
- package/dist/ci/distribution.d.ts +38 -0
- package/dist/ci/distribution.js +134 -0
- package/dist/ci/init.d.ts +4 -1
- package/dist/ci/init.js +13 -2
- package/dist/ci/runtime.js +33 -6
- package/dist/cli/commands/run.js +17 -4
- package/dist/cli/index.js +24 -5
- package/dist/cli/manual_server/workspace_server.js +90 -74
- package/dist/manual/security.d.ts +6 -6
- package/dist/native/accessibility.d.ts +4 -0
- package/dist/native/accessibility.js +84 -0
- package/dist/native/assertions.d.ts +3 -0
- package/dist/native/assertions.js +223 -0
- package/dist/native/compatibility.d.ts +3 -0
- package/dist/native/compatibility.js +16 -0
- package/dist/native/components.d.ts +12 -0
- package/dist/native/components.js +122 -0
- package/dist/native/dashboard.d.ts +7 -0
- package/dist/native/dashboard.js +32 -0
- package/dist/native/deadline.d.ts +2 -0
- package/dist/native/deadline.js +13 -0
- package/dist/native/events.d.ts +35 -0
- package/dist/native/events.js +308 -0
- package/dist/native/evidence.d.ts +3 -0
- package/dist/native/evidence.js +31 -0
- package/dist/native/functions.d.ts +10 -0
- package/dist/native/functions.js +131 -0
- package/dist/native/runtime.d.ts +17 -0
- package/dist/native/runtime.js +160 -0
- package/dist/native/validation.d.ts +107 -0
- package/dist/native/validation.js +84 -0
- package/dist/parser/assertion_contract.d.ts +12 -0
- package/dist/parser/assertion_contract.js +56 -0
- package/dist/parser/lexer.js +15 -4
- package/dist/parser/parser.js +15 -7
- 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 +5 -0
- package/dist/runner/flow_orchestrator.js +30 -8
- package/dist/runner/flow_worker.d.ts +15 -1
- package/dist/runner/flow_worker.js +219 -42
- 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 +13 -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 +23 -9
- package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/index-B7MuBTWm.js +0 -460
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Search tables, target cells and reuse their values
|
|
2
|
+
|
|
3
|
+
Flowstride can interact with a table through readable controls, exact selectors or indexed coordinates. It can extract a cell's text and use it in another page in the same test.
|
|
4
|
+
|
|
5
|
+
| Requirement | Value |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Engine | OS or Cloud |
|
|
8
|
+
| Browser | Chromium, Firefox or WebKit |
|
|
9
|
+
| Application | Shared local demo |
|
|
10
|
+
| Cloud account | Optional |
|
|
11
|
+
|
|
12
|
+
Complete the [shared setup](../README.md) and leave the demo application running.
|
|
13
|
+
|
|
14
|
+
## Search, choose an action and reuse a cell
|
|
15
|
+
|
|
16
|
+
The demo has two orders: FS-101 is Pending and FS-102 is Approved. Its search control hides unmatched rows; selecting a row or its View button opens that order's details.
|
|
17
|
+
|
|
18
|
+
[search-extract-reuse.flow](flows/search-extract-reuse.flow):
|
|
19
|
+
|
|
20
|
+
```flow
|
|
21
|
+
Feature: Table and cell reuse
|
|
22
|
+
Scenario: Search, extract and reuse a table value
|
|
23
|
+
Given "The orders table is open"
|
|
24
|
+
flow.open "/orders";
|
|
25
|
+
When "Find the approved order"
|
|
26
|
+
flow.type input "Search orders" "FS-102";
|
|
27
|
+
flow.assert.that {
|
|
28
|
+
"selector": "#orders tbody tr:not([hidden])",
|
|
29
|
+
"condition": "count",
|
|
30
|
+
"value": 1
|
|
31
|
+
};
|
|
32
|
+
flow.assert.that {
|
|
33
|
+
"selector": "#orders tbody tr:has(td:text-is('FS-102')) td:nth-child(3)",
|
|
34
|
+
"condition": "text",
|
|
35
|
+
"value": "Approved"
|
|
36
|
+
};
|
|
37
|
+
When "Extract the order and open its row action"
|
|
38
|
+
flow.extract text "table[0] tr[2] td[0]" as "selectedOrder";
|
|
39
|
+
flow.click button "View" inside "table[0] tr[2] td[3]";
|
|
40
|
+
And "The selected order is shown"
|
|
41
|
+
flow.expect text "Order @selectedOrder";
|
|
42
|
+
When "Reuse the cell on another page"
|
|
43
|
+
flow.open "/reuse";
|
|
44
|
+
flow.type input "Selected value" "@selectedOrder";
|
|
45
|
+
flow.click button "Apply value";
|
|
46
|
+
Then "The reused value remains correct"
|
|
47
|
+
flow.expect text "Selected FS-102";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx flowstride run workflow-examples/tables-and-cells/flows/search-extract-reuse.flow --ci
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Expected: one visible matching row, Approved in the selected status cell, `Order FS-102` after the exact row action, and `Selected FS-102` after navigation and reuse. The run exits `0`.
|
|
55
|
+
|
|
56
|
+
`flow.type` uses the application's search input. The application implements filtering. The assertion excludes rows hidden by this fixture's `hidden` attribute; adapt it if your app removes rows or uses another hiding mechanism.
|
|
57
|
+
|
|
58
|
+
The extraction stores the text as `@selectedOrder`. Extraction itself does not assert that the value is correct; the following outcome checks do.
|
|
59
|
+
|
|
60
|
+
## Indexed table/cell targets
|
|
61
|
+
|
|
62
|
+
[coordinates.flow](flows/coordinates.flow) demonstrates direct cell selection and a repeated button inside an exact cell:
|
|
63
|
+
|
|
64
|
+
```flow
|
|
65
|
+
Feature: Table coordinates and exact cell actions
|
|
66
|
+
|
|
67
|
+
Scenario: Select a cell and a repeated row action
|
|
68
|
+
Given "The unfiltered orders table is open"
|
|
69
|
+
flow.open "/orders";
|
|
70
|
+
|
|
71
|
+
When "Select the first cell of the second data row"
|
|
72
|
+
flow.click "table[0] tr[2] td[0]";
|
|
73
|
+
|
|
74
|
+
And "The second order is selected"
|
|
75
|
+
flow.expect text "Order FS-102";
|
|
76
|
+
|
|
77
|
+
When "Click the button in the first data row's action cell"
|
|
78
|
+
flow.click button "View" inside "table[0] tr[1] td[3]";
|
|
79
|
+
|
|
80
|
+
Then "The first order is selected"
|
|
81
|
+
flow.expect text "Order FS-101";
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npx flowstride run workflow-examples/tables-and-cells/flows/coordinates.flow --ci
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Expected: clicking `table[0] tr[2] td[0]` selects FS-102, then clicking View in the first data row's action cell selects FS-101.
|
|
89
|
+
|
|
90
|
+
| Target | Meaning in this fixture |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `table[0]` | First table |
|
|
93
|
+
| `table[0] tr[0]` | Header row |
|
|
94
|
+
| `table[0] tr[1]` | First data row |
|
|
95
|
+
| `table[0] tr[2] td[0]` | First data cell of the second data row |
|
|
96
|
+
| `table[0] tr[1] td[3]` | Action cell of the first data row |
|
|
97
|
+
|
|
98
|
+
Indexes are zero-based. Header rows count as rows, and hidden matching DOM rows can still affect indexes. CSS `nth-child` positions, used in the status assertion, are one-based.
|
|
99
|
+
|
|
100
|
+
## Deliberate failure
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx flowstride run workflow-examples/tables-and-cells/negative/wrong-cell-value.flow --ci
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
That file expects Rejected where the fixture contains Approved. It must fail, retain evidence and exit nonzero.
|
|
107
|
+
|
|
108
|
+
## Adapting to changing tables
|
|
109
|
+
|
|
110
|
+
For sorted, filtered or paginated tables, prefer a stable record identifier when position is not what you are testing. For example:
|
|
111
|
+
|
|
112
|
+
```flow
|
|
113
|
+
flow.click "#orders tbody tr:has(td:text-is('FS-102')) button";
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The match must identify the intended record uniquely. Pagination, virtual scrolling and data loading remain application behavior: navigate or load the row before targeting it. An indexed HTML-table helper is not a universal query language for every virtual grid or ARIA treegrid.
|
|
117
|
+
|
|
118
|
+
See the [table reference](https://docs.flowstride.io/flow-language/flow-elements/table) and [variable reuse](../variables-and-reuse/README.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Feature: Table coordinates and exact cell actions
|
|
2
|
+
|
|
3
|
+
Scenario: Select a cell and a repeated row action
|
|
4
|
+
Given "The unfiltered orders table is open"
|
|
5
|
+
flow.open "/orders";
|
|
6
|
+
|
|
7
|
+
When "Select the first cell of the second data row"
|
|
8
|
+
flow.click "table[0] tr[2] td[0]";
|
|
9
|
+
|
|
10
|
+
And "The second order is selected"
|
|
11
|
+
flow.expect text "Order FS-102";
|
|
12
|
+
|
|
13
|
+
When "Click the button in the first data row's action cell"
|
|
14
|
+
flow.click button "View" inside "table[0] tr[1] td[3]";
|
|
15
|
+
|
|
16
|
+
Then "The first order is selected"
|
|
17
|
+
flow.expect text "Order FS-101";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Feature: Table and cell reuse
|
|
2
|
+
Scenario: Search, extract and reuse a table value
|
|
3
|
+
Given "The orders table is open"
|
|
4
|
+
flow.open "/orders";
|
|
5
|
+
When "Find the approved order"
|
|
6
|
+
flow.type input "Search orders" "FS-102";
|
|
7
|
+
flow.assert.that {
|
|
8
|
+
"selector": "#orders tbody tr:not([hidden])",
|
|
9
|
+
"condition": "count",
|
|
10
|
+
"value": 1
|
|
11
|
+
};
|
|
12
|
+
flow.assert.that {
|
|
13
|
+
"selector": "#orders tbody tr:has(td:text-is('FS-102')) td:nth-child(3)",
|
|
14
|
+
"condition": "text",
|
|
15
|
+
"value": "Approved"
|
|
16
|
+
};
|
|
17
|
+
When "Extract the order and open its row action"
|
|
18
|
+
flow.extract text "table[0] tr[2] td[0]" as "selectedOrder";
|
|
19
|
+
flow.click button "View" inside "table[0] tr[2] td[3]";
|
|
20
|
+
And "The selected order is shown"
|
|
21
|
+
flow.expect text "Order @selectedOrder";
|
|
22
|
+
When "Reuse the cell on another page"
|
|
23
|
+
flow.open "/reuse";
|
|
24
|
+
flow.type input "Selected value" "@selectedOrder";
|
|
25
|
+
flow.click button "Apply value";
|
|
26
|
+
Then "The reused value remains correct"
|
|
27
|
+
flow.expect text "Selected FS-102";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Feature: Table and cell reuse
|
|
2
|
+
Scenario: Search, extract and reuse a table value
|
|
3
|
+
Given "The orders table is open"
|
|
4
|
+
flow.open "/orders";
|
|
5
|
+
When "Find the approved order"
|
|
6
|
+
flow.type input "Search orders" "FS-102";
|
|
7
|
+
flow.assert.that {
|
|
8
|
+
"selector": "#orders tbody tr:not([hidden])",
|
|
9
|
+
"condition": "count",
|
|
10
|
+
"value": 1
|
|
11
|
+
};
|
|
12
|
+
flow.assert.that {
|
|
13
|
+
"selector": "#orders tbody tr:has(td:text-is('FS-102')) td:nth-child(3)",
|
|
14
|
+
"condition": "text",
|
|
15
|
+
"value": "Rejected"
|
|
16
|
+
};
|
|
17
|
+
When "Extract the order and open its row action"
|
|
18
|
+
flow.extract text "table[0] tr[2] td[0]" as "selectedOrder";
|
|
19
|
+
flow.click button "View" inside "table[0] tr[2] td[3]";
|
|
20
|
+
And "The selected order is shown"
|
|
21
|
+
flow.expect text "Order @selectedOrder";
|
|
22
|
+
When "Reuse the cell on another page"
|
|
23
|
+
flow.open "/reuse";
|
|
24
|
+
flow.type input "Selected value" "@selectedOrder";
|
|
25
|
+
flow.click button "Apply value";
|
|
26
|
+
Then "The reused value remains correct"
|
|
27
|
+
flow.expect text "Selected FS-102";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Variables, reuse and saved browser sessions
|
|
2
|
+
|
|
3
|
+
Flowstride can generate test data, extract values from APIs or pages, reuse them in later commands and persist a browser session. These mechanisms have different scopes and lifetimes.
|
|
4
|
+
|
|
5
|
+
| Requirement | Value |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Engine | OS or Cloud |
|
|
8
|
+
| Browser | Chromium, Firefox or WebKit |
|
|
9
|
+
| Application | Shared local demo |
|
|
10
|
+
| Cloud account | Optional |
|
|
11
|
+
|
|
12
|
+
Complete the [shared setup](../README.md), including `EXAMPLE_LABEL="Workflow fixture"`, and keep the same demo-server process running for the session examples.
|
|
13
|
+
|
|
14
|
+
## Generate once, extract and reuse
|
|
15
|
+
|
|
16
|
+
[setup.first.flow](flows/setup.first.flow) sends generated data to the demo API, extracts it and reuses it in another API call and the browser:
|
|
17
|
+
|
|
18
|
+
```flow
|
|
19
|
+
Feature: Generated and extracted variable reuse
|
|
20
|
+
Scenario: Preserve one generated value across API and UI
|
|
21
|
+
Given "Generate data once and extract the response"
|
|
22
|
+
flow.post "/api/echo" with reqBody;
|
|
23
|
+
"""
|
|
24
|
+
{
|
|
25
|
+
"email": "$randomEmail@example.test",
|
|
26
|
+
"label": "{{env.EXAMPLE_LABEL}}"
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
flow.extract resBody "email" as "rememberedEmail";
|
|
30
|
+
flow.extract resBody "label" as global "sharedLabel";
|
|
31
|
+
When "The generated value is reused in a second API request"
|
|
32
|
+
flow.post "/api/echo" with reqBody;
|
|
33
|
+
"""
|
|
34
|
+
{
|
|
35
|
+
"email": "@rememberedEmail"
|
|
36
|
+
}
|
|
37
|
+
"""
|
|
38
|
+
And "Generation did not silently create another value"
|
|
39
|
+
flow.expect resBody "email" equals "@randomEmail@example.test";
|
|
40
|
+
When "Reuse the extraction in a browser field"
|
|
41
|
+
flow.open "/reuse";
|
|
42
|
+
flow.type input "Selected value" "@rememberedEmail";
|
|
43
|
+
flow.click button "Apply value";
|
|
44
|
+
Then "The browser received the exact value"
|
|
45
|
+
flow.expect text "Selected @rememberedEmail";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`$randomEmail` generates a value; `@randomEmail` reuses the last generated value for that generator in this file. The suffix is literal: `$randomEmail@example.test` and `@randomEmail@example.test` keep the same complete address. A later `$randomEmail` call generates another value.
|
|
49
|
+
|
|
50
|
+
`flow.extract ... as "rememberedEmail"` stores a file-local extraction. `as global "sharedLabel"` makes the extraction explicitly process-global.
|
|
51
|
+
|
|
52
|
+
[reuse.flow](flows/reuse.flow) reads that global value from the next file:
|
|
53
|
+
|
|
54
|
+
```flow
|
|
55
|
+
Feature: Explicit process-global reuse
|
|
56
|
+
Scenario: Read a global extraction in the next file
|
|
57
|
+
Given "Open the reuse form"
|
|
58
|
+
flow.open "/reuse";
|
|
59
|
+
When "Use the explicitly global label"
|
|
60
|
+
flow.type input "Selected value" "@global[sharedLabel]";
|
|
61
|
+
flow.click button "Apply value";
|
|
62
|
+
Then "The environment value survived the file boundary"
|
|
63
|
+
flow.expect text "Selected Workflow fixture";
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Run the two files together, sequentially:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx flowstride run workflow-examples/variables-and-reuse/flows --workers 1 --ci
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `.first.flow` setup executes before the ordinary file. Expected: generated-email equality, exact UI reuse and `Selected Workflow fixture` in the second file. Both files pass.
|
|
73
|
+
|
|
74
|
+
## Scope and lifetime
|
|
75
|
+
|
|
76
|
+
| Mechanism | Use | Lifetime |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `{{env.NAME}}` | Application URLs, configuration values and private test credentials | Supplied by the process and loaded environment files |
|
|
79
|
+
| `$randomEmail` / `@randomEmail` | Generate a value / reuse its last remembered value | Generator memory resets for each file |
|
|
80
|
+
| `@rememberedEmail` | Reuse a local extraction | Current flow file, including later scenarios in that file |
|
|
81
|
+
| `@global[sharedLabel]` | Reuse an explicit global extraction | Current worker process; not shared memory across workers or machines |
|
|
82
|
+
| Browser `localStorage` / `sessionStorage` | Application browser storage | Browser profile/origin and application lifecycle |
|
|
83
|
+
| Persisted named session | Restore a saved browser snapshot in a later run | Local `.flowstride` session data until replaced/removed |
|
|
84
|
+
|
|
85
|
+
Keep the global example at one worker. A Cloud parallel run can use several processes; a global variable is not a shared cross-worker database. A later CLI invocation starts a new process and does not inherit these global variables. Undefined global references fail explicitly.
|
|
86
|
+
|
|
87
|
+
## Save and restore browser state
|
|
88
|
+
|
|
89
|
+
Run the [session-saving flow](sessions/save.flow):
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npx flowstride run workflow-examples/variables-and-reuse/sessions/save.flow --ci
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It creates a demo user, signs in through the UI and uses:
|
|
96
|
+
|
|
97
|
+
```flow
|
|
98
|
+
flow.save session "workflow-reader" persist;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then run [restore.flow](sessions/restore.flow) in a fresh CLI invocation:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npx flowstride run workflow-examples/variables-and-reuse/sessions/restore.flow --ci
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
That file uses:
|
|
108
|
+
|
|
109
|
+
```flow
|
|
110
|
+
Feature: Restore a persisted account
|
|
111
|
+
Scenario: Restore in a fresh process
|
|
112
|
+
Given "Load the saved browser state"
|
|
113
|
+
flow.use session "workflow-reader";
|
|
114
|
+
When "Open the account page"
|
|
115
|
+
flow.open "/dashboard";
|
|
116
|
+
Then "Cookies and both storage types are restored"
|
|
117
|
+
flow.expect visible "Welcome, Workflow Reader";
|
|
118
|
+
flow.expect attribute "#storage" "data-mode" "signed-in";
|
|
119
|
+
flow.expect attribute "#storage" "data-session" "active";
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Expected: the signed-in account is visible and the stored local/session values are available before the dashboard's startup code reads them. Keep the demo server running: restarting it removes the backend user even if the browser snapshot still exists.
|
|
123
|
+
|
|
124
|
+
A snapshot captures cookies and Web Storage for the selected browser profile. Storage restoration is restricted to its matching top-level HTTP(S) origin and seeds the saved values once; later app changes and reloads are preserved. It does not inject saved storage into another site's frame.
|
|
125
|
+
|
|
126
|
+
Use a current snapshot with saved origin metadata. Legacy snapshots lacking an origin prefer the configured application base URL, then an available current application page. Configure the intended `BASE_URL` before restoring a legacy record. Persisted sessions contain credentials and belong outside version control.
|
|
127
|
+
|
|
128
|
+
Saving a session does not create a second live user. Use [named browser profiles](../../../NATIVE_BROWSER.md#separate-users-in-one-test) when customer and admin must stay independently signed in. Saved browser state is also separate from application backend data and arbitrary flow variables.
|
|
129
|
+
|
|
130
|
+
## Deliberate failures
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
npx flowstride run workflow-examples/variables-and-reuse/negative/wrong-reused-value.flow --ci
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This reuses the extraction but expects a different UI value, so its assertion must fail and exit nonzero.
|
|
137
|
+
|
|
138
|
+
To observe process isolation, run `flows/reuse.flow` by itself in a new CLI invocation. It has no setup in that process, so `@global[sharedLabel]` must be undefined. This is a setup failure rather than a successful assertion; inspect the error and incomplete steps.
|
|
139
|
+
|
|
140
|
+
The [full variable reference](https://docs.flowstride.io/flow-language/getting-started/variables) covers aliases and additional examples. General `{{env.NAME}}` substitution belongs in command string values; do not assume every JSON configuration field performs the same substitution.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Feature: Explicit process-global reuse
|
|
2
|
+
Scenario: Read a global extraction in the next file
|
|
3
|
+
Given "Open the reuse form"
|
|
4
|
+
flow.open "/reuse";
|
|
5
|
+
When "Use the explicitly global label"
|
|
6
|
+
flow.type input "Selected value" "@global[sharedLabel]";
|
|
7
|
+
flow.click button "Apply value";
|
|
8
|
+
Then "The environment value survived the file boundary"
|
|
9
|
+
flow.expect text "Selected Workflow fixture";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Feature: Generated and extracted variable reuse
|
|
2
|
+
Scenario: Preserve one generated value across API and UI
|
|
3
|
+
Given "Generate data once and extract the response"
|
|
4
|
+
flow.post "/api/echo" with reqBody;
|
|
5
|
+
"""
|
|
6
|
+
{
|
|
7
|
+
"email": "$randomEmail@example.test",
|
|
8
|
+
"label": "{{env.EXAMPLE_LABEL}}"
|
|
9
|
+
}
|
|
10
|
+
"""
|
|
11
|
+
flow.extract resBody "email" as "rememberedEmail";
|
|
12
|
+
flow.extract resBody "label" as global "sharedLabel";
|
|
13
|
+
When "The generated value is reused in a second API request"
|
|
14
|
+
flow.post "/api/echo" with reqBody;
|
|
15
|
+
"""
|
|
16
|
+
{
|
|
17
|
+
"email": "@rememberedEmail"
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
And "Generation did not silently create another value"
|
|
21
|
+
flow.expect resBody "email" equals "@randomEmail@example.test";
|
|
22
|
+
When "Reuse the extraction in a browser field"
|
|
23
|
+
flow.open "/reuse";
|
|
24
|
+
flow.type input "Selected value" "@rememberedEmail";
|
|
25
|
+
flow.click button "Apply value";
|
|
26
|
+
Then "The browser received the exact value"
|
|
27
|
+
flow.expect text "Selected @rememberedEmail";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Feature: Generated and extracted variable reuse
|
|
2
|
+
Scenario: Preserve one generated value across API and UI
|
|
3
|
+
Given "Generate data once and extract the response"
|
|
4
|
+
flow.post "/api/echo" with reqBody;
|
|
5
|
+
"""
|
|
6
|
+
{
|
|
7
|
+
"email": "$randomEmail@example.test",
|
|
8
|
+
"label": "{{env.EXAMPLE_LABEL}}"
|
|
9
|
+
}
|
|
10
|
+
"""
|
|
11
|
+
flow.extract resBody "email" as "rememberedEmail";
|
|
12
|
+
flow.extract resBody "label" as global "sharedLabel";
|
|
13
|
+
When "The generated value is reused in a second API request"
|
|
14
|
+
flow.post "/api/echo" with reqBody;
|
|
15
|
+
"""
|
|
16
|
+
{
|
|
17
|
+
"email": "@rememberedEmail"
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
And "Generation did not silently create another value"
|
|
21
|
+
flow.expect resBody "email" equals "@randomEmail@example.test";
|
|
22
|
+
When "Reuse the extraction in a browser field"
|
|
23
|
+
flow.open "/reuse";
|
|
24
|
+
flow.type input "Selected value" "@rememberedEmail";
|
|
25
|
+
flow.click button "Apply value";
|
|
26
|
+
Then "The browser received the exact value"
|
|
27
|
+
flow.expect text "Selected impossible@example.test";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Feature: Restore a persisted account
|
|
2
|
+
Scenario: Restore in a fresh process
|
|
3
|
+
Given "Load the saved browser state"
|
|
4
|
+
flow.use session "workflow-reader";
|
|
5
|
+
When "Open the account page"
|
|
6
|
+
flow.open "/dashboard";
|
|
7
|
+
Then "Cookies and both storage types are restored"
|
|
8
|
+
flow.expect visible "Welcome, Workflow Reader";
|
|
9
|
+
flow.expect attribute "#storage" "data-mode" "signed-in";
|
|
10
|
+
flow.expect attribute "#storage" "data-session" "active";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Feature: API and browser in one test
|
|
2
|
+
Scenario: Create through API and sign in through UI
|
|
3
|
+
Given "Create a disposable user"
|
|
4
|
+
flow.post "/api/users" with reqBody;
|
|
5
|
+
"""
|
|
6
|
+
{
|
|
7
|
+
"email": "$randomEmail@example.test",
|
|
8
|
+
"password": "fixture-password",
|
|
9
|
+
"name": "Workflow Reader"
|
|
10
|
+
}
|
|
11
|
+
"""
|
|
12
|
+
flow.extract resBody "id" as "createdId";
|
|
13
|
+
flow.extract resBody "email" as "createdEmail";
|
|
14
|
+
And "Creation succeeds"
|
|
15
|
+
flow.expect status "201";
|
|
16
|
+
When "Sign in through the browser"
|
|
17
|
+
flow.open "/login";
|
|
18
|
+
flow.type input "Email" "@createdEmail";
|
|
19
|
+
flow.type input "Password" "fixture-password";
|
|
20
|
+
flow.click button "Sign in";
|
|
21
|
+
And "The UI shows the created account"
|
|
22
|
+
flow.expect visible "Welcome, Workflow Reader";
|
|
23
|
+
When "Verify backend state from the same test"
|
|
24
|
+
flow.get "/api/users/@createdId";
|
|
25
|
+
When "Persist authenticated browser state"
|
|
26
|
+
flow.save session "workflow-reader" persist;
|
|
27
|
+
Then "The backend recorded the browser login"
|
|
28
|
+
flow.expect status "200";
|
|
29
|
+
flow.expect resBody "loginCount" equals "1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowstride",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "An opensource flowfirst test automation framework",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"packages/flowstride_report"
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"src/ui/dist",
|
|
13
13
|
"NATIVE_CI.md",
|
|
14
|
-
"examples/native-game"
|
|
14
|
+
"examples/native-game",
|
|
15
|
+
"NATIVE_BROWSER.md",
|
|
16
|
+
"NATIVE_WORKFLOWS.md",
|
|
17
|
+
"examples/native-workflows"
|
|
15
18
|
],
|
|
16
19
|
"bin": {
|
|
17
20
|
"flowstride": "dist/cli/index.js"
|
|
@@ -27,10 +30,12 @@
|
|
|
27
30
|
"verify:candidate": "node scripts/verify-candidate.cjs",
|
|
28
31
|
"test:dashboard:unit": "node --test tests/dashboard/store.test.cjs",
|
|
29
32
|
"test:dashboard": "node --test --test-concurrency=1 tests/dashboard/*.test.cjs",
|
|
30
|
-
"test:unit": "node --test tests/native/core.test.cjs tests/native/combined.test.cjs tests/native/curl.test.cjs tests/native/upload.test.cjs tests/dashboard/store.test.cjs tests/native/replay.test.cjs tests/dashboard/replay.test.cjs tests/native/assertions-sessions.test.cjs",
|
|
31
|
-
"test:security": "node --test tests/manual-security.test.cjs tests/native/curl.test.cjs tests/native/combined.test.cjs tests/native/replay.test.cjs tests/native/assertions-sessions.test.cjs",
|
|
33
|
+
"test:unit": "node --test tests/native/core.test.cjs tests/native/combined.test.cjs tests/native/curl.test.cjs tests/native/upload.test.cjs tests/dashboard/store.test.cjs tests/native/replay.test.cjs tests/dashboard/replay.test.cjs tests/native/assertions-sessions.test.cjs tests/native/capabilities.test.cjs tests/native/browser-selection.unit.test.cjs tests/native/manual-boundary.test.cjs tests/native/storage-hover.test.cjs tests/native/plugin-runtime.test.cjs tests/native/runtime-assertions.test.cjs tests/native/runtime-api-refresh.test.cjs tests/dashboard/report-status.test.cjs tests/dashboard/manual-title.test.cjs tests/native/assertion-results.test.cjs tests/dashboard/assertion-results.test.cjs",
|
|
34
|
+
"test:security": "node --test tests/manual-security.test.cjs tests/native/curl.test.cjs tests/native/combined.test.cjs tests/native/replay.test.cjs tests/native/assertions-sessions.test.cjs tests/native/capabilities.test.cjs tests/native/browser-selection.unit.test.cjs tests/native/manual-boundary.test.cjs tests/native/storage-hover.test.cjs tests/native/plugin-runtime.test.cjs tests/native/runtime-assertions.test.cjs tests/native/runtime-api-refresh.test.cjs tests/dashboard/report-status.test.cjs tests/dashboard/manual-title.test.cjs tests/dashboard/manual-history.browser.test.cjs tests/native/assertion-results.test.cjs tests/dashboard/assertion-results.test.cjs",
|
|
32
35
|
"test:replay": "node --test tests/native/replay.test.cjs tests/dashboard/replay.test.cjs tests/dashboard/curl.browser.test.cjs",
|
|
33
|
-
"test:assertions-sessions": "node --test --test-concurrency=1 tests/native/assertions-sessions*.test.cjs"
|
|
36
|
+
"test:assertions-sessions": "node --test --test-concurrency=1 tests/native/assertions-sessions*.test.cjs",
|
|
37
|
+
"test:capabilities": "node --test --test-concurrency=1 tests/native/capabilities*.test.cjs",
|
|
38
|
+
"test:examples": "node --test --test-concurrency=1 tests/native/workflow-examples.test.cjs"
|
|
34
39
|
},
|
|
35
40
|
"keywords": [
|
|
36
41
|
"automation",
|
|
@@ -44,28 +49,37 @@
|
|
|
44
49
|
"node": ">=22.12.0"
|
|
45
50
|
},
|
|
46
51
|
"dependencies": {
|
|
52
|
+
"@axe-core/playwright": "4.14.0",
|
|
47
53
|
"@inquirer/prompts": "^7.10.1",
|
|
54
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
55
|
+
"@vitejs/plugin-vue": "6.0.10",
|
|
48
56
|
"axios": "^1.20.0",
|
|
49
57
|
"chalk": "^4.1.2",
|
|
50
58
|
"commander": "^11.0.0",
|
|
59
|
+
"dom-accessibility-api": "0.7.1",
|
|
51
60
|
"dotenv": "^17.4.2",
|
|
52
61
|
"open": "^11.0.0",
|
|
53
62
|
"ora": "^5.4.1",
|
|
54
63
|
"playwright": "1.59.1",
|
|
64
|
+
"playwright-core": "1.59.1",
|
|
55
65
|
"playwright-extra": "^4.3.6",
|
|
56
66
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
67
|
+
"sanitize-html": "2.18.1",
|
|
57
68
|
"tar": "^7.5.22",
|
|
58
69
|
"ts-node": "^10.9.2",
|
|
59
70
|
"undici": "^7.30.1",
|
|
71
|
+
"vite": "8.3.5",
|
|
60
72
|
"ws": "^8.22.0",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
73
|
+
"zod": "4.6.7",
|
|
74
|
+
"typescript": "5.9.3"
|
|
63
75
|
},
|
|
64
76
|
"devDependencies": {
|
|
65
77
|
"@types/node": "^25.9.1",
|
|
78
|
+
"@types/sanitize-html": "2.16.2",
|
|
66
79
|
"@types/ws": "^8.18.1",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
80
|
+
"react": "18.3.1",
|
|
81
|
+
"react-dom": "18.3.1",
|
|
82
|
+
"three": "0.186.1"
|
|
69
83
|
},
|
|
70
84
|
"optionalDependencies": {
|
|
71
85
|
"@rolldown/binding-darwin-arm64": "^0.15.1"
|