assuremind 1.2.3 → 1.4.0

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/docs/STUDIO.md CHANGED
@@ -48,7 +48,7 @@ The sidebar on the left has these pages:
48
48
  | 📋 | Reports | Browse historical run results |
49
49
  | 🔧 | Variables | Manage test variables and secrets |
50
50
  | 🩺 | Self-Healing | Review and apply AI-generated selector fixes |
51
- | ⚙️ | Settings | Environment, browsers, healing, capture settings, MCP integration |
51
+ | ⚙️ | Settings | Environment, browsers, healing, capture settings, MCP integration, CI/CD integration |
52
52
  | 🌿 | Git Control Center | Branch management, AI commits, push/pull, conflict resolution |
53
53
 
54
54
  ---
@@ -74,6 +74,8 @@ While data is loading, animated skeleton cards are shown so the layout never jum
74
74
 
75
75
  ## Test Editor
76
76
 
77
+ > **Writing steps:** for a full catalog of plain-English step phrasings (UI **and** API) with examples, see [WRITING-STEPS.md](WRITING-STEPS.md).
78
+
77
79
  ### Suite Types
78
80
 
79
81
  Assuremind supports three suite types:
@@ -466,6 +468,19 @@ The **Device Emulation** card appears below the Browsers card. It lets you emula
466
468
 
467
469
  **Equivalent CLI flag:** `--device "iPhone 15 Pro"`
468
470
 
471
+ ### CI Config Generator
472
+
473
+ Click **Generate CI Config** to produce a pipeline file from your current run settings (browsers, parallel workers, device). Pick a platform tab — **GitHub Actions**, **GitLab CI**, or **Jenkins**.
474
+
475
+ - The YAML opens in a **preview** (read-only). Click **✎ Edit** to customise it — change branches, Node version, add steps, env vars, etc. Your edits apply to **Copy**, **Download**, and **Write to repo**. **↺ Reset** discards edits and regenerates from the run config.
476
+ - **Copy** and **Download** are always available.
477
+ - **Write to repo** writes the file into your project at its standard path:
478
+ - GitHub Actions → `.github/workflows/assuremind.yml`
479
+ - GitLab CI → `.gitlab-ci.yml`
480
+ - Jenkins → `Jenkinsfile`
481
+
482
+ This button only appears when **Settings → CI/CD Integration → "Write CI files to repo"** is enabled (off by default). It **only writes the file** — it does **not** commit or push. Review the change and commit it from the **Git Control Center** when you're ready.
483
+
469
484
  ---
470
485
 
471
486
  ## Reports
@@ -476,6 +491,7 @@ Browse all historical run results:
476
491
  - Click a run to see the **full result tree**: Suite → Case → Step
477
492
  - Each step shows: status, duration, generated code, and screenshot (on failure)
478
493
  - Failed steps show the error message and a screenshot thumbnail
494
+ - A **Downloads** panel appears for runs that captured files (see [File Upload & Download](#file-upload--download)) — click a file to save it locally
479
495
 
480
496
  ### Lighthouse Results (Audit Suites)
481
497
 
@@ -700,6 +716,85 @@ The Faker Data modal (accessible from the Test Editor) lets you browse 100+ dyna
700
716
 
701
717
  ---
702
718
 
719
+ ## File Upload & Download
720
+
721
+ ### Uploading files (for upload steps)
722
+
723
+ In the **Test Editor**, click the **Files** button (next to *Fake Data*) to open the file picker:
724
+
725
+ 1. Click **Choose a file to upload** and pick a file from your machine.
726
+ 2. It's saved into your project at **`fixtures/uploads/`** (committed with the repo, so it works in CI) and appears in the list.
727
+ 3. Click the file to insert a **`{{FILE:name}}`** token into your step. Delete files with the trash icon.
728
+
729
+ The token resolves to the file's relative path at runtime, so it works with `setInputFiles`:
730
+
731
+ | Step (plain English) | Generated code |
732
+ |----------------------|----------------|
733
+ | `Upload {{FILE:resume.pdf}} to the Resume field` | `await page.getByLabel('Resume').setInputFiles('{{FILE:resume.pdf}}');` |
734
+ | `Upload {{FILE:resume.pdf}}` | `await page.locator('input[type="file"]').setInputFiles('{{FILE:resume.pdf}}');` |
735
+
736
+ For a button that opens a native file chooser, keep the listener and the click in **one step**:
737
+
738
+ ```js
739
+ const fileChooserPromise = page.waitForEvent('filechooser');
740
+ await page.getByRole('button', { name: 'Choose File' }).click();
741
+ (await fileChooserPromise).setFiles('{{FILE:resume.pdf}}');
742
+ ```
743
+
744
+ Files inside an iframe still need `frameLocator(...)`, e.g.
745
+ `await page.frameLocator('#frame').locator('input[type="file"]').setInputFiles('{{FILE:doc.pdf}}');`
746
+
747
+ **Waiting for the upload to complete** — `setInputFiles` only attaches the file; there is no universal "upload done" event. The step waits for a completion signal:
748
+
749
+ | Step (plain English) | Wait behavior |
750
+ |----------------------|---------------|
751
+ | `Upload {{FILE:resume.pdf}} to the Resume field` | waits for the network to settle (`waitForLoadState('networkidle')`) |
752
+ | `Upload {{FILE:resume.pdf}} to the Resume field and wait for 'Upload complete'` | waits for the success text — **most reliable** |
753
+ | `Upload {{FILE:photo.png}}` | uploads to the page's file input, then waits for network |
754
+
755
+ **Example upload steps:**
756
+
757
+ ```text
758
+ Upload {{FILE:resume.pdf}} to the Resume field
759
+ Upload {{FILE:resume.pdf}} to the Resume field and wait for 'Upload complete'
760
+ Upload {{FILE:photo.png}}
761
+ Upload {{FILE:invoice.pdf}} into the Attachment input
762
+ ```
763
+
764
+ ### Downloads (auto-captured)
765
+
766
+ You do **not** need any special code to handle downloads — just trigger them with a click. The step
767
+ **waits until the file finishes downloading** (`waitForEvent('download')` → `await download.path()`),
768
+ and the runner **automatically saves every download** to `results/downloads/<runId>/`. Avoid
769
+ `download.saveAs()` / `fs` in steps — the sandbox blocks them and it isn't needed.
770
+
771
+ **Example download steps:**
772
+
773
+ ```text
774
+ Download the "Export CSV" link
775
+ Click the "Export" button to download
776
+ Download the file by clicking "Report PDF"
777
+ Click "Download invoice" and wait for download
778
+ ```
779
+
780
+ Each generates:
781
+ ```js
782
+ const downloadPromise = page.waitForEvent('download');
783
+ await page.getByRole('link', { name: 'Export CSV' }).click();
784
+ const download = await downloadPromise;
785
+ await download.path(); // resolves once the download is complete
786
+ ```
787
+
788
+ To view and retrieve captured files:
789
+
790
+ 1. Go to **Reports → Run Reports** and click the run to expand it.
791
+ 2. A **Downloads** panel at the top lists every file captured during that run (name + size).
792
+ 3. Click any file to download it to your local machine.
793
+
794
+ The panel only appears when the run produced downloads.
795
+
796
+ ---
797
+
703
798
  ## Visual Regression
704
799
 
705
800
  Visual regression is configured in **Settings** → Visual Regression and reviewed in **Reports** → Visual Diffs / Baselines tabs.
@@ -0,0 +1,171 @@
1
+ # Writing Test Steps
2
+
3
+ A reference for describing test steps in plain English. Type a step in the **Test Editor** (or `assuremind generate`) and AssureMind turns it into Playwright code.
4
+
5
+ Two engines handle generation:
6
+ - **⚡ Template engine** — deterministic, zero AI cost. Handles the common phrasings below (marked ⚡).
7
+ - **AI engine** — handles anything else, and (with **MCP** on) sees the real page to pick accurate locators.
8
+
9
+ Tips that apply everywhere:
10
+ - Put the **target name in quotes** for reliability: `Click the "Sign in" button`.
11
+ - Insert dynamic data with the **Fake Data** picker (`{{FAKE_*}}`) and uploaded files with the **Files** picker (`{{FILE:name}}`).
12
+ - Reference saved values with `{{VAR_NAME}}`; `{{BASE_URL}}` is your configured base URL.
13
+ - Prefix any assertion with **"Soft"** to keep running on failure (`Soft verify …`).
14
+
15
+ ---
16
+
17
+ ## UI steps
18
+
19
+ ### Navigation ⚡
20
+ ```text
21
+ Go to https://example.com
22
+ Navigate to /dashboard
23
+ Open https://demoqa.com/upload-download
24
+ Go back
25
+ Refresh the page
26
+ ```
27
+
28
+ ### Click — buttons, links, tabs ⚡
29
+ Name the role so the right locator is used (`getByRole`):
30
+ ```text
31
+ Click the "Login" button
32
+ Click on "Submit"
33
+ Click the "Dashboard" link
34
+ Click the "Settings" tab
35
+ Double click the "Row 1" element
36
+ Right click the "File" element
37
+ ```
38
+ > The role word should match the element's **actual ARIA role**. A control that looks like a button but is `<a role="button">` is still a button; a plain `<a href>` is a link.
39
+
40
+ ### Text input ⚡
41
+ ```text
42
+ Enter "deepak" in the Username field
43
+ Type {{FAKE_INTERNET_EMAIL}} in the Email field
44
+ Fill "secret123" in the Password field # password → getByLabel automatically
45
+ Enter {{FAKE_NUMBER_INT}} in the Quantity field # number → spinbutton role
46
+ Clear the Search field
47
+ ```
48
+
49
+ ### Dropdowns, checkboxes, radios ⚡
50
+ ```text
51
+ Select "India" from the Country dropdown
52
+ Check the "Remember me" checkbox
53
+ Uncheck the "Subscribe" checkbox
54
+ Select the "Male" radio button
55
+ ```
56
+
57
+ ### Keyboard ⚡
58
+ ```text
59
+ Press Enter
60
+ Press Escape
61
+ Press Tab
62
+ Press "Control+A"
63
+ Type "hello" and press Enter
64
+ ```
65
+
66
+ ### Waits, scroll, hover, screenshot ⚡
67
+ ```text
68
+ Wait for 2 seconds
69
+ Wait for the page to load
70
+ Scroll down
71
+ Scroll to bottom
72
+ Hover over the "Profile" menu
73
+ Take a screenshot
74
+ ```
75
+
76
+ ### File upload ⚡
77
+ Upload a file with the **Files** picker, then reference its `{{FILE:name}}` token:
78
+ ```text
79
+ Upload {{FILE:resume.pdf}} to the Resume field
80
+ Upload {{FILE:resume.pdf}} to the Resume field and wait for 'Upload complete' # most reliable
81
+ Upload {{FILE:photo.png}} # page's file input
82
+ ```
83
+
84
+ ### File download ⚡
85
+ Downloads are auto-captured (Reports → Downloads, and attached to Allure). The step waits until the file finishes:
86
+ ```text
87
+ Download the "Export CSV" link
88
+ Click the "Download" button to download
89
+ Download the file by clicking "Report PDF"
90
+ Click "Download invoice" and wait for download
91
+ ```
92
+
93
+ ### Assertions ⚡
94
+ ```text
95
+ Verify "Welcome back" is visible
96
+ Verify "Error" is not visible
97
+ Verify the page title is "Dashboard"
98
+ Verify the URL contains "/dashboard"
99
+ Verify the "Email" field has value "user@test.com"
100
+ Soft verify "Saved successfully" is visible # continues on failure
101
+ ```
102
+
103
+ ### Scenarios the AI handles (MCP recommended)
104
+ ```text
105
+ Login as admin using {{ADMIN_EMAIL}} and {{DEFAULT_PASSWORD}}
106
+ Click the "Submit" button inside the payment iframe
107
+ Fill the card number "4111111111111111" in the Stripe iframe
108
+ Accept the confirmation dialog after clicking "Delete"
109
+ Verify the table has 5 rows
110
+ Click the element inside the shadow DOM component
111
+ ```
112
+ - **iframes** → wrapped in `page.frameLocator(...)` automatically.
113
+ - **shadow DOM** → locator chaining (`page.locator('host').locator('inner')`).
114
+ - **dialogs** → a `page.once('dialog', …)` handler is added before the trigger.
115
+
116
+ ---
117
+
118
+ ## API steps
119
+
120
+ Mark a suite/case as **API** type. Steps use Playwright's `page.request` API. Use `{{BASE_URL}}` and saved variables.
121
+
122
+ ### Basic requests
123
+ ```text
124
+ Send a GET request to {{BASE_URL}}/api/users and verify status is 200
125
+ Send a POST request to {{BASE_URL}}/api/users with body { "name": "John", "email": "john@test.com" } and verify status 201
126
+ Send a PUT request to {{BASE_URL}}/api/users/1 with body { "name": "Jane" }
127
+ Send a DELETE request to {{BASE_URL}}/api/users/1 and verify status 204
128
+ ```
129
+
130
+ ### Auth — extract and reuse a token
131
+ ```text
132
+ POST to {{BASE_URL}}/api/login with { "email": "{{ADMIN_EMAIL}}", "password": "{{DEFAULT_PASSWORD}}" }, save token as AUTH_TOKEN
133
+ GET {{BASE_URL}}/api/profile with Authorization Bearer {{AUTH_TOKEN}} and verify status 200
134
+ ```
135
+ - "save / store / extract … as NAME" → `context.setVariable('NAME', …)`.
136
+ - `{{AUTH_TOKEN}}` (or `context.getVariable`) reuses it in later steps/cases.
137
+
138
+ ### Assertions on the response
139
+ ```text
140
+ Verify the response status is 200
141
+ Verify the response body has property "id"
142
+ Verify the response field "name" equals "Admin"
143
+ Verify the response has 5 users
144
+ Verify the content-type header contains "application/json"
145
+ Verify the response time is under 500 ms
146
+ Soft verify the response status is 200
147
+ ```
148
+
149
+ ### Headers, query params, form bodies
150
+ ```text
151
+ Send a GET request to {{BASE_URL}}/api/search with query { "q": "phone", "page": "1" }
152
+ Send a POST to {{BASE_URL}}/api/upload with form { "name": "doc", "type": "pdf" }
153
+ Send a GET to {{BASE_URL}}/api/data with header "X-Api-Key" "{{API_KEY}}"
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Soft vs hard assertions
159
+ - **Hard** (default): the test stops at the first failure.
160
+ - **Soft**: prefix with **"Soft"** (`Soft verify …`) — failures are collected and the test keeps going. In the Recorder, use `Ctrl+Shift+Click` for a soft assertion.
161
+
162
+ ## Variables & dynamic data
163
+ | Token | Resolves to |
164
+ |-------|-------------|
165
+ | `{{BASE_URL}}` | Configured base URL for the active environment |
166
+ | `{{FAKE_PERSON_FULLNAME}}`, `{{FAKE_INTERNET_EMAIL}}`, … | Fresh random data each run (Fake Data picker) |
167
+ | `{{FILE:resume.pdf}}` | `fixtures/uploads/resume.pdf` (Files picker) |
168
+ | `{{MY_VAR}}` | A value from Variables or saved earlier with `context.setVariable` |
169
+
170
+ ## Editing generated code
171
+ Every step's code is editable — expand the step (chevron) and edit the code block directly, or click the **✎** to change the instruction and **regenerate** (wand). Use this for anything the engines don't phrase exactly the way your app needs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assuremind",
3
- "version": "1.2.3",
3
+ "version": "1.4.0",
4
4
  "description": "AI-powered codeless UI & API automation framework",
5
5
  "author": "Deepak Hiremath",
6
6
  "license": "MIT",
@@ -41,12 +41,14 @@
41
41
  "ai",
42
42
  "codeless",
43
43
  "no-code",
44
+ "zero-code",
44
45
  "test-automation",
45
46
  "qa",
46
47
  "self-healing",
47
48
  "claude",
48
49
  "openai",
49
50
  "gemini",
51
+ "ollama",
50
52
  "ui-testing"
51
53
  ],
52
54
  "scripts": {
@@ -74,7 +76,7 @@
74
76
  "@google/generative-ai": "^0.21.0",
75
77
  "@modelcontextprotocol/sdk": "^1.29.0",
76
78
  "@playwright/mcp": "^0.0.70",
77
- "@playwright/test": "^1.47.0",
79
+ "@playwright/test": "^1.60.0",
78
80
  "allure-commandline": "^2.38.1",
79
81
  "allure-js-commons": "^3.0.0",
80
82
  "allure-playwright": "^3.0.0",
@@ -97,10 +99,10 @@
97
99
  "pino": "^9.4.0",
98
100
  "pino-pretty": "^11.2.0",
99
101
  "pixelmatch": "^7.1.0",
100
- "playwright": "^1.47.0",
102
+ "playwright": "^1.60.0",
101
103
  "pngjs": "^7.0.0",
102
104
  "simple-git": "^3.33.0",
103
- "uuid": "^10.0.0",
105
+ "uuid": "11.1.1",
104
106
  "zod": "^3.23.0"
105
107
  },
106
108
  "devDependencies": {
@@ -113,12 +115,12 @@
113
115
  "@types/ws": "^8.5.0",
114
116
  "@typescript-eslint/eslint-plugin": "^8.8.0",
115
117
  "@typescript-eslint/parser": "^8.8.0",
116
- "@vitest/coverage-v8": "^2.1.0",
118
+ "@vitest/coverage-v8": "3.2.6",
117
119
  "docx": "^9.6.1",
118
120
  "eslint": "^9.12.0",
119
121
  "prettier": "^3.3.0",
120
122
  "tsup": "^8.3.0",
121
123
  "typescript": "^5.6.0",
122
- "vitest": "^2.1.0"
124
+ "vitest": "3.2.6"
123
125
  }
124
126
  }
@@ -0,0 +1,171 @@
1
+ # Writing Test Steps
2
+
3
+ A reference for describing test steps in plain English. Type a step in the **Test Editor** (or `assuremind generate`) and AssureMind turns it into Playwright code.
4
+
5
+ Two engines handle generation:
6
+ - **⚡ Template engine** — deterministic, zero AI cost. Handles the common phrasings below (marked ⚡).
7
+ - **AI engine** — handles anything else, and (with **MCP** on) sees the real page to pick accurate locators.
8
+
9
+ Tips that apply everywhere:
10
+ - Put the **target name in quotes** for reliability: `Click the "Sign in" button`.
11
+ - Insert dynamic data with the **Fake Data** picker (`{{FAKE_*}}`) and uploaded files with the **Files** picker (`{{FILE:name}}`).
12
+ - Reference saved values with `{{VAR_NAME}}`; `{{BASE_URL}}` is your configured base URL.
13
+ - Prefix any assertion with **"Soft"** to keep running on failure (`Soft verify …`).
14
+
15
+ ---
16
+
17
+ ## UI steps
18
+
19
+ ### Navigation ⚡
20
+ ```text
21
+ Go to https://example.com
22
+ Navigate to /dashboard
23
+ Open https://demoqa.com/upload-download
24
+ Go back
25
+ Refresh the page
26
+ ```
27
+
28
+ ### Click — buttons, links, tabs ⚡
29
+ Name the role so the right locator is used (`getByRole`):
30
+ ```text
31
+ Click the "Login" button
32
+ Click on "Submit"
33
+ Click the "Dashboard" link
34
+ Click the "Settings" tab
35
+ Double click the "Row 1" element
36
+ Right click the "File" element
37
+ ```
38
+ > The role word should match the element's **actual ARIA role**. A control that looks like a button but is `<a role="button">` is still a button; a plain `<a href>` is a link.
39
+
40
+ ### Text input ⚡
41
+ ```text
42
+ Enter "deepak" in the Username field
43
+ Type {{FAKE_INTERNET_EMAIL}} in the Email field
44
+ Fill "secret123" in the Password field # password → getByLabel automatically
45
+ Enter {{FAKE_NUMBER_INT}} in the Quantity field # number → spinbutton role
46
+ Clear the Search field
47
+ ```
48
+
49
+ ### Dropdowns, checkboxes, radios ⚡
50
+ ```text
51
+ Select "India" from the Country dropdown
52
+ Check the "Remember me" checkbox
53
+ Uncheck the "Subscribe" checkbox
54
+ Select the "Male" radio button
55
+ ```
56
+
57
+ ### Keyboard ⚡
58
+ ```text
59
+ Press Enter
60
+ Press Escape
61
+ Press Tab
62
+ Press "Control+A"
63
+ Type "hello" and press Enter
64
+ ```
65
+
66
+ ### Waits, scroll, hover, screenshot ⚡
67
+ ```text
68
+ Wait for 2 seconds
69
+ Wait for the page to load
70
+ Scroll down
71
+ Scroll to bottom
72
+ Hover over the "Profile" menu
73
+ Take a screenshot
74
+ ```
75
+
76
+ ### File upload ⚡
77
+ Upload a file with the **Files** picker, then reference its `{{FILE:name}}` token:
78
+ ```text
79
+ Upload {{FILE:resume.pdf}} to the Resume field
80
+ Upload {{FILE:resume.pdf}} to the Resume field and wait for 'Upload complete' # most reliable
81
+ Upload {{FILE:photo.png}} # page's file input
82
+ ```
83
+
84
+ ### File download ⚡
85
+ Downloads are auto-captured (Reports → Downloads, and attached to Allure). The step waits until the file finishes:
86
+ ```text
87
+ Download the "Export CSV" link
88
+ Click the "Download" button to download
89
+ Download the file by clicking "Report PDF"
90
+ Click "Download invoice" and wait for download
91
+ ```
92
+
93
+ ### Assertions ⚡
94
+ ```text
95
+ Verify "Welcome back" is visible
96
+ Verify "Error" is not visible
97
+ Verify the page title is "Dashboard"
98
+ Verify the URL contains "/dashboard"
99
+ Verify the "Email" field has value "user@test.com"
100
+ Soft verify "Saved successfully" is visible # continues on failure
101
+ ```
102
+
103
+ ### Scenarios the AI handles (MCP recommended)
104
+ ```text
105
+ Login as admin using {{ADMIN_EMAIL}} and {{DEFAULT_PASSWORD}}
106
+ Click the "Submit" button inside the payment iframe
107
+ Fill the card number "4111111111111111" in the Stripe iframe
108
+ Accept the confirmation dialog after clicking "Delete"
109
+ Verify the table has 5 rows
110
+ Click the element inside the shadow DOM component
111
+ ```
112
+ - **iframes** → wrapped in `page.frameLocator(...)` automatically.
113
+ - **shadow DOM** → locator chaining (`page.locator('host').locator('inner')`).
114
+ - **dialogs** → a `page.once('dialog', …)` handler is added before the trigger.
115
+
116
+ ---
117
+
118
+ ## API steps
119
+
120
+ Mark a suite/case as **API** type. Steps use Playwright's `page.request` API. Use `{{BASE_URL}}` and saved variables.
121
+
122
+ ### Basic requests
123
+ ```text
124
+ Send a GET request to {{BASE_URL}}/api/users and verify status is 200
125
+ Send a POST request to {{BASE_URL}}/api/users with body { "name": "John", "email": "john@test.com" } and verify status 201
126
+ Send a PUT request to {{BASE_URL}}/api/users/1 with body { "name": "Jane" }
127
+ Send a DELETE request to {{BASE_URL}}/api/users/1 and verify status 204
128
+ ```
129
+
130
+ ### Auth — extract and reuse a token
131
+ ```text
132
+ POST to {{BASE_URL}}/api/login with { "email": "{{ADMIN_EMAIL}}", "password": "{{DEFAULT_PASSWORD}}" }, save token as AUTH_TOKEN
133
+ GET {{BASE_URL}}/api/profile with Authorization Bearer {{AUTH_TOKEN}} and verify status 200
134
+ ```
135
+ - "save / store / extract … as NAME" → `context.setVariable('NAME', …)`.
136
+ - `{{AUTH_TOKEN}}` (or `context.getVariable`) reuses it in later steps/cases.
137
+
138
+ ### Assertions on the response
139
+ ```text
140
+ Verify the response status is 200
141
+ Verify the response body has property "id"
142
+ Verify the response field "name" equals "Admin"
143
+ Verify the response has 5 users
144
+ Verify the content-type header contains "application/json"
145
+ Verify the response time is under 500 ms
146
+ Soft verify the response status is 200
147
+ ```
148
+
149
+ ### Headers, query params, form bodies
150
+ ```text
151
+ Send a GET request to {{BASE_URL}}/api/search with query { "q": "phone", "page": "1" }
152
+ Send a POST to {{BASE_URL}}/api/upload with form { "name": "doc", "type": "pdf" }
153
+ Send a GET to {{BASE_URL}}/api/data with header "X-Api-Key" "{{API_KEY}}"
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Soft vs hard assertions
159
+ - **Hard** (default): the test stops at the first failure.
160
+ - **Soft**: prefix with **"Soft"** (`Soft verify …`) — failures are collected and the test keeps going. In the Recorder, use `Ctrl+Shift+Click` for a soft assertion.
161
+
162
+ ## Variables & dynamic data
163
+ | Token | Resolves to |
164
+ |-------|-------------|
165
+ | `{{BASE_URL}}` | Configured base URL for the active environment |
166
+ | `{{FAKE_PERSON_FULLNAME}}`, `{{FAKE_INTERNET_EMAIL}}`, … | Fresh random data each run (Fake Data picker) |
167
+ | `{{FILE:resume.pdf}}` | `fixtures/uploads/resume.pdf` (Files picker) |
168
+ | `{{MY_VAR}}` | A value from Variables or saved earlier with `context.setVariable` |
169
+
170
+ ## Editing generated code
171
+ Every step's code is editable — expand the step (chevron) and edit the code block directly, or click the **✎** to change the instruction and **regenerate** (wand). Use this for anything the engines don't phrase exactly the way your app needs.