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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Feature: Microphone and transcript UI fixture
|
|
2
|
+
Scenario: Capture a known microphone signal
|
|
3
|
+
Given "Configure a PCM WAV before browser startup"
|
|
4
|
+
flow.injectAudio "workflow-examples/audio-to-text/fixtures/microphone-tone.wav";
|
|
5
|
+
flow.browser.permissions {
|
|
6
|
+
"permissions": [
|
|
7
|
+
"microphone"
|
|
8
|
+
]
|
|
9
|
+
};
|
|
10
|
+
flow.open "/audio";
|
|
11
|
+
When "Start recording"
|
|
12
|
+
flow.click button "Start recording";
|
|
13
|
+
flow.expect visible "Recording active";
|
|
14
|
+
When "Allow the fixture audio to play and stop"
|
|
15
|
+
flow.waitForPipeline;
|
|
16
|
+
flow.click button "Stop recording";
|
|
17
|
+
Then "The simulated transcription result is displayed"
|
|
18
|
+
flow.expect transcript contains "Fixture transcript: audio received.";
|
|
19
|
+
flow.assert.that {
|
|
20
|
+
"selector": "#transcript",
|
|
21
|
+
"condition": "text",
|
|
22
|
+
"value": "Fixture transcript: audio received."
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Feature: Microphone and transcript UI fixture
|
|
2
|
+
Scenario: Capture a known microphone signal
|
|
3
|
+
Given "Configure a PCM WAV before browser startup"
|
|
4
|
+
flow.injectAudio "workflow-examples/audio-to-text/fixtures/microphone-tone.wav";
|
|
5
|
+
flow.browser.permissions {
|
|
6
|
+
"permissions": [
|
|
7
|
+
"microphone"
|
|
8
|
+
]
|
|
9
|
+
};
|
|
10
|
+
flow.open "/audio";
|
|
11
|
+
When "Start recording"
|
|
12
|
+
flow.click button "Start recording";
|
|
13
|
+
flow.expect visible "Recording active";
|
|
14
|
+
When "Allow the fixture audio to play and stop"
|
|
15
|
+
flow.waitForPipeline;
|
|
16
|
+
flow.click button "Stop recording";
|
|
17
|
+
Then "The simulated transcription result is displayed"
|
|
18
|
+
flow.expect transcript contains "This phrase was never produced.";
|
|
19
|
+
flow.assert.that {
|
|
20
|
+
"selector": "#transcript",
|
|
21
|
+
"condition": "text",
|
|
22
|
+
"value": "Fixture transcript: audio received."
|
|
23
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Feature: Verify a real application's speech transcription
|
|
2
|
+
|
|
3
|
+
Scenario: Record known spoken audio and check the resulting transcript
|
|
4
|
+
Given "A spoken PCM WAV and an authorized recording page"
|
|
5
|
+
flow.injectAudio "fixtures/voice-note.wav";
|
|
6
|
+
flow.browser.permissions {
|
|
7
|
+
"permissions": ["microphone"]
|
|
8
|
+
};
|
|
9
|
+
flow.open "{{env.TRANSCRIPTION_URL}}";
|
|
10
|
+
|
|
11
|
+
When "Start the application's recording"
|
|
12
|
+
flow.click button "Start recording";
|
|
13
|
+
flow.expect visible "Recording active";
|
|
14
|
+
|
|
15
|
+
When "Allow the audio clip to play and stop recording"
|
|
16
|
+
flow.waitForPipeline;
|
|
17
|
+
flow.click button "Stop recording";
|
|
18
|
+
|
|
19
|
+
Then "The new transcript contains the expected spoken words"
|
|
20
|
+
flow.assert.that {
|
|
21
|
+
"selector": "#transcript",
|
|
22
|
+
"condition": "containsText",
|
|
23
|
+
"value": "{{env.EXPECTED_TRANSCRIPT}}",
|
|
24
|
+
"timeout": 60000
|
|
25
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Work with iFrames
|
|
2
|
+
|
|
3
|
+
Flowstride can discover a readable control in a child frame and can scope native assertions to an explicit iframe. For an action requiring an exact child-frame CSS selector, use the focused plugin example in this chapter.
|
|
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 app running.
|
|
13
|
+
|
|
14
|
+
## Readable target with an explicit frame assertion
|
|
15
|
+
|
|
16
|
+
The `/frames` page embeds a profile containing a **Frame action** button. The button is inside the iframe; the parent page has no matching control.
|
|
17
|
+
|
|
18
|
+
[readable-frame.flow](flows/readable-frame.flow):
|
|
19
|
+
|
|
20
|
+
```flow
|
|
21
|
+
Feature: Supported iframe interaction
|
|
22
|
+
Scenario: Find a readable target in a child frame
|
|
23
|
+
Given "The embedded profile is open"
|
|
24
|
+
flow.open "/frames";
|
|
25
|
+
When "Click the readable child-frame target"
|
|
26
|
+
flow.click "Frame action";
|
|
27
|
+
Then "Assert the explicit child-frame result"
|
|
28
|
+
flow.assert.that {
|
|
29
|
+
"frame": "#profile-frame",
|
|
30
|
+
"selector": "#frame-result",
|
|
31
|
+
"condition": "text",
|
|
32
|
+
"value": "Clicked in frame"
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx flowstride run workflow-examples/iframes/flows/readable-frame.flow --ci
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Expected: the child-frame button is clicked, its result changes to `Clicked in frame`, and the frame-scoped assertion passes.
|
|
43
|
+
|
|
44
|
+
`flow.click "Frame action"` uses readable-target discovery. The assertion's `frame` identifies the iframe in the parent document, and its `selector` identifies the result inside that frame.
|
|
45
|
+
|
|
46
|
+
## Exact frame action through a project plugin
|
|
47
|
+
|
|
48
|
+
A raw selector such as `flow.click "#frame-only"` is not a general instruction to search every frame. The raw-selector path and readable-target discovery have different behavior.
|
|
49
|
+
|
|
50
|
+
[frame-actions.ts](plugins/frame-actions.ts) uses the public plugin browser access:
|
|
51
|
+
|
|
52
|
+
This optional TypeScript plugin needs a compatible compiler in your project. Install it before running the plugin example:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm i --save-dev typescript@5.9.3
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The native readable-target example above does not need this extra setup. Keep the compiler in your lockfile so CI uses the same version; newer compiler majors may be incompatible with the plugin loader.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export async function click(sdk: any, args: { frame: string; selector: string }) {
|
|
62
|
+
await sdk.browser.page()
|
|
63
|
+
.frameLocator(args.frame)
|
|
64
|
+
.locator(args.selector)
|
|
65
|
+
.click();
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The supplied file also validates its arguments. [explicit-frame-plugin.flow](flows/explicit-frame-plugin.flow) registers the plugin and calls it:
|
|
70
|
+
|
|
71
|
+
```flow
|
|
72
|
+
Feature: An explicit iframe action through a focused project plugin
|
|
73
|
+
let frameActions = "workflow-examples/iframes/plugins/frame-actions.ts";
|
|
74
|
+
|
|
75
|
+
Scenario: Click a CSS target in the selected child frame
|
|
76
|
+
Given "The embedded profile is open"
|
|
77
|
+
flow.open "/frames";
|
|
78
|
+
|
|
79
|
+
When "Click the explicit child-frame selector"
|
|
80
|
+
frameActions.click {
|
|
81
|
+
"frame": "#profile-frame",
|
|
82
|
+
"selector": "#frame-only"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
Then "The selected frame shows its result"
|
|
86
|
+
flow.assert.that {
|
|
87
|
+
"frame": "#profile-frame",
|
|
88
|
+
"selector": "#frame-result",
|
|
89
|
+
"condition": "text",
|
|
90
|
+
"value": "Clicked in frame"
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
npx flowstride run workflow-examples/iframes/flows/explicit-frame-plugin.flow --ci
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Expected: the same child-frame result passes. This is a project extension for the explicit action; it uses the normal Flowstride runner and requires no private runner imports.
|
|
99
|
+
|
|
100
|
+
## Deliberate failure
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx flowstride run workflow-examples/iframes/negative/wrong-frame-result.flow --ci
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The button is clicked, but the assertion expects `Never clicked`. The mismatch must fail, retain failure evidence and exit nonzero.
|
|
107
|
+
|
|
108
|
+
## Scope and troubleshooting
|
|
109
|
+
|
|
110
|
+
- Use a unique readable target or select the frame explicitly when several frames contain the same control.
|
|
111
|
+
- The `frame` option belongs to the native assertion shown here; it is not an extra argument supported by every legacy action.
|
|
112
|
+
- This fixture has one same-origin iframe. Qualify your own nested, cross-origin, sandboxed or dynamically replaced frame structure. A focused plugin can explicitly chain `frameLocator` calls for nested frames.
|
|
113
|
+
- Confirm that the iframe and its document actually load. An assertion retries its condition; it does not repair a failed application navigation.
|
|
114
|
+
- iFrames and browser tabs are different. `flow.switchTo "next"` switches pages/tabs in the current profile, not an iframe.
|
|
115
|
+
|
|
116
|
+
See the [plugin guide](https://docs.flowstride.io/flow-language/getting-started/plugins) and [native assertions](../../../NATIVE_BROWSER.md#assertions-that-wait-for-a-condition).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Feature: An explicit iframe action through a focused project plugin
|
|
2
|
+
let frameActions = "workflow-examples/iframes/plugins/frame-actions.ts";
|
|
3
|
+
|
|
4
|
+
Scenario: Click a CSS target in the selected child frame
|
|
5
|
+
Given "The embedded profile is open"
|
|
6
|
+
flow.open "/frames";
|
|
7
|
+
|
|
8
|
+
When "Click the explicit child-frame selector"
|
|
9
|
+
frameActions.click {
|
|
10
|
+
"frame": "#profile-frame",
|
|
11
|
+
"selector": "#frame-only"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
Then "The selected frame shows its result"
|
|
15
|
+
flow.assert.that {
|
|
16
|
+
"frame": "#profile-frame",
|
|
17
|
+
"selector": "#frame-result",
|
|
18
|
+
"condition": "text",
|
|
19
|
+
"value": "Clicked in frame"
|
|
20
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Feature: Supported iframe interaction
|
|
2
|
+
Scenario: Find a readable target in a child frame
|
|
3
|
+
Given "The embedded profile is open"
|
|
4
|
+
flow.open "/frames";
|
|
5
|
+
When "Click the readable child-frame target"
|
|
6
|
+
flow.click "Frame action";
|
|
7
|
+
Then "Assert the explicit child-frame result"
|
|
8
|
+
flow.assert.that {
|
|
9
|
+
"frame": "#profile-frame",
|
|
10
|
+
"selector": "#frame-result",
|
|
11
|
+
"condition": "text",
|
|
12
|
+
"value": "Clicked in frame"
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Feature: Supported iframe interaction
|
|
2
|
+
Scenario: Find a readable target in a child frame
|
|
3
|
+
Given "The embedded profile is open"
|
|
4
|
+
flow.open "/frames";
|
|
5
|
+
When "Click the readable child-frame target"
|
|
6
|
+
flow.click "Frame action";
|
|
7
|
+
Then "Assert the explicit child-frame result"
|
|
8
|
+
flow.assert.that {
|
|
9
|
+
"frame": "#profile-frame",
|
|
10
|
+
"selector": "#frame-result",
|
|
11
|
+
"condition": "text",
|
|
12
|
+
"value": "Never clicked"
|
|
13
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A project extension for an explicit frame action; not a private runner import. */
|
|
2
|
+
export async function click(sdk: any, args: { frame: string; selector: string }) {
|
|
3
|
+
if (typeof args.frame !== 'string' || typeof args.selector !== 'string') {
|
|
4
|
+
throw new Error('frame and selector must be strings');
|
|
5
|
+
}
|
|
6
|
+
await sdk.browser.page().frameLocator(args.frame).locator(args.selector).click();
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Manual design, execution and saved results
|
|
2
|
+
|
|
3
|
+
The Manual Workspace lets you describe steps and expected results visually, perform those actions in your application, record outcomes and reopen a saved run. It is available in OS and Cloud. A human decides whether each manual step matches its expected result.
|
|
4
|
+
|
|
5
|
+
Complete the [shared setup](../README.md) and leave the local demo app running. The default application page for this chapter is `http://127.0.0.1:4107/profile`. If you changed the demo port, use the printed server URL and update the design instructions to match.
|
|
6
|
+
|
|
7
|
+
## 1. Load the supplied design
|
|
8
|
+
|
|
9
|
+
The fixture [profile-review.json](fixtures/profile-review.json) defines three connected steps, expected results, a Test Plan identifier and the local environment. Copy it into the normal project template folder:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
mkdir -p manual_flows/templates
|
|
13
|
+
cp workflow-examples/manual-testing/fixtures/profile-review.json "manual_flows/templates/Profile review.json"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
PowerShell:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
New-Item -ItemType Directory -Force manual_flows/templates
|
|
20
|
+
Copy-Item workflow-examples/manual-testing/fixtures/profile-review.json "manual_flows/templates/Profile review.json"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Use a new destination filename when you already have a template with that name. Open the local workspace:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx flowstride manual
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Choose **Profile review** / its template filename from the flow selector. Refresh the workspace if it was open before the file was copied. Inspect the title, Test Plan, instructions and expected results. This JSON is a manual design, not an automated `.flow` test.
|
|
30
|
+
|
|
31
|
+
You can also create a design with **Step**, **Arrow**, **Group** and **Text**. The runtime starts with the lowest internal node ID and follows outgoing connections. Moving a node or changing its visible step label does not change its execution identity. Keep an unambiguous path and confirm the highlighted starting step.
|
|
32
|
+
|
|
33
|
+
## 2. Save and perform a passing run
|
|
34
|
+
|
|
35
|
+
In Design Mode, click **Save** and wait for confirmation. Switch to **Execution Mode**; the design is saved before the transition.
|
|
36
|
+
|
|
37
|
+
Open the demo application's `/profile` page in another tab/window and perform these actions:
|
|
38
|
+
|
|
39
|
+
| Step | Action | Expected result |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| Open the sample profile | Open `/profile` | Display name is visible |
|
|
42
|
+
| Update the display name | Enter `Lola Idowu`, then click Save profile | The page shows `Saved as Lola Idowu` |
|
|
43
|
+
| Reopen the profile | Reload the application page | Display name still contains `Lola Idowu` |
|
|
44
|
+
|
|
45
|
+
After checking each result, return focus to the workspace canvas and press **Enter** to mark it Passed. All three steps should be Passed. The server keeps the profile change in memory; restarting it resets demo data.
|
|
46
|
+
|
|
47
|
+
If nodes extend beyond the viewport, pan with the Hand tool or use the zoom/recenter controls. The workspace's metadata environment label does not navigate or configure the application browser for you.
|
|
48
|
+
|
|
49
|
+
## 3. Save the execution record
|
|
50
|
+
|
|
51
|
+
| Engine | Button in Execution Mode |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| OS | **Save** |
|
|
54
|
+
| Cloud | **Submit Run** |
|
|
55
|
+
|
|
56
|
+
Wait for confirmation. The design remains under `manual_flows/templates`; the new run is stored separately under `manual_flows/runs`. A browser file download is not proof that a run was persisted: confirm the saved record in **Runs**.
|
|
57
|
+
|
|
58
|
+
The passing exercise should show three Passed steps and a PASS run summary. Keep your template and referenced media together when backing up the project.
|
|
59
|
+
|
|
60
|
+
## 4. Reopen history
|
|
61
|
+
|
|
62
|
+
Open **Runs**, locate the saved record and select it. Check its saved flow name, step results and canvas. Review historical records as evidence of that execution; return to the design for a fresh run. Cloud presents a vaulted historical record with its editing controls locked.
|
|
63
|
+
|
|
64
|
+
Manual results are local records by default in OS. Cloud can synchronize manual results when valid Cloud credentials are configured. Check delivery status separately and avoid including real personal data in example evidence.
|
|
65
|
+
|
|
66
|
+
## 5. Practice a failure and skipped work
|
|
67
|
+
|
|
68
|
+
Copy [profile-review-mismatch.json](fixtures/profile-review-mismatch.json) into the template folder under its own filename and open it as another design. It deliberately expects `Saved as A Different Person` after you enter `Lola Idowu`.
|
|
69
|
+
|
|
70
|
+
Perform the first action and record Passed. At the mismatching second step, observe the actual application result and record Failed. For this exercise, mark the remaining follow-up step Skipped and save/submit the run. Expected: one Passed, one Failed, one Skipped, and an overall FAIL summary. Skipped work must not be reported as verified success.
|
|
71
|
+
|
|
72
|
+
In Execution Mode, focus the canvas before using these shortcuts:
|
|
73
|
+
|
|
74
|
+
| Key | Result |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| Enter | Passed |
|
|
77
|
+
| Delete / Backspace | Failed |
|
|
78
|
+
| Tab | Blocked |
|
|
79
|
+
| Escape | Skipped |
|
|
80
|
+
|
|
81
|
+
These keys have different meanings in Design Mode. A focused text editor may consume them instead of the canvas.
|
|
82
|
+
|
|
83
|
+
Expand **Failure Details** for the failed step. Describe the actual versus expected outcome, add appropriate evidence/assignee, save the defect form, and then save/submit the run. Confirm the recorded defect when reopening history. Design-reference media and defect evidence serve different purposes; keep uploads within the limits shown by their respective chooser.
|
|
84
|
+
|
|
85
|
+
The [manual documentation](https://docs.flowstride.io/manual-workspace/manual-testing) covers planning, canvas controls, evidence, reporting and collaboration in more detail.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"flowName": "Profile review \u2014 intentional mismatch",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"environment": "Local",
|
|
6
|
+
"testPlan": "TP-PROFILE-01",
|
|
7
|
+
"lastUpdatedBy": "Workflow fixture"
|
|
8
|
+
},
|
|
9
|
+
"nodes": [
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"x": 80,
|
|
13
|
+
"y": 160,
|
|
14
|
+
"width": 280,
|
|
15
|
+
"height": 150,
|
|
16
|
+
"visual_step": "1",
|
|
17
|
+
"title": "Open the sample profile",
|
|
18
|
+
"description": "Disposable local example data.",
|
|
19
|
+
"steps": "<p>Open http://127.0.0.1:4107/profile in your application browser.</p>",
|
|
20
|
+
"expected_result": "<p>The Display name field is visible.</p>",
|
|
21
|
+
"references": "",
|
|
22
|
+
"priority": "High",
|
|
23
|
+
"defect_report": "",
|
|
24
|
+
"defect_media": "",
|
|
25
|
+
"assigned_to": "",
|
|
26
|
+
"is_active": false
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": 2,
|
|
30
|
+
"x": 430,
|
|
31
|
+
"y": 160,
|
|
32
|
+
"width": 280,
|
|
33
|
+
"height": 150,
|
|
34
|
+
"visual_step": "2",
|
|
35
|
+
"title": "Update the display name",
|
|
36
|
+
"description": "Disposable local example data.",
|
|
37
|
+
"steps": "<p>Enter Lola Idowu and click Save profile.</p>",
|
|
38
|
+
"expected_result": "<p>The page shows Saved as A Different Person.</p>",
|
|
39
|
+
"references": "",
|
|
40
|
+
"priority": "High",
|
|
41
|
+
"defect_report": "",
|
|
42
|
+
"defect_media": "",
|
|
43
|
+
"assigned_to": "",
|
|
44
|
+
"is_active": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": 3,
|
|
48
|
+
"x": 780,
|
|
49
|
+
"y": 160,
|
|
50
|
+
"width": 280,
|
|
51
|
+
"height": 150,
|
|
52
|
+
"visual_step": "3",
|
|
53
|
+
"title": "Reopen the profile",
|
|
54
|
+
"description": "Disposable local example data.",
|
|
55
|
+
"steps": "<p>Reload the profile page.</p>",
|
|
56
|
+
"expected_result": "<p>The Display name field still contains Lola Idowu.</p>",
|
|
57
|
+
"references": "",
|
|
58
|
+
"priority": "Low",
|
|
59
|
+
"defect_report": "",
|
|
60
|
+
"defect_media": "",
|
|
61
|
+
"assigned_to": "",
|
|
62
|
+
"is_active": false
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"groups": [],
|
|
66
|
+
"texts": [],
|
|
67
|
+
"connections": [
|
|
68
|
+
{
|
|
69
|
+
"id": 4,
|
|
70
|
+
"start_x": 360,
|
|
71
|
+
"start_y": 235,
|
|
72
|
+
"end_x": 430,
|
|
73
|
+
"end_y": 235,
|
|
74
|
+
"start_node_id": 1,
|
|
75
|
+
"end_node_id": 2,
|
|
76
|
+
"start_anchor": "Right",
|
|
77
|
+
"end_anchor": "Left",
|
|
78
|
+
"is_active": false
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": 5,
|
|
82
|
+
"start_x": 710,
|
|
83
|
+
"start_y": 235,
|
|
84
|
+
"end_x": 780,
|
|
85
|
+
"end_y": 235,
|
|
86
|
+
"start_node_id": 2,
|
|
87
|
+
"end_node_id": 3,
|
|
88
|
+
"start_anchor": "Right",
|
|
89
|
+
"end_anchor": "Left",
|
|
90
|
+
"is_active": false
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"flowName": "Profile review",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"environment": "Local",
|
|
6
|
+
"testPlan": "TP-PROFILE-01",
|
|
7
|
+
"lastUpdatedBy": "Workflow fixture"
|
|
8
|
+
},
|
|
9
|
+
"nodes": [
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"x": 80,
|
|
13
|
+
"y": 160,
|
|
14
|
+
"width": 280,
|
|
15
|
+
"height": 150,
|
|
16
|
+
"visual_step": "1",
|
|
17
|
+
"title": "Open the sample profile",
|
|
18
|
+
"description": "Disposable local example data.",
|
|
19
|
+
"steps": "<p>Open http://127.0.0.1:4107/profile in your application browser.</p>",
|
|
20
|
+
"expected_result": "<p>The Display name field is visible.</p>",
|
|
21
|
+
"references": "",
|
|
22
|
+
"priority": "High",
|
|
23
|
+
"defect_report": "",
|
|
24
|
+
"defect_media": "",
|
|
25
|
+
"assigned_to": "",
|
|
26
|
+
"is_active": false
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": 2,
|
|
30
|
+
"x": 430,
|
|
31
|
+
"y": 160,
|
|
32
|
+
"width": 280,
|
|
33
|
+
"height": 150,
|
|
34
|
+
"visual_step": "2",
|
|
35
|
+
"title": "Update the display name",
|
|
36
|
+
"description": "Disposable local example data.",
|
|
37
|
+
"steps": "<p>Enter Lola Idowu and click Save profile.</p>",
|
|
38
|
+
"expected_result": "<p>The page shows Saved as Lola Idowu.</p>",
|
|
39
|
+
"references": "",
|
|
40
|
+
"priority": "High",
|
|
41
|
+
"defect_report": "",
|
|
42
|
+
"defect_media": "",
|
|
43
|
+
"assigned_to": "",
|
|
44
|
+
"is_active": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": 3,
|
|
48
|
+
"x": 780,
|
|
49
|
+
"y": 160,
|
|
50
|
+
"width": 280,
|
|
51
|
+
"height": 150,
|
|
52
|
+
"visual_step": "3",
|
|
53
|
+
"title": "Reopen the profile",
|
|
54
|
+
"description": "Disposable local example data.",
|
|
55
|
+
"steps": "<p>Reload the profile page.</p>",
|
|
56
|
+
"expected_result": "<p>The Display name field still contains Lola Idowu.</p>",
|
|
57
|
+
"references": "",
|
|
58
|
+
"priority": "Low",
|
|
59
|
+
"defect_report": "",
|
|
60
|
+
"defect_media": "",
|
|
61
|
+
"assigned_to": "",
|
|
62
|
+
"is_active": false
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"groups": [],
|
|
66
|
+
"texts": [],
|
|
67
|
+
"connections": [
|
|
68
|
+
{
|
|
69
|
+
"id": 4,
|
|
70
|
+
"start_x": 360,
|
|
71
|
+
"start_y": 235,
|
|
72
|
+
"end_x": 430,
|
|
73
|
+
"end_y": 235,
|
|
74
|
+
"start_node_id": 1,
|
|
75
|
+
"end_node_id": 2,
|
|
76
|
+
"start_anchor": "Right",
|
|
77
|
+
"end_anchor": "Left",
|
|
78
|
+
"is_active": false
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": 5,
|
|
82
|
+
"start_x": 710,
|
|
83
|
+
"start_y": 235,
|
|
84
|
+
"end_x": 780,
|
|
85
|
+
"end_y": 235,
|
|
86
|
+
"start_node_id": 2,
|
|
87
|
+
"end_node_id": 3,
|
|
88
|
+
"start_anchor": "Right",
|
|
89
|
+
"end_anchor": "Left",
|
|
90
|
+
"is_active": false
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const escapeHtml = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
6
|
+
const page = (body, script = '') => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Flowstride workflow examples</title></head><body>${body}<script>${script}</script></body></html>`;
|
|
7
|
+
|
|
8
|
+
/** Local demonstration application. It sends no email and performs no speech recognition. */
|
|
9
|
+
export async function startExampleApp({ port = 4107 } = {}) {
|
|
10
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Example port must be between 0 and 65535.');
|
|
11
|
+
const users = new Map();
|
|
12
|
+
let lastAudioPeak = 0;
|
|
13
|
+
let profileName = 'Ada Obi';
|
|
14
|
+
const server = http.createServer(async (request, response) => {
|
|
15
|
+
const host = request.headers.host;
|
|
16
|
+
const boundPort = server.address().port;
|
|
17
|
+
const allowed = [`127.0.0.1:${boundPort}`, `localhost:${boundPort}`];
|
|
18
|
+
if (!allowed.includes(host) || (request.headers.origin && request.headers.origin !== `http://${host}`)) {
|
|
19
|
+
response.writeHead(403); response.end('Forbidden'); return;
|
|
20
|
+
}
|
|
21
|
+
const url = new URL(request.url, `http://${host}`);
|
|
22
|
+
const json = (status, data) => { response.writeHead(status, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(data)); };
|
|
23
|
+
const html = source => { response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); response.end(source); };
|
|
24
|
+
let raw = '', size = 0;
|
|
25
|
+
for await (const chunk of request) {
|
|
26
|
+
size += chunk.length;
|
|
27
|
+
if (size > 65536) { json(413, { error: 'Demo request exceeds 64 KiB' }); return; }
|
|
28
|
+
raw += chunk;
|
|
29
|
+
}
|
|
30
|
+
let data = {};
|
|
31
|
+
if (raw) { try { data = JSON.parse(raw); } catch { json(400, { error: 'Invalid JSON' }); return; } }
|
|
32
|
+
if (url.pathname === '/api/users' && request.method === 'POST') {
|
|
33
|
+
if (!['email', 'password', 'name'].every(key => typeof data[key] === 'string' && data[key].trim())) return json(400, { error: 'Email, password and name are required' });
|
|
34
|
+
const user = { id: randomUUID(), email: data.email, password: data.password, name: data.name, loginCount: 0 };
|
|
35
|
+
users.set(user.id, user);
|
|
36
|
+
return json(201, { id: user.id, email: user.email, name: user.name, loginCount: user.loginCount });
|
|
37
|
+
}
|
|
38
|
+
if (url.pathname.startsWith('/api/users/') && request.method === 'GET') {
|
|
39
|
+
const user = users.get(url.pathname.split('/').pop());
|
|
40
|
+
return user ? json(200, { id: user.id, email: user.email, name: user.name, loginCount: user.loginCount }) : json(404, { error: 'Unknown demo user' });
|
|
41
|
+
}
|
|
42
|
+
if (url.pathname === '/api/session' && request.method === 'POST') {
|
|
43
|
+
const user = [...users.values()].find(value => value.email === data.email && value.password === data.password);
|
|
44
|
+
if (!user) return json(401, { error: 'Invalid demo credentials' });
|
|
45
|
+
user.loginCount += 1;
|
|
46
|
+
response.setHeader('Set-Cookie', `example-user=${user.id}; Path=/; HttpOnly; SameSite=Lax`);
|
|
47
|
+
return json(200, { id: user.id });
|
|
48
|
+
}
|
|
49
|
+
if (url.pathname === '/api/echo' && request.method === 'POST') return json(200, data);
|
|
50
|
+
if (url.pathname === '/api/audio-observed' && request.method === 'POST') { lastAudioPeak = Number(data.peak) || 0; return json(200, { received: lastAudioPeak > 0.001 }); }
|
|
51
|
+
if (url.pathname === '/api/audio-observed' && request.method === 'GET') return json(200, { peak: lastAudioPeak });
|
|
52
|
+
if (url.pathname === '/api/profile' && request.method === 'POST') {
|
|
53
|
+
if (typeof data.name !== 'string' || !data.name.trim()) return json(400, { error: 'A display name is required' });
|
|
54
|
+
profileName = data.name.trim(); return json(200, { name: profileName });
|
|
55
|
+
}
|
|
56
|
+
if (url.pathname === '/login') return html(page('<h1>Demo sign in</h1><input placeholder="Email" type="email"><input placeholder="Password" type="password"><button>Sign in</button><p id="error"></p>', `document.querySelector('button').onclick=async()=>{const r=await fetch('/api/session',{method:'POST',body:JSON.stringify({email:document.querySelector('input[type=email]').value,password:document.querySelector('input[type=password]').value})});if(r.ok){localStorage.setItem('account-mode','signed-in');sessionStorage.setItem('account-session','active');location.href='/dashboard';}else document.querySelector('#error').textContent='Invalid credentials';};`));
|
|
57
|
+
if (url.pathname === '/dashboard') {
|
|
58
|
+
const id = request.headers.cookie?.match(/example-user=([^;]+)/)?.[1], user = users.get(id);
|
|
59
|
+
return html(page(`<h1>${user ? 'Welcome, ' + escapeHtml(user.name) : 'Not authenticated'}</h1><div id="storage"></div>`, `document.querySelector('#storage').dataset.mode=localStorage.getItem('account-mode')||'missing';document.querySelector('#storage').dataset.session=sessionStorage.getItem('account-session')||'missing';`));
|
|
60
|
+
}
|
|
61
|
+
if (url.pathname === '/frames') return html(page('<h1>Embedded profile</h1><iframe id="profile-frame" title="Embedded profile" src="/frame"></iframe>'));
|
|
62
|
+
if (url.pathname === '/frame') return html(page('<button id="frame-only">Frame action</button><p id="frame-result">Waiting</p>', `document.querySelector('button').onclick=()=>document.querySelector('#frame-result').textContent='Clicked in frame';`));
|
|
63
|
+
if (url.pathname === '/orders') return html(page('<h1>Orders</h1><input placeholder="Search orders"><table id="orders"><thead><tr><th>Order</th><th>Customer</th><th>Status</th><th>Action</th></tr></thead><tbody><tr><td>FS-101</td><td>Ada Obi</td><td>Pending</td><td><button>View</button></td></tr><tr><td>FS-102</td><td>Bayo Ade</td><td>Approved</td><td><button>View</button></td></tr></tbody></table><p id="details">Select an order</p>', `const rows=[...document.querySelectorAll('tbody tr')];document.querySelector('input').oninput=e=>rows.forEach(r=>r.hidden=!r.textContent.toLowerCase().includes(e.target.value.toLowerCase()));rows.forEach(r=>r.onclick=()=>document.querySelector('#details').textContent='Order '+r.cells[0].textContent);`));
|
|
64
|
+
if (url.pathname === '/reuse') return html(page('<h1>Reuse extracted data</h1><input placeholder="Selected value"><button>Apply value</button><p id="reused"></p>', `document.querySelector('button').onclick=()=>document.querySelector('#reused').textContent='Selected '+document.querySelector('input').value;`));
|
|
65
|
+
if (url.pathname === '/profile') return html(page(`<h1>Profile review</h1><label>Display name<input id="name" value="${escapeHtml(profileName)}"></label><button>Save profile</button><p id="profile-result"></p>`, `document.querySelector('button').onclick=async()=>{const r=await fetch('/api/profile',{method:'POST',body:JSON.stringify({name:document.querySelector('input').value})});const data=await r.json();document.querySelector('#profile-result').textContent=r.ok?'Saved as '+data.name:data.error;};`));
|
|
66
|
+
if (url.pathname === '/audio') return html(page('<h1>Microphone demo — simulated transcription</h1><button id="start">Start recording</button><button id="stop" disabled>Stop recording</button><p id="recording"></p><p id="transcript"></p>', `let stream,context,timer,peak=0;document.querySelector('#start').onclick=async()=>{stream=await navigator.mediaDevices.getUserMedia({audio:true});context=new AudioContext();await context.resume();const source=context.createMediaStreamSource(stream),analyser=context.createAnalyser();source.connect(analyser);const samples=new Float32Array(analyser.fftSize);timer=setInterval(()=>{analyser.getFloatTimeDomainData(samples);for(const sample of samples)peak=Math.max(peak,Math.abs(sample));},10);document.querySelector('#recording').textContent='Recording active';document.querySelector('#stop').disabled=false;};document.querySelector('#stop').onclick=async()=>{clearInterval(timer);stream.getTracks().forEach(t=>t.stop());await context.close();await fetch('/api/audio-observed',{method:'POST',body:JSON.stringify({peak})});document.querySelector('#recording').textContent='Recording stopped';setTimeout(()=>document.querySelector('#transcript').textContent=peak>0.001?'Fixture transcript: audio received.':'Microphone input was silent',150);};`));
|
|
67
|
+
if (url.pathname === '/') return html(page('<h1>Flowstride workflow examples</h1><ul><li><a href="/login">Demo login</a></li><li><a href="/frames">Iframe</a></li><li><a href="/orders">Orders table</a></li><li><a href="/reuse">Variable reuse</a></li><li><a href="/audio">Microphone demo</a></li><li><a href="/profile">Manual profile review</a></li></ul>'));
|
|
68
|
+
return json(404, { error: 'Unknown example route' });
|
|
69
|
+
});
|
|
70
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
|
|
71
|
+
return { url: `http://127.0.0.1:${server.address().port}`, close: async () => { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
75
|
+
const port = process.env.FLOWSTRIDE_EXAMPLE_PORT === undefined ? 4107 : Number(process.env.FLOWSTRIDE_EXAMPLE_PORT);
|
|
76
|
+
const app = await startExampleApp({ port });
|
|
77
|
+
console.log(`Flowstride example application: ${app.url}`);
|
|
78
|
+
console.log('Local demo only. No real email delivery or speech recognition.');
|
|
79
|
+
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().then(() => process.exit(0)));
|
|
80
|
+
}
|