Files
RenewCMS/features/editor_insert_link.qa.md
T

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Long URL - the title attribute holds the full 90-character value, the dialog has no horizontal overflow, "Save link" is enabled.
  6. Leave without saving, Examples-driven - Cancel, Esc, outside click: nothing changes; focus returns.
  7. 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" gives target="_blank" rel="noopener noreferrer", off gives neither.
  8. 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.
  9. 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.
  10. 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.ts and 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=html returns the stored HTML as is, and safeHref runs 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 in title.
  • 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.md says "11 states"; screens/editor.md has 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.