form-tester 0.9.1 → 0.10.1
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.
|
@@ -52,113 +52,123 @@ Notes:
|
|
|
52
52
|
- The CLI opens the form with Playwright CLI, saves an initial snapshot + screenshot, and prints next-step commands.
|
|
53
53
|
- Use `/save {label}` to capture additional snapshots + screenshots into the same output folder.
|
|
54
54
|
- Use `/update` after changing the skill to update Playwright CLI + skills, then reload the skill in Copilot CLI with `/skills` (reload) or `/restart`.
|
|
55
|
-
- Use `/people` to rescan the visible person list and get a numbered selection prompt.
|
|
56
|
-
- Use the next-step checklist printed by the CLI (cookies, person selection, validation fix, Dokumenter verification, save HTML/PDF).
|
|
57
|
-
- If the error modal appears on save or submit ("Det skjedde en feil under innsending av skjema. Prøv igjen senere."), open DevTools -> Network before retrying. Then try resubmitting once. If it persists, find the failed request and capture the Correlation ID header in test_results.txt.
|
|
58
55
|
- Use `--help` or `-h` to print the command list without starting the prompt.
|
|
59
56
|
- IMPORTANT: Always use `form-tester exec` instead of `playwright-cli` directly. This records all commands for replay. Same syntax: `form-tester exec fill e1 "value"`, `form-tester exec click e3`, `form-tester exec close` (finalizes recording).
|
|
60
57
|
- Replay a previous run: `form-tester replay output/form-id/timestamp/recording.json`
|
|
61
58
|
- IMPORTANT: When something unexpected happens during a test — wrong page state, unexpected modal, failed command, element not found, timeout, wrong document format — ALWAYS log an issue:
|
|
62
59
|
`form-tester issue <category> "<description of what happened>"`
|
|
63
60
|
Categories: person-selection, navigation, form-fill, submission, documents, pdf-download, html-capture, screenshot, snapshot, validation, modal, timeout, other
|
|
64
|
-
Example: `form-tester issue modal "Submit showed error modal instead of success: Det skjedde en feil"`
|
|
65
|
-
Example: `form-tester issue person-selection "Person list showed 0 options, had to retry manually"`
|
|
66
|
-
These logs help us improve the skill to handle more scenarios automatically.
|
|
67
61
|
- View logged issues: `form-tester issues`
|
|
68
|
-
- IMPORTANT: All screenshots
|
|
69
|
-
|
|
62
|
+
- IMPORTANT: All screenshots MUST use `--full-page`. Take a full-page screenshot EVERY TIME the page changes.
|
|
63
|
+
|
|
64
|
+
Test flow (step by step):
|
|
70
65
|
|
|
71
|
-
Test flow (when /test is triggered):
|
|
72
66
|
IMPORTANT: Each prompt below MUST be asked as a separate message to the user. Wait for the user's response before proceeding to the next step. Do NOT combine multiple prompts into one message.
|
|
73
67
|
|
|
74
68
|
1. Prompt for PNR (if not in URL). Wait for response.
|
|
75
69
|
2. Prompt for persona (1-6 selection). Show the numbered list and wait for the user's response.
|
|
76
|
-
3. Prompt for test scenario in a NEW separate message.
|
|
77
|
-
4. Only after receiving answers to all prompts
|
|
70
|
+
3. Prompt for test scenario in a NEW separate message. Wait for the user's response.
|
|
71
|
+
4. Only after receiving answers to all prompts, proceed with the steps below.
|
|
72
|
+
|
|
73
|
+
Step 1 — Open and setup:
|
|
74
|
+
```
|
|
75
|
+
form-tester test <url> --auto --pnr <pnr> --persona <id> --scenario "<text>"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Step 2 — Dismiss cookies:
|
|
79
|
+
```
|
|
80
|
+
form-tester cookies
|
|
81
|
+
```
|
|
82
|
+
This automatically finds and clicks the cookie banner. No-op if no banner is present.
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
Step 3 — Select person:
|
|
85
|
+
```
|
|
86
|
+
form-tester select-person
|
|
87
|
+
```
|
|
88
|
+
This scans for available persons, selects the recommended one ("Uromantisk Direktør"), clicks it, and takes a screenshot. To select a specific person:
|
|
89
|
+
```
|
|
90
|
+
form-tester select-person "Navn Navnesen"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Step 4 — Study the form structure:
|
|
94
|
+
Take a snapshot and study the FULL form before filling anything:
|
|
81
95
|
1. Identify ALL sections, including collapsed/accordion sections (buttons with arrow icons).
|
|
82
|
-
2. Expand ALL collapsed sections FIRST by clicking their header buttons.
|
|
83
|
-
3.
|
|
84
|
-
4.
|
|
96
|
+
2. Expand ALL collapsed sections FIRST by clicking their header buttons.
|
|
97
|
+
3. Take a new snapshot after expanding.
|
|
98
|
+
4. Identify ALL required fields across all sections.
|
|
99
|
+
|
|
100
|
+
Step 5 — Fill the form:
|
|
101
|
+
Fill fields section by section, top to bottom. Use `form-tester exec` for all commands.
|
|
85
102
|
|
|
86
103
|
Autosuggest / search fields (e.g. "Søk opp et legemiddel"):
|
|
87
|
-
|
|
88
|
-
Instead:
|
|
104
|
+
Do NOT use `fill` + `Enter` — the value won't commit. Instead:
|
|
89
105
|
```
|
|
90
106
|
form-tester exec fill <ref> "search text"
|
|
91
107
|
```
|
|
92
|
-
|
|
108
|
+
Wait for the dropdown, take a snapshot to find the suggestion, then click it:
|
|
93
109
|
```
|
|
94
110
|
form-tester exec snapshot
|
|
111
|
+
form-tester exec click <suggestion-ref>
|
|
95
112
|
```
|
|
96
|
-
|
|
113
|
+
Or use run-code:
|
|
97
114
|
```
|
|
98
115
|
form-tester exec run-code "async page => { const input = page.locator('#fieldId'); await input.fill('search text'); await page.waitForTimeout(1000); const option = page.locator('[role=\"option\"]').first(); await option.click(); }"
|
|
99
116
|
```
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
1. MAXIMUM 3 submit attempts. If validation errors persist after 3 attempts, STOP. Log the remaining errors with `form-tester issue validation "..."` and note them in test_results.txt. Do NOT keep retrying.
|
|
104
|
-
2. After each failed submit, take a snapshot and READ the validation error list carefully.
|
|
105
|
-
3. Each validation error is a clickable link with an href like `#fieldId`. Click the error link to scroll to and focus the unfilled field. This is the ONLY reliable way to find the field.
|
|
106
|
-
4. After clicking the error link, take a snapshot to see the field in context and fill it.
|
|
107
|
-
5. Do NOT re-fill fields that are already filled. Only fix the fields listed in the validation errors.
|
|
108
|
-
6. Do NOT use JavaScript `dispatchEvent` hacks or `element.evaluate()` to set values — these bypass React's state and the form won't register the value. Always use Playwright's `fill`, `click`, `select` commands.
|
|
109
|
-
7. Before resubmitting, verify that the number of validation errors has decreased. If the same errors persist after you tried to fix them, the approach isn't working — try a different strategy (e.g., expand a collapsed section, use a different selector).
|
|
110
|
-
8. Some forms have accordion/collapsible sections. Validation errors inside collapsed sections cannot be filled until the section is expanded. Look for buttons near the error's field ID in the snapshot and click to expand.
|
|
118
|
+
Step 6 — Submit:
|
|
119
|
+
Take a screenshot before submitting, then click the submit button.
|
|
111
120
|
|
|
112
|
-
|
|
121
|
+
Step 7 — Handle validation errors:
|
|
122
|
+
IMPORTANT: If validation errors appear after clicking submit, the form DID NOT SUBMIT. The errors are blocking submission. You must fix them first.
|
|
123
|
+
|
|
124
|
+
Run the validate command to find and scroll to each error:
|
|
125
|
+
```
|
|
126
|
+
form-tester validate
|
|
127
|
+
```
|
|
128
|
+
This will:
|
|
129
|
+
1. Take a snapshot and parse all validation errors
|
|
130
|
+
2. Click each error link to scroll to the unfilled field
|
|
131
|
+
3. Take a snapshot at each field so you can see what needs to be filled
|
|
132
|
+
4. Print structured output with field IDs and error messages
|
|
133
|
+
|
|
134
|
+
After `form-tester validate`, fix each field it found, then run `form-tester validate` again to confirm errors are resolved. Only then resubmit.
|
|
135
|
+
|
|
136
|
+
CRITICAL RULES:
|
|
137
|
+
- MAXIMUM 3 submit attempts. After 3, STOP. Log remaining errors with `form-tester issue validation "..."` and note them in test_results.txt.
|
|
138
|
+
- Do NOT re-fill fields that are already filled. Only fix fields listed in validation errors.
|
|
139
|
+
- Do NOT use JavaScript `dispatchEvent` hacks or `element.evaluate()` to set values — these bypass React's state. Always use Playwright's `fill`, `click`, `select` commands.
|
|
140
|
+
- If the same errors persist after fixing, the field might be inside a collapsed accordion section. Expand it first.
|
|
141
|
+
|
|
142
|
+
Step 8 — Post-submit verification:
|
|
113
143
|
After a successful submission, read the modal text carefully:
|
|
114
|
-
- If it says the form is stored in Dokumenter (e.g. "
|
|
115
|
-
- If the modal does NOT mention Dokumenter,
|
|
144
|
+
- If it says the form is stored in Dokumenter (e.g. "lagret i Dokumenter"), proceed with Dokumenter verification.
|
|
145
|
+
- If the modal does NOT mention Dokumenter, skip Dokumenter verification. Record this in test_results.txt.
|
|
116
146
|
|
|
117
|
-
Dokumenter verification (only when modal confirms storage):
|
|
118
|
-
Use the standardized documents command — it handles navigation, format detection, PDF download, and HTML capture automatically:
|
|
147
|
+
Step 9 — Dokumenter verification (only when modal confirms storage):
|
|
119
148
|
```
|
|
120
149
|
form-tester documents
|
|
121
150
|
```
|
|
122
|
-
This
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
3. Click "Se detaljer" on the first document, then click "Åpne dokumentet".
|
|
134
|
-
4. After clicking "Åpne dokumentet", determine the document format:
|
|
135
|
-
|
|
136
|
-
How to detect format:
|
|
137
|
-
- Take a snapshot: `form-tester exec snapshot`
|
|
138
|
-
- If snapshot shows a link with href containing `/pdf/` or `blob:` → PDF
|
|
139
|
-
- If `--full-page` screenshot times out → PDF (do NOT retry, switch to download)
|
|
140
|
-
- If snapshot shows rendered HTML content (headings, paragraphs, form data) → HTML
|
|
141
|
-
|
|
142
|
-
PDF documents — DOWNLOAD, do NOT screenshot:
|
|
143
|
-
IMPORTANT: `run-code` does NOT have access to `require` or `fs`. Do NOT use `require('fs')`. Use Playwright's download API instead.
|
|
144
|
-
|
|
145
|
-
Find the PDF download link in the snapshot (look for `a[href*="/pdf/"]` or "Last ned" link), then download using the Playwright download event:
|
|
151
|
+
This handles the full flow: navigate to Dokumenter, find latest doc, detect format, download PDF or screenshot HTML.
|
|
152
|
+
|
|
153
|
+
If `form-tester documents` fails (logged as issues), fall back to manual steps:
|
|
154
|
+
1. Navigate to `/dokumenter?pnr={PNR}` and select the same person.
|
|
155
|
+
2. Click "Se detaljer" on the first document, then "Åpne dokumentet".
|
|
156
|
+
3. Detect format from snapshot:
|
|
157
|
+
- `a[href*="/pdf/"]` or `blob:` → PDF
|
|
158
|
+
- `--full-page` screenshot times out → PDF
|
|
159
|
+
- Rendered HTML content → HTML
|
|
160
|
+
|
|
161
|
+
PDF — download:
|
|
146
162
|
```
|
|
147
163
|
form-tester exec run-code "async page => { const link = page.locator('a[href*=\"/pdf/\"]').first(); const [download] = await Promise.all([ page.waitForEvent('download'), link.click() ]); await download.saveAs('$OUTPUT_DIR/document.pdf'); }"
|
|
148
164
|
```
|
|
149
|
-
If there is no direct PDF link but a "Last ned" button:
|
|
150
|
-
```
|
|
151
|
-
form-tester exec run-code "async page => { const [download] = await Promise.all([ page.waitForEvent('download'), page.getByRole('link', { name: 'Last ned' }).click() ]); await download.saveAs('$OUTPUT_DIR/document.pdf'); }"
|
|
152
|
-
```
|
|
153
|
-
Verify the download: check that document.pdf exists in the output folder.
|
|
154
165
|
|
|
155
|
-
HTML
|
|
166
|
+
HTML — screenshot + save:
|
|
156
167
|
```
|
|
157
168
|
form-tester exec screenshot --filename "$OUTPUT_DIR/document_screenshot.png" --full-page
|
|
169
|
+
form-tester exec eval "document.documentElement.outerHTML"
|
|
158
170
|
```
|
|
159
|
-
Also save raw HTML: `form-tester exec eval "document.documentElement.outerHTML"` → save to document.html.
|
|
160
|
-
|
|
161
|
-
XML/other: Note type in test_results.txt, skip capture.
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
Step 10 — Finalize:
|
|
173
|
+
- Write test_results.txt with status, data used, and notes.
|
|
174
|
+
- Close browser: `form-tester exec close`
|
|
@@ -47,21 +47,31 @@ Replay a previous run:
|
|
|
47
47
|
form-tester replay output/form-id/timestamp/recording.json
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Standardized commands
|
|
51
|
+
|
|
52
|
+
Use these instead of manual steps — they handle retries, edge cases, and error logging automatically:
|
|
51
53
|
|
|
52
|
-
After form submission, use the standardized documents command:
|
|
53
54
|
```bash
|
|
54
|
-
form-tester
|
|
55
|
+
form-tester cookies # dismiss cookie banner (tries known selectors)
|
|
56
|
+
form-tester select-person # select recommended person ("Uromantisk Direktør")
|
|
57
|
+
form-tester select-person "Name" # select specific person by name
|
|
58
|
+
form-tester validate # parse validation errors, scroll to each field, show context
|
|
59
|
+
form-tester documents # navigate to Dokumenter, detect PDF/HTML, capture
|
|
60
|
+
form-tester issue <category> "<text>" # log an issue for skill improvement
|
|
61
|
+
form-tester issues # view recent issues
|
|
55
62
|
```
|
|
56
|
-
|
|
63
|
+
|
|
64
|
+
### Typical test flow:
|
|
65
|
+
1. `form-tester test <url> --auto` — open form
|
|
66
|
+
2. `form-tester cookies` — dismiss cookies
|
|
67
|
+
3. `form-tester select-person` — select person
|
|
68
|
+
4. Fill the form with `form-tester exec fill/click/select`
|
|
69
|
+
5. Submit, then `form-tester validate` — find and fix any validation errors
|
|
70
|
+
6. `form-tester documents` — verify document after successful submission
|
|
71
|
+
7. `form-tester exec close` — finalize recording
|
|
57
72
|
|
|
58
73
|
## Issue logging
|
|
59
74
|
|
|
60
|
-
When something unexpected happens during a test, log it for skill improvement:
|
|
61
|
-
```bash
|
|
62
|
-
form-tester issue <category> "<description>"
|
|
63
|
-
form-tester issues # view recent issues
|
|
64
|
-
```
|
|
65
75
|
Categories: `person-selection`, `navigation`, `form-fill`, `submission`, `documents`, `pdf-download`, `html-capture`, `screenshot`, `snapshot`, `validation`, `modal`, `timeout`, `other`
|
|
66
76
|
|
|
67
77
|
## Interactive commands
|
|
@@ -10,96 +10,105 @@ npm install -g form-tester
|
|
|
10
10
|
form-tester install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
## Running
|
|
13
|
+
## Running a test
|
|
14
14
|
|
|
15
|
+
When the user gives you a form URL to test, execute ALL steps below in sequence WITHOUT stopping to ask. Do not ask "want me to continue?" — just do the entire flow.
|
|
16
|
+
|
|
17
|
+
### Step 1 — Start the test
|
|
15
18
|
```bash
|
|
16
|
-
# AI mode (default) — no prompts:
|
|
17
19
|
form-tester test <url> --auto
|
|
20
|
+
```
|
|
21
|
+
Or with options:
|
|
22
|
+
```bash
|
|
18
23
|
form-tester test <url> --auto --pnr 12345 --persona ung-mann --scenario "test validation"
|
|
19
|
-
|
|
20
|
-
# Human mode — user chooses persona and scenario:
|
|
21
|
-
form-tester test <url> --human # lists personas
|
|
22
|
-
form-tester test <url> --human --persona ung-mann --scenario "test X" # run with choices
|
|
23
|
-
|
|
24
|
-
# Full interactive CLI:
|
|
25
|
-
form-tester
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
2. Ask the user to pick a persona and scenario.
|
|
33
|
-
3. Re-run: `form-tester test <url> --human --persona <id> --scenario "<text>"` (use `""` for standard test).
|
|
34
|
-
Otherwise default to `--auto`.
|
|
35
|
-
|
|
36
|
-
## Commands
|
|
37
|
-
|
|
38
|
-
| Command | Description |
|
|
39
|
-
|---------|-------------|
|
|
40
|
-
| `/setup` | Initial setup |
|
|
41
|
-
| `/update` | Update Playwright CLI + skills |
|
|
42
|
-
| `/version` | Show version |
|
|
43
|
-
| `/people` | Rescan visible person list |
|
|
44
|
-
| `/test {url}` | Test a form URL |
|
|
45
|
-
| `/save {label}` | Save snapshot + screenshot |
|
|
46
|
-
| `/clear` | Clear session |
|
|
47
|
-
| `/quit` | Exit CLI |
|
|
48
|
-
|
|
49
|
-
## Playwright CLI (use via form-tester exec)
|
|
26
|
+
### Step 2 — Dismiss cookies
|
|
27
|
+
```bash
|
|
28
|
+
form-tester cookies
|
|
29
|
+
```
|
|
50
30
|
|
|
51
|
-
|
|
31
|
+
### Step 3 — Select person
|
|
32
|
+
```bash
|
|
33
|
+
form-tester select-person
|
|
34
|
+
```
|
|
35
|
+
To select a specific person: `form-tester select-person "Name"`
|
|
52
36
|
|
|
37
|
+
### Step 4 — Study the form
|
|
38
|
+
Take a snapshot and identify ALL sections and required fields:
|
|
53
39
|
```bash
|
|
54
|
-
form-tester exec open https://example.com
|
|
55
40
|
form-tester exec snapshot
|
|
56
|
-
form-tester exec fill e1 "value"
|
|
57
|
-
form-tester exec click e3
|
|
58
|
-
form-tester exec screenshot --filename=page.png --full-page
|
|
59
|
-
form-tester exec close # finalizes recording
|
|
60
41
|
```
|
|
42
|
+
Look for collapsed/accordion sections (buttons with arrow icons). Expand ALL of them by clicking their header buttons before filling anything.
|
|
61
43
|
|
|
62
|
-
|
|
44
|
+
### Step 5 — Fill the form
|
|
45
|
+
Use `form-tester exec` for ALL commands (this records them for replay):
|
|
63
46
|
```bash
|
|
64
|
-
form-tester
|
|
47
|
+
form-tester exec fill <ref> "value"
|
|
48
|
+
form-tester exec click <ref>
|
|
49
|
+
form-tester exec select <ref> "option text"
|
|
50
|
+
form-tester exec screenshot --filename "path.png" --full-page
|
|
65
51
|
```
|
|
66
52
|
|
|
67
|
-
|
|
53
|
+
For autosuggest/search fields: fill the text, wait for dropdown, then click the suggestion:
|
|
54
|
+
```bash
|
|
55
|
+
form-tester exec fill <ref> "search text"
|
|
56
|
+
form-tester exec snapshot # find the suggestion element
|
|
57
|
+
form-tester exec click <suggestion-ref>
|
|
58
|
+
```
|
|
68
59
|
|
|
69
|
-
|
|
60
|
+
### Step 6 — Submit
|
|
61
|
+
Take a screenshot, then click the submit button.
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
### Step 7 — Handle validation errors
|
|
64
|
+
IMPORTANT: If validation errors appear after submit, the form DID NOT SUBMIT. Run:
|
|
65
|
+
```bash
|
|
66
|
+
form-tester validate
|
|
67
|
+
```
|
|
68
|
+
This parses all validation errors, clicks each error link to scroll to the field, and shows what needs to be filled. Fix each field, then run `form-tester validate` again to confirm. Only then resubmit.
|
|
75
69
|
|
|
76
|
-
|
|
70
|
+
RULES:
|
|
71
|
+
- Maximum 3 submit attempts. After 3, STOP and write results.
|
|
72
|
+
- Do NOT re-fill fields that are already filled.
|
|
73
|
+
- Do NOT use JavaScript `dispatchEvent` or `element.evaluate()` to set values.
|
|
74
|
+
- Always use Playwright's `fill`, `click`, `select` commands.
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- If an error modal appears on submit, open DevTools -> Network, retry once, and capture the Correlation ID header.
|
|
76
|
+
### Step 8 — Post-submit verification
|
|
77
|
+
After successful submission, read the modal text:
|
|
78
|
+
- If it mentions Dokumenter storage ("lagret i Dokumenter") → run document verification
|
|
79
|
+
- If it does NOT mention Dokumenter → skip, note in test_results.txt
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
### Step 9 — Document verification
|
|
82
|
+
```bash
|
|
83
|
+
form-tester documents
|
|
84
|
+
```
|
|
85
|
+
This handles everything: navigate to Dokumenter, find latest doc, detect PDF vs HTML, download or screenshot.
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
87
|
+
### Step 10 — Finalize
|
|
88
|
+
- Write test_results.txt with status, data used, and notes
|
|
89
|
+
- Close browser: `form-tester exec close`
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
## Issue logging
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
form-tester exec run-code "async page => { const link = page.locator('a[href*=\"/pdf/\"]').first(); const [download] = await Promise.all([ page.waitForEvent('download'), link.click() ]); await download.saveAs('$OUTPUT_DIR/document.pdf'); }"
|
|
97
|
-
```
|
|
98
|
-
Or if there's a "Last ned" button:
|
|
99
|
-
```
|
|
100
|
-
form-tester exec run-code "async page => { const [download] = await Promise.all([ page.waitForEvent('download'), page.getByRole('link', { name: 'Last ned' }).click() ]); await download.saveAs('$OUTPUT_DIR/document.pdf'); }"
|
|
93
|
+
When something unexpected happens, log it:
|
|
94
|
+
```bash
|
|
95
|
+
form-tester issue <category> "<description>"
|
|
101
96
|
```
|
|
97
|
+
Categories: `person-selection`, `navigation`, `form-fill`, `submission`, `documents`, `pdf-download`, `html-capture`, `screenshot`, `snapshot`, `validation`, `modal`, `timeout`, `other`
|
|
98
|
+
|
|
99
|
+
View recent issues: `form-tester issues`
|
|
100
|
+
|
|
101
|
+
## Important rules
|
|
102
|
+
|
|
103
|
+
- ALWAYS use `form-tester exec` instead of `playwright-cli` directly
|
|
104
|
+
- ALL screenshots MUST use `--full-page`
|
|
105
|
+
- Take a screenshot EVERY TIME the page changes
|
|
106
|
+
- Do NOT stop to ask the user between steps — execute the full flow
|
|
107
|
+
- Persona IDs: `ung-mann`, `gravid-kvinne`, `eldre-kvinne`, `kronisk-syk-mann`, `noen`
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
## Human mode
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
When user asks for `--human` mode:
|
|
112
|
+
1. Run `form-tester test <url> --human` to get persona list
|
|
113
|
+
2. Ask user to pick persona and scenario
|
|
114
|
+
3. Re-run: `form-tester test <url> --human --persona <id> --scenario "<text>"`
|
package/form-tester.js
CHANGED
|
@@ -7,7 +7,7 @@ const { spawn, execSync } = require("child_process");
|
|
|
7
7
|
const CONFIG_PATH = path.join(process.cwd(), "form-tester.config.json");
|
|
8
8
|
const OUTPUT_BASE = path.resolve(process.cwd(), "output");
|
|
9
9
|
const ISSUES_PATH = path.join(OUTPUT_BASE, "issues.jsonl");
|
|
10
|
-
const LOCAL_VERSION = "0.
|
|
10
|
+
const LOCAL_VERSION = "0.10.1";
|
|
11
11
|
const RECOMMENDED_PERSON = "Uromantisk Direktør";
|
|
12
12
|
|
|
13
13
|
// Recording — persisted to disk so `form-tester exec` can append across processes
|
|
@@ -257,6 +257,268 @@ async function handleDocuments(config, flags = {}) {
|
|
|
257
257
|
return 0;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// --- Standardized commands: cookies, select-person, validate ---
|
|
261
|
+
|
|
262
|
+
async function handleCookies() {
|
|
263
|
+
// Try known cookie banner selectors
|
|
264
|
+
const script = `() => {
|
|
265
|
+
const selectors = [
|
|
266
|
+
'[data-testid="reject-all-cookies"]',
|
|
267
|
+
'[data-testid="accept-all-cookies"]',
|
|
268
|
+
'button[id*="cookie" i]',
|
|
269
|
+
'button[class*="cookie" i]',
|
|
270
|
+
'[data-testid*="cookie" i]',
|
|
271
|
+
'button:has-text("Avvis alle")',
|
|
272
|
+
'button:has-text("Reject")',
|
|
273
|
+
'button:has-text("Aksepter")',
|
|
274
|
+
];
|
|
275
|
+
for (const sel of selectors) {
|
|
276
|
+
const el = document.querySelector(sel);
|
|
277
|
+
if (el && el.offsetParent !== null) {
|
|
278
|
+
el.click();
|
|
279
|
+
return { found: true, selector: sel, text: (el.textContent || "").trim().substring(0, 60) };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { found: false };
|
|
283
|
+
}`;
|
|
284
|
+
const result = await runPlaywrightCliCapture(["eval", script]);
|
|
285
|
+
const output = result.stdout.replace(/^### Result\s*/i, "").trim();
|
|
286
|
+
try {
|
|
287
|
+
const parsed = JSON.parse(output);
|
|
288
|
+
if (parsed.found) {
|
|
289
|
+
console.log(`Cookie banner dismissed: "${parsed.text}" (${parsed.selector})`);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
// parse failed, check raw output
|
|
294
|
+
}
|
|
295
|
+
console.log("No cookie banner found.");
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function handleSelectPerson(config, targetName) {
|
|
300
|
+
const v = "normal";
|
|
301
|
+
const log = (msg) => console.log(msg);
|
|
302
|
+
|
|
303
|
+
// Step 1: Try to find person list from current page snapshot
|
|
304
|
+
const tmpSnapshot = path.join(
|
|
305
|
+
config.lastRunDir || OUTPUT_BASE,
|
|
306
|
+
`_person_scan_${Date.now()}.yml`,
|
|
307
|
+
);
|
|
308
|
+
await runPlaywrightCli(["snapshot", "--filename", tmpSnapshot]);
|
|
309
|
+
|
|
310
|
+
let options = extractPersonsFromSnapshotFile(tmpSnapshot);
|
|
311
|
+
|
|
312
|
+
// Step 2: If no options from snapshot, try JS-based detection
|
|
313
|
+
if (!options.length) {
|
|
314
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
315
|
+
log(`Scanning for person options (attempt ${attempt + 1})...`);
|
|
316
|
+
await sleep(1500);
|
|
317
|
+
options = await fetchPersonOptions();
|
|
318
|
+
if (options.length) break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!options.length) {
|
|
323
|
+
console.error("No person options found on page.");
|
|
324
|
+
logIssue("person-selection", "No person options found by select-person command");
|
|
325
|
+
// Clean up temp file
|
|
326
|
+
try { fs.unlinkSync(tmpSnapshot); } catch (e) {}
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
options = prioritizeRecommended(options, RECOMMENDED_PERSON);
|
|
331
|
+
|
|
332
|
+
// Step 3: Determine which person to select
|
|
333
|
+
let chosen;
|
|
334
|
+
if (targetName) {
|
|
335
|
+
const target = targetName.toLowerCase();
|
|
336
|
+
chosen = options.find((o) => o.toLowerCase().includes(target));
|
|
337
|
+
if (!chosen) {
|
|
338
|
+
log(`Person "${targetName}" not found. Available: ${options.join(", ")}`);
|
|
339
|
+
chosen = options[0];
|
|
340
|
+
log(`Falling back to: ${chosen}`);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
chosen = options[0]; // recommended or first
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Step 4: Click the person button
|
|
347
|
+
log(`Selecting person: ${chosen}`);
|
|
348
|
+
const clickScript = `() => {
|
|
349
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"], [role="option"], a'));
|
|
350
|
+
const normalize = (s) => (s || "").replace(/\\s+/g, " ").trim();
|
|
351
|
+
const target = ${JSON.stringify(chosen)};
|
|
352
|
+
for (const btn of buttons) {
|
|
353
|
+
const text = normalize(btn.textContent);
|
|
354
|
+
if (text.includes(target) || text === target) {
|
|
355
|
+
btn.click();
|
|
356
|
+
return { clicked: true, text: text.substring(0, 80) };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { clicked: false, available: buttons.slice(0, 10).map(b => normalize(b.textContent).substring(0, 60)).filter(Boolean) };
|
|
360
|
+
}`;
|
|
361
|
+
const clickResult = await runPlaywrightCliCapture(["eval", clickScript]);
|
|
362
|
+
const clickOutput = clickResult.stdout.replace(/^### Result\s*/i, "").trim();
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(clickOutput);
|
|
365
|
+
if (parsed.clicked) {
|
|
366
|
+
log(`Person selected: ${parsed.text}`);
|
|
367
|
+
config.lastPerson = chosen;
|
|
368
|
+
saveConfig(config);
|
|
369
|
+
await sleep(1000);
|
|
370
|
+
// Take screenshot after selection
|
|
371
|
+
if (config.lastRunDir) {
|
|
372
|
+
await runPlaywrightCli(["screenshot", "--filename", path.join(config.lastRunDir, "person_selected.png"), "--full-page"]);
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
log("Could not click person button. Available buttons:");
|
|
376
|
+
(parsed.available || []).forEach((b) => log(` - ${b}`));
|
|
377
|
+
logIssue("person-selection", `Could not click "${chosen}". Buttons found but none matched.`);
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
log(`Person selection result: ${clickOutput}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Clean up temp file
|
|
384
|
+
try { fs.unlinkSync(tmpSnapshot); } catch (e) {}
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parseValidationErrors(snapshotText) {
|
|
389
|
+
const lines = snapshotText.split(/\r?\n/);
|
|
390
|
+
const errors = [];
|
|
391
|
+
// Look for the validation status block: status "Sjekk at følgende er riktig utfylt:"
|
|
392
|
+
let inValidation = false;
|
|
393
|
+
let validationIndent = null;
|
|
394
|
+
|
|
395
|
+
for (const line of lines) {
|
|
396
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
397
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
398
|
+
|
|
399
|
+
if (!inValidation) {
|
|
400
|
+
if (line.includes('status "Sjekk at følgende er riktig utfylt:"') || line.includes('status "Sjekk at f')) {
|
|
401
|
+
inValidation = true;
|
|
402
|
+
validationIndent = indent;
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Exit validation block when we hit same or lower indent that's not part of the list
|
|
408
|
+
if (indent <= validationIndent && !line.includes("list") && !line.includes("listitem") && !line.includes("link") && line.trim().startsWith("-")) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Parse error links: link "error text" [ref=eXXX] ... /url: "#fieldId"
|
|
413
|
+
const linkMatch = line.match(/link "([^"]+)" \[ref=(e\d+)\]/);
|
|
414
|
+
if (linkMatch) {
|
|
415
|
+
const errorText = linkMatch[1];
|
|
416
|
+
const ref = linkMatch[2];
|
|
417
|
+
// Look for the URL on the next line or in the same block
|
|
418
|
+
errors.push({ text: errorText, ref, fieldId: null });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Parse URL for the most recent error
|
|
422
|
+
const urlMatch = line.match(/\/url:\s*"#([^"]+)"/);
|
|
423
|
+
if (urlMatch && errors.length > 0 && !errors[errors.length - 1].fieldId) {
|
|
424
|
+
errors[errors.length - 1].fieldId = urlMatch[1];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return errors;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function handleValidate(config) {
|
|
432
|
+
const outputDir = config.lastRunDir;
|
|
433
|
+
if (!outputDir) {
|
|
434
|
+
console.error("No output folder. Run a test first.");
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
438
|
+
|
|
439
|
+
// Step 1: Take a fresh snapshot
|
|
440
|
+
const snapshotPath = path.join(outputDir, `validate_${Date.now()}.yml`);
|
|
441
|
+
await runPlaywrightCli(["snapshot", "--filename", snapshotPath]);
|
|
442
|
+
|
|
443
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
444
|
+
console.error("Failed to take snapshot.");
|
|
445
|
+
return 1;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const snapshotText = fs.readFileSync(snapshotPath, "utf8");
|
|
449
|
+
const errors = parseValidationErrors(snapshotText);
|
|
450
|
+
|
|
451
|
+
if (!errors.length) {
|
|
452
|
+
console.log("No validation errors found. Form is ready to submit.");
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(`\n${errors.length} validation error(s) found:\n`);
|
|
457
|
+
|
|
458
|
+
for (let i = 0; i < errors.length; i++) {
|
|
459
|
+
const err = errors[i];
|
|
460
|
+
const fieldId = err.fieldId ? decodeURIComponent(err.fieldId) : "unknown";
|
|
461
|
+
console.log(` ${i + 1}. [${fieldId}] "${err.text}" (ref=${err.ref})`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Step 2: Click each error link to scroll to it and take a snapshot
|
|
465
|
+
console.log("\nScrolling to each error and taking snapshots...\n");
|
|
466
|
+
|
|
467
|
+
for (let i = 0; i < errors.length; i++) {
|
|
468
|
+
const err = errors[i];
|
|
469
|
+
const fieldId = err.fieldId ? decodeURIComponent(err.fieldId) : null;
|
|
470
|
+
|
|
471
|
+
// Click the error link to scroll to the field
|
|
472
|
+
const clickCode = await runPlaywrightCli(["click", err.ref]);
|
|
473
|
+
if (clickCode !== 0) {
|
|
474
|
+
console.log(` ${i + 1}. Could not click error link ${err.ref}`);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await sleep(500);
|
|
479
|
+
|
|
480
|
+
// Take a snapshot focused on the field area
|
|
481
|
+
const fieldSnapshotPath = path.join(outputDir, `validation_error_${i + 1}.yml`);
|
|
482
|
+
await runPlaywrightCli(["snapshot", "--filename", fieldSnapshotPath]);
|
|
483
|
+
|
|
484
|
+
// Read the snapshot to find the field context
|
|
485
|
+
if (fs.existsSync(fieldSnapshotPath)) {
|
|
486
|
+
const fieldSnapshot = fs.readFileSync(fieldSnapshotPath, "utf8");
|
|
487
|
+
|
|
488
|
+
// Find the field in the snapshot by its ID
|
|
489
|
+
let fieldContext = "";
|
|
490
|
+
if (fieldId) {
|
|
491
|
+
const fieldLines = fieldSnapshot.split(/\r?\n/);
|
|
492
|
+
for (let j = 0; j < fieldLines.length; j++) {
|
|
493
|
+
// Look for elements with aria-invalid or the field ID
|
|
494
|
+
if (fieldLines[j].includes(`[aria-invalid`) || fieldLines[j].includes(fieldId.replace(/\./g, "%2E"))) {
|
|
495
|
+
// Grab surrounding context (5 lines before, 10 after)
|
|
496
|
+
const start = Math.max(0, j - 5);
|
|
497
|
+
const end = Math.min(fieldLines.length, j + 10);
|
|
498
|
+
fieldContext = fieldLines.slice(start, end).join("\n");
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (fieldContext) {
|
|
505
|
+
console.log(` ${i + 1}. [${fieldId}] "${err.text}"`);
|
|
506
|
+
console.log(` Field context from snapshot:`);
|
|
507
|
+
console.log(fieldContext.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
508
|
+
console.log("");
|
|
509
|
+
} else {
|
|
510
|
+
console.log(` ${i + 1}. [${fieldId}] "${err.text}" — scrolled to field, see ${fieldSnapshotPath}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Step 3: Scroll back to top
|
|
516
|
+
await runPlaywrightCliCapture(["eval", "window.scrollTo(0, 0)"]);
|
|
517
|
+
|
|
518
|
+
console.log(`\nFix these ${errors.length} field(s), then run 'form-tester validate' again before submitting.`);
|
|
519
|
+
return errors.length;
|
|
520
|
+
}
|
|
521
|
+
|
|
260
522
|
const PERSONAS = [
|
|
261
523
|
{
|
|
262
524
|
id: "ung-mann",
|
|
@@ -650,9 +912,12 @@ function printHelp() {
|
|
|
650
912
|
" form-tester test <url> --human Interactive test with prompts",
|
|
651
913
|
" form-tester exec <command> [args] Run playwright-cli command (recorded)",
|
|
652
914
|
" form-tester replay <recording.json> Replay a recorded test run",
|
|
653
|
-
" form-tester
|
|
654
|
-
" form-tester
|
|
655
|
-
" form-tester
|
|
915
|
+
" form-tester cookies Dismiss cookie banner",
|
|
916
|
+
" form-tester select-person [name] Select person (recommended or by name)",
|
|
917
|
+
" form-tester validate Parse validation errors, scroll to each field",
|
|
918
|
+
" form-tester documents Verify document in Dokumenter (auto PDF/HTML)",
|
|
919
|
+
" form-tester issue <category> <message> Log an issue for skill improvement",
|
|
920
|
+
" form-tester issues [limit] Show recent logged issues",
|
|
656
921
|
"",
|
|
657
922
|
"Interactive commands:",
|
|
658
923
|
" /test {url} Open form URL and start test",
|
|
@@ -1543,6 +1808,24 @@ async function main() {
|
|
|
1543
1808
|
process.exit(code);
|
|
1544
1809
|
}
|
|
1545
1810
|
|
|
1811
|
+
if (args[0] === "cookies") {
|
|
1812
|
+
const code = await handleCookies();
|
|
1813
|
+
process.exit(code);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
if (args[0] === "select-person") {
|
|
1817
|
+
const config = loadConfig();
|
|
1818
|
+
const targetName = args.slice(1).join(" ") || null;
|
|
1819
|
+
const code = await handleSelectPerson(config, targetName);
|
|
1820
|
+
process.exit(code);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (args[0] === "validate") {
|
|
1824
|
+
const config = loadConfig();
|
|
1825
|
+
const code = await handleValidate(config);
|
|
1826
|
+
process.exit(code);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1546
1829
|
if (args[0] === "test" && args.includes("--human")) {
|
|
1547
1830
|
const config = loadConfig();
|
|
1548
1831
|
const url = args.find((a) => a.startsWith("http"));
|
package/package.json
CHANGED