Files
RenewCMS/features/media_upload_selection_duplicates.qa.md
T

8.2 KiB

QA: Media page, part 2 (upload, selection, duplicates)

Scope note

SPA only. No Go test and no test/ file; run the Go suite once (task test) to confirm nothing regressed. The server never returns 429 for POST /v1/media/ today (no rate limiter is mounted on the media router), so every 429 case is proven with a mock only.

Automated (Vitest + Vue Test Utils, task test:web)

Mount MediaPage in DashboardShell with a verified session and a test router at /media, as in the B1 spec. Mock useApi's request for GET/DELETE/merge and the list; replace XMLHttpRequest with a controllable stub (per instance: open, send, abort, upload.onprogress with loaded/total, status, responseText, getResponseHeader('Retry-After'), load, error, abort events). Stub crypto.subtle.digest to a known hex per file with vi.stubGlobal, and to undefined for the unavailable case. Use fake timers for the 150 ms skeleton, the 120 ms search debounce and the 1 s Retry-After tick. Sample data is the 12-image fixture of media_browse.feature. Files are built with new File([...], name, { type }) and a size override for over-limit sizes.

  1. Hashing lib (pure): digest of a known byte array gives the expected lowercase 64-char hex; crypto.subtle undefined gives null; nothing in the lib imports from the Media page.
  2. Browser checks: the six rows of the .feature, plus 10485760 bytes accepted and 10485761 rejected; size beats type; a rejected file triggers no hash call, no GET and no XHR; titles and text exact.
  3. Entry points: Upload (desktop header), the mobile pinned button and "Upload an image" each click the hidden input[type=file] (multiple, exact accept list).
  4. Drop overlay: dragenter with Files shows the copy exactly; dragleave hides; drop starts uploads; a drag without Files shows nothing; mobile pinned button hidden while shown.
  5. Queue: 6 valid files give at most 2 XHRs open at once; order kept; header "Uploading 6 images" and "N of 6 done" counts settled rows; "Uploading 1 image" singular; progress 1.1 of 2.6 MB reads "Uploading 1.1 of 2.6 MB" with a 42 % bar; 201 gives "Added to your library"; a silent list reload follows; files added while open are appended.
  6. Duplicate check: GET ?hash=...&page=1&size=100 precedes POST; a match gives the Duplicate row with exact text (oldest match named); slot released; "Use existing" drops the row and selects the id (bulk bar "1 selected", live region text), including the reset-to-All path; "Upload anyway" sends POST; crypto undefined and lookup failure skip straight to POST.
  7. Server statuses: 201, 413, 415, 400, 500, network error: row copy and icon exact; "Try again" re-sends only that file with no new GET ?hash=.
  8. Cancel: row x aborts that XHR and removes the row, the next file starts; "Cancel all" aborts all and removes waiting rows; panel closes when empty.
  9. Summary: Examples-driven summary lines; "Upload finished" / "{n} images" and "Dismiss"; "Nothing was uploaded" with "3 files were rejected" and with "2 files need attention", no summary; live-region text; Dismiss removes every row.
  10. 429: Retry-After: 42 gives the alert copy and "Try again in 0:42" (disabled, timer icon; mobile pinned button too); the 429'd file returns to Waiting; advancing 42 s re-enables, removes the alert and resumes the queue; Added rows kept; missing header gives 1:00; drops ignored while counting.
  11. Offline: Upload, "Upload an image", bulk "Delete" and "Merge into this one" disabled; drops ignored; Clear and selection still work; back online re-enables.
  12. Selection: checkbox visibility (hover, focus, any selection, always on mobile); tick and Space toggle; Enter opens the panel and ticking does not; live-region texts for select, unselect and "Selection cleared"; the checkbox is not a tab stop; select-all states (unchecked, partial, checked) in the grid row and the table header (no row in the mobile card list); Escape closes the panel first and clears the selection next; Clear; selection cleared on q/tab/sort change, kept on view change.
  13. Bulk bar: copy, buttons, desktop and mobile placement classes, replaces the mobile pinned button.
  14. Bulk delete: dialog copy exact (title, description with the summed count, rows "Used in 2", "Used in 3", "Unused"), singular form; no usage request; in progress (one DELETE per id, buttons disabled, not dismissible); done (cards leave, selection empty, focus target, toast id and text, silent reload); partly failed (two removed, failed one stays selected, bar "1 selected", toast with Retry and no auto-dismiss, Retry re-sends only the failed DELETE); several failures title; 404 counted as deleted; all failed gives the inline alert.
  15. Duplicates tab: enabled, URL and request (filter=duplicates, size=100, page 1, 2, ... until total is reached; amended by features/media_pagination_screen.feature), deep link, "Review duplicates" enabled and switching tab; sort and view toggle shown (sort sent with the request, toggle keeps the preference in the URL); grouping by sha256, group order, headers (including the SVG/unknown uploader case), column order, "Suggested" tag and footer, "Uploaded 14 Jun 2026 by Inès Duarte", "Used in 4 articles" opens the panel; suggested-copy Examples; empty state and "Back to all images"; badge absent at 0.
  16. Merge: dialog copy exact for floorboards.jpg and another kept copy, singular form; POST body { keep_id: 12, merge_ids: [5, 6] }; in progress not dismissible; 200 gives group removed, toast id "media-merged-12" and text, live-region text, silent reload, "No duplicates" when last; 500 and network give the alert; 409 gives the alert and a reload; 404 gives the info toast and closes.
  17. Global errors: 401 and 403 from XHR upload, merge, delete and hash lookup call the same redirect handler as useApi; no local copy.
  18. B1 regression: media_browse specs updated for the superseded statements and the rest still passes.

Visual (task visual:check -- media, desktop and mobile, light and dark)

Reference: docs/design/screens/media.md and source/MediaFrame.dc.html. The 20 B2 states lose skip: 'pending' and get a setup in web/visual/states.ts. None depends on the current date (dates are absolute), so none is skip: 'clock-sensitive'. Where a board frame contradicts the prose or fixture (bulk delete done / partly failed, "4 articles", tab badge at zero, "2 of 6 done", merge done with one group left), the user corrects the board; no exception is added.

Harness requirements:

  • Upload states use an init-script XHR stub that holds a request at 1.1 of 2.6 MB (42 %); files come from setInputFiles; the duplicate state mocks GET /v1/media/?hash= by the hash of the file's bytes.
  • The drag state dispatches a synthetic dragenter with dataTransfer.types = ['Files'].
  • The 429 state freezes timers with Playwright page.clock so the button reads "0:42".

Setups:

  • Selection and bulk bar (ids 1, 2, 8; partial select-all)
  • Bulk delete: confirm, in progress, partly failed, done
  • Upload: drag over the page, several files mixed, summary, rejected files (413, 400), too many requests (429)
  • Duplicates: groups, empty
  • Merge: confirm, in progress, failed, conflict (409), done

Manual checks screenshots cannot cover:

  • Keyboard and focus order: Space toggles selection, Enter opens the panel, Escape order (panel first, then selection); focus after a bulk delete; dialog focus starts on Cancel.
  • Drag and drop with real files from the OS, including a folder and a non-image drop.
  • Progress bar smoothness, hover and focus rings, reduced motion.
  • Screen reader announcements for selection, upload completion and merge.

Manual verification

  • Real upload round trip against a running server (task dev): progress on a large file, 413 at 10 MB + 1 byte, 415, 400 for a corrupt PNG, uploading the same bytes twice (duplicate row, "Upload anyway", Duplicates tab).
  • Real SHA-256 over a 10 MB file, and the skip path on an insecure origin (plain http on a LAN address).
  • Real merge: article bodies rewritten, counts after the refresh.
  • Real 429 cannot be checked: the media router has no limiter.
  • Real bulk delete of 20+ images with one deleted from another tab first (404 handling).