feat: upload attachments to test cases within a test run - #461
feat: upload attachments to test cases within a test run#461kk-marriott-blc wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed frontend correctness/performance issues (stale state updates in RunCaseAttachments and unreleased object URLs on download) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds first-class support for uploading and managing attachments (e.g., screenshots) on a test case within a specific test run, by introducing a new backend join table + routes and a new frontend “Attachments” tab that reuses a shared uploader component.
Changes:
- Backend: adds
runCaseAttachmentsjoin table +/runcaseattachmentsGET/POST/DELETE routes with run-case-to-project permission middleware. - Backend: factors shared multer config into
backend/config/upload.js(enforces 50MB limit server-side; fixes same-request filename collisions). - Frontend: introduces run-case attachments UI tab and shares
AttachmentsEditor+isImageacross case and run-case views, with new i18n message group.
File summaries
| File | Description |
|---|---|
| frontend/utils/runCaseAttachmentControl.ts | Adds API helpers for listing/creating/deleting run-case attachments. |
| frontend/utils/runCaseAttachmentControl.test.ts | Unit tests for the new run-case attachment API helpers. |
| frontend/utils/isImage.ts | Shared image-type detection helper for attachment rendering. |
| frontend/utils/attachmentDownload.ts | Centralized attachment download helper used by both views. |
| frontend/types/run.ts | Adds attachments label for run case detail tabs. |
| frontend/types/case.ts | Extends attachment typing to support run-case join and shared editor labels. |
| frontend/types/attachment.ts | Introduces shared message types for the reusable attachments editor. |
| frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/RunCaseAttachments.tsx | New run-case attachments tab content and interaction logic. |
| frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/page.tsx | Wires new “Attachments” messages into the run-case detail pane. |
| frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/DetailPane.tsx | Adds the new Attachments tab and deep-linking via ?tab=attachments. |
| frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/page.tsx | Adds close label needed by the shared AttachmentsEditor. |
| frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseEditor.tsx | Switches case attachment UI to the shared AttachmentsEditor. |
| frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachmentControl.ts | Removes duplicated download helper in favor of shared download util. |
| frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachmentControl.test.ts | Updates tests to use the shared isImage helper and normalized join typing. |
| frontend/messages/zh-CN.json | Adds new strings for Attachments tab and shared editor actions. |
| frontend/messages/pt-BR.json | Adds new strings for Attachments tab and shared editor actions. |
| frontend/messages/ja.json | Adds new strings for Attachments tab and shared editor actions. |
| frontend/messages/en.json | Adds new strings for Attachments tab and shared editor actions. |
| frontend/messages/de.json | Adds new strings for Attachments tab and shared editor actions. |
| frontend/components/AttachmentsEditor.tsx | Refactors uploader into a shared component; adds image preview modal. |
| docs/docs/dev/er.md | Updates ER diagram to include runCases, runCaseAttachments, and attachments.filename. |
| backend/server.ts | Registers the new /runcaseattachments routes. |
| backend/routes/runcaseattachments/new.test.js | Tests run-case attachment upload behavior, permission gating, and rollback cleanup. |
| backend/routes/runcaseattachments/new.js | Implements POST upload/linking route for run-case attachments. |
| backend/routes/runcaseattachments/index.test.js | Tests listing run-case attachments with visibility constraints. |
| backend/routes/runcaseattachments/index.js | Implements GET list route for run-case attachments. |
| backend/routes/runcaseattachments/delete.test.js | Tests scoped deletion and transactional behavior. |
| backend/routes/runcaseattachments/delete.js | Implements scoped DELETE route for run-case attachments. |
| backend/routes/attachments/new.js | Switches existing case-attachment upload route to shared upload middleware. |
| backend/models/runCases.js | Adds association between RunCase and Attachment via runCaseAttachments. |
| backend/models/runCaseAttachments.js | Adds new RunCaseAttachment model definition. |
| backend/models/attachments.js | Adds association between Attachment and RunCase via runCaseAttachments. |
| backend/migrations/20260824000000-create-run-case-attachments.js | Migration creating runCaseAttachments + unique index. |
| backend/middleware/verifyVisible.js | Adds verifyProjectVisibleFromRunCaseId helper. |
| backend/middleware/verifyEditable.js | Adds verifyProjectReporterFromRunCaseId helper. |
| backend/config/upload.test.js | Tests shared upload constraints + collision handling. |
| backend/config/upload.js | Introduces shared multer config with enforced size/count limits + collision fix. |
Review details
Suppressed comments (2)
frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/RunCaseAttachments.tsx:95
- This delete handler filters using the
attachmentsvalue from the render that created the callback; if attachments change between scheduling and completion (e.g. concurrent upload/delete), the state update can resurrect removed items. Use a functional update to ensure it applies to the latest list.
setAttachments(attachments.filter((attachment) => attachment.id !== attachmentId));
frontend/components/AttachmentsEditor.tsx:59
- Using the array index as the React
keycan cause incorrect UI updates when attachments are inserted/removed/reordered (e.g. preview state or DOM reuse mismatching items). Prefer a stable identifier such asimage.id.
- Files reviewed: 36/37 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!newAttachments) { | ||
| throw new Error('Failed to upload attachments'); | ||
| } | ||
| setAttachments([...attachments, ...newAttachments]); |
| const blob = await response.blob(); | ||
| const downloadUrl = window.URL.createObjectURL(blob); | ||
|
|
||
| const link = document.createElement('a'); | ||
| link.href = downloadUrl; | ||
| link.download = downloadFileName; | ||
| document.body.appendChild(link); | ||
| link.click(); | ||
| document.body.removeChild(link); |
|
Thanks for the PR. The addition of test code and the integration into the existing Attachments look perfect. I’d appreciate it if you could also take a look at the review comments from GitHub Copilot. |
thanks for pointing out visibility problem. i created a issue. |
Adds the ability to attach files — typically failure screenshots — to a test case within a test run. Attachments were previously only possible on the test case itself, so there was no way to record evidence against a specific run.
Backend
New
runCaseAttachmentsjoin table (migration20260824000000), mirroring the existingcaseAttachmentsstructure with a unique index on(runCaseId, attachmentId)and cascade deletes from both parents. Theattachmentstable is unchanged — it was already parent-agnostic.Three endpoints under
/runcaseattachments:GET /?runCaseId=— list, requires project visibilityPOST /?runCaseId=— upload, requires reporterDELETE /:attachmentId?runCaseId=— delete, requires reporter, and refuses attachments that aren't linked to that run caseReporter is the minimum role, matching what commenting on a run case already requires. Two middleware helpers do the run case → run → project lookup:
verifyProjectReporterFromRunCaseIdandverifyProjectVisibleFromRunCaseId. Downloads reuse the existingGET /attachments/download/:attachmentId, which is already parent-agnostic.Frontend
A new "Attachments" tab in the run's test case detail pane, alongside Comments and History, deep-linkable with
?tab=attachments. Clicking a thumbnail opens a full-screen preview with a download button.CaseAttachmentsEditorandisImagemove tofrontend/components/AttachmentsEditor.tsxandfrontend/utils/isImage.tsso the test case and run views share one uploader.Changes to existing test case attachments
The multer setup moves out of
routes/attachments/new.jsintobackend/config/upload.jsso both upload routes share it. Two consequences for the existing test case upload path:fs.existsSynccheck saw nothing and both files claimed the same name. Names claimed within a request are now tracked, so they are suffixed as intended.Docs
docs/docs/dev/er.mdgainsrunCaseAttachments. I corrected two pre-existing gaps while there:runCaseswas missing from the diagram entirely, andattachments.pathwas renamed tofilenameby migration20250712000000.Tests
25 new Vitest tests — 14 across the three routes (permissions, run case scoping on delete, transaction rollback with cleanup of files already written to disk), 5 for the shared upload config (including the collision fix and the size limit), and 6 for the frontend control.
Note
backend/publicis served statically, so uploaded files stay readable by URL without a token. That's pre-existing behaviour for test case attachments and unchanged here, but it does mean run attachments aren't private. Happy to address that separately if you'd like it in scope.