5.8 KiB
QA: Inserting, editing and removing a link in the article body
Automated (Vitest + Vue Test Utils, task test:web)
Mount EditorPage at /article/12/edit with the editor_core.qa.md fixture and the body
<p>Every bathroom is different. We also check the timber windows before any tiling starts.</p>.
Mock request for GET and PATCH /v1/article/12 only. Tiptap runs for real in jsdom; drive selection
through the editor instance. request is never called except by steps 8 and 9. Mock matchMedia for
pointer: coarse and for the viewport width.
- Button states, Examples-driven - default, tooltip text per platform (Windows and Linux "Insert link (Ctrl+K)", macOS "Insert link (⌘K)", none on touch), focus ring class, active for cursor-in and for partial or full selection, disabled for an image selection, and Ctrl/Cmd+K inert in Preview and with
disabled. - Create, Examples-driven (3 rows) - Ctrl+K and the button: title, prefill, focus target, toggle off; submit; body HTML holds the anchor; "Unsaved changes"; zero requests.
- Edit and remove, Examples-driven (3 rows) - Edit and Ctrl+K entry, prefilled fields incl. the toggle, change href in place, no-op leaves a Saved draft Saved, "Remove link" with no confirm and undo restoring href and target.
- Validation, Examples-driven - every row of both validation Outlines, plus the accepted-forms Outline: exact error text, dialog open, body and dirty state unchanged, focus target, and the trimmed href; the blur case. For the unsafe rows assert the body HTML never has "javascript:", "data:" or "vbscript:" in an href.
- Long URL - the
titleattribute holds the full 90-character value, the dialog has no horizontal overflow, "Save link" is enabled. - Leave without saving, Examples-driven - Cancel, Esc, outside click: nothing changes; focus returns.
- Dialog/sheet and popover/sheet, Examples-driven - width mock gives dialog or sheet; fine pointer gives popover (Esc closes), coarse gives sheet with three 48 px rows; Open calls
window.open(url, '_blank', 'noopener,noreferrer'); Remove has no confirm and is undoable; "Open in new tab" givestarget="_blank" rel="noopener noreferrer", off gives neither. - Round trip - save with a new-tab link: the PATCH body has the anchor with target and rel; remount with it: link and toggle restored; without the toggle no target or rel.
- Unsafe hrefs, Examples-driven - on load: text kept, no link, button not active, draft not dirty, zero requests; then an edit and Save: the PATCH body has no unsafe href. Paste of the same HTML: same neutralising, draft dirty.
- Selection edge cases, Examples-driven (3 rows) - the new link covers the range, a partial link is replaced, no throw, and the text content before and after is identical. Then the French completeness check: every new key is in
messages.tsand the dialog title and two error strings are spot-checked.
Follow-ups and notes (not asserted here)
- Follow-up Go slice (out of scope):
?format=htmlreturns the stored HTML as is, andsafeHrefruns only for markdown and json. An API client that PATCHes<a href="javascript:...">gets it back in the html read. A separate Go slice should sanitise href schemes in the converter and probably reject them on write. The SPA sanitising here is defence in depth only. - Board-vs-reality note for the next Claude Design pass: row 16 says the URL field "truncates in the middle of the line", and the board draws an ellipsis in the text. CSS cannot truncate the middle of an
<input>. The implementation is a single scrolling line with the full URL intitle. - Design gap for the next pass: the helper text "Links open in the same tab unless you turn on a new tab." is in the board only.
components.md("LinkModal") needs it. - README undercount:
docs/design/README.mdsays "11 states";screens/editor.mdhas 17 link rows (11-27). All 17 are covered.
Visual: task visual:check -- editor
Setups go in web/visual/states.ts with the board's sample data: article "How we price a bathroom renovation", SaveStatus "Unsaved changes", link text "timber windows", URL "/articles/winter-checklist" (rows 13, 20, 22-26), "www.example" (14), "#measuring" (15), and for row 16 the text "planning guidance" with the 90-character URL above. Hover, focus and selection (rows 18, 23-26) are forced pseudo-states in the setup. If a row cannot be matched within 10 %, stop and ask the user: no per-state exception without approvedBy. No row is clock-sensitive.
| Row | State |
|---|---|
| 11 | Link: create from selection |
| 12 | Link: create with no selection |
| 13 | Link: edit existing (toggle on, Remove link) |
| 14 | Link: invalid URL |
| 15 | Link: empty text |
| 16 | Link: long URL |
| 17 | Link button: default |
| 18 | Link button: hover and tooltip (desktop only) |
| 19 | Link button: focus-visible |
| 20 | Link button: active |
| 21 | Link button: disabled (image selected) |
| 22 | Link popover (desktop) and sheet (mobile) |
| 23-26 | Rendered link: default, hover, focus, selected |
| 27 | Toolbar overflow (390 px, scrolled to the end; desktop unchanged) |
- Keyboard and focus: Tab order, Enter/Space on the button, focus trap and return, Esc on the popover, Enter on a focused link.
- Hover and motion: tooltip delay, underline thickening, sheet slide.
- Screen reader: aria-pressed, dialog labelling, error association.
Manual verification
- Ctrl/Cmd+K in Chrome, Firefox, Safari and Edge: the browser shortcut is prevented only inside the focused editor.
- macOS tooltip on a real Mac.
- Real touch on a phone: a tap on a link opens the sheet and does not follow the link; the sideways toolbar scroll and fades work, including iOS momentum.
- IME in the Link text field.
- Save and public read against
task dev: save a new-tab link, then GET ?format=html, ?format=json and ?format=markdown, and open the public page.