import React from 'react'; import '@testing-library/jest-dom'; import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { api } from './api'; import { I18nProvider } from './i18n/I18nProvider'; import { translations, type UiLanguage } from './i18n/translations'; import DocumentsPage from './views/DocumentsPage'; const mockedApi = api as jest.Mocked; const defaults = { page: 1, pageSize: 25, q: undefined, type: 'all', state: 'all', visibility: 'all', sort: 'date-desc' }; function copy(key: string, params: Record = {}, language: UiLanguage = document.documentElement.lang === 'nb-NO' ? 'nb' : 'en') { return ((translations[language] as Record)[key] ?? key).replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`)); } function item(overrides: Record = {}) { return { id: 'cv:12', resourceId: '12', type: 'cv', name: 'Metadata CV ÆØÅ', state: 'current', visibility: 'public', jobApplicationId: 42, jobTitle: 'Recorded role', companyName: 'Nordlys AS', jobIsDeleted: false, recordedAt: '2026-02-13T11:30:00.0000000Z', dateKind: 'updated-utc', version: 4, historyCount: null, attachmentCount: null, cvVariantId: null, cvVariantName: null, cvVariantVersion: null, fileType: null, fileSize: null, purpose: null, useForAi: null, provider: null, sourceHref: '/career/builder/12', downloadHref: null, publicHref: '/cv/metadata-cv', hasCurrentContent: null, ...overrides }; } function page(items: ReturnType[] = [item()], currentPage = 1, total = items.length) { return { items, page: currentPage, pageSize: 25, total, totalPages: Math.ceil(total / 25) }; } function deferred() { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } function Probe() { const location = useLocation(); const navigate = useNavigate(); return <>{JSON.stringify({ pathname: location.pathname, search: location.search, hash: location.hash, state: location.state })}; } function renderPage(path = '/documents', state: unknown = null) { const url = new URL(path, 'https://local.test'); return render(} />Other workflow

} />
); } function location() { return JSON.parse(screen.getByTestId('documents-location').textContent!) as { pathname: string; search: string; hash: string; state: unknown }; } function reads() { return mockedApi.get.mock.calls.filter(([url]) => url === '/documents'); } function row(name: string) { return within(screen.getByText(name, { exact: true }).closest('tr, article') as HTMLElement); } async function select(labelKey: string, optionKey: string) { fireEvent.mouseDown(screen.getByRole('combobox', { name: copy(labelKey) })); fireEvent.click(await screen.findByRole('option', { name: copy(optionKey) })); } async function language(value: UiLanguage) { await act(async () => { window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: value })); }); } beforeEach(() => { localStorage.clear(); document.documentElement.lang = 'en'; jest.clearAllMocks(); mockedApi.get.mockReset().mockImplementation((url: string) => url === '/documents' ? Promise.resolve({ data: page() } as never) : Promise.reject(new Error(`Unexpected read ${url}`))); for (const method of [mockedApi.post, mockedApi.put, mockedApi.patch, mockedApi.delete]) method.mockReset().mockRejectedValue(new Error('Unexpected document-index write')); Object.defineProperty(URL, 'createObjectURL', { configurable: true, writable: true, value: jest.fn(() => 'blob:uploaded-file') }); Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, writable: true, value: jest.fn() }); }); afterEach(() => { expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.put).not.toHaveBeenCalled(); expect(mockedApi.patch).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled(); jest.restoreAllMocks(); }); test('[P7-07 documents] validates URL filters and reads one metadata page with literal Nordic search', async () => { renderPage('/documents?q=100%25_%21+%C3%86%C3%98%C3%85&page=bad&type=unknown&state=unknown&visibility=unknown&sort=unknown&jobApplicationId=-2&keep=1#context'); expect(await screen.findByText('Metadata CV ÆØÅ', { exact: true })).toBeVisible(); expect(reads()).toHaveLength(1); expect(reads()[0][1]?.params).toEqual({ ...defaults, q: '100%_! ÆØÅ' }); expect(screen.getByRole('textbox', { name: copy('documentsSearch') })).toHaveValue('100%_! ÆØÅ'); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents']); expect(URL.createObjectURL).not.toHaveBeenCalled(); }); test('[P7-07 documents] server pagination and filter history preserve unknown query, hash and router state', async () => { mockedApi.get.mockImplementation((_url: string, config?: any) => Promise.resolve({ data: page([item({ name: `Page ${config.params.page} ${config.params.type}` })], config.params.page, 26) } as never)); const state = { from: '/jobs/42', draftContext: 'preserve-navigation-context' }; renderPage('/documents?page=2&jobApplicationId=42&keep=%C3%A6%26x#materials', state); expect(await screen.findByText('Page 2 all')).toBeVisible(); expect(reads()[0][1]?.params).toEqual({ ...defaults, page: 2, jobApplicationId: 42 }); await select('documentsType', 'documentsTypeCv'); expect(await screen.findByText('Page 1 cv')).toBeVisible(); const current = location(); const query = new URLSearchParams(current.search); expect(query.get('keep')).toBe('æ&x'); expect(query.get('jobApplicationId')).toBe('42'); expect(query.get('page')).not.toBe('2'); expect(current.hash).toBe('#materials'); expect(current.state).toEqual(state); expect(reads().at(-1)?.[1]?.params).toEqual({ ...defaults, type: 'cv', jobApplicationId: 42 }); fireEvent.click(screen.getByRole('button', { name: 'History Back' })); expect(await screen.findByText('Page 2 all')).toBeVisible(); expect(reads().at(-1)?.[1]?.params).toEqual({ ...defaults, page: 2, jobApplicationId: 42 }); }); test('[P7-07 documents] literal search all remains free text and preserves unrelated navigation context', async () => { mockedApi.get.mockImplementation((_url: string, config?: any) => Promise.resolve({ data: page([item({ name: config.params.q === 'all' ? 'Literal all metadata' : 'Initial metadata' })]) } as never)); const state = { from: '/jobs/42', draftContext: 'preserve-literal-search-context' }; renderPage('/documents?keep=%C3%A6%26x#materials', state); await screen.findByText('Initial metadata'); const search = screen.getByRole('textbox', { name: copy('documentsSearch') }); fireEvent.change(search, { target: { value: 'all' } }); expect(search).toHaveValue('all'); expect(await screen.findByText('Literal all metadata')).toBeVisible(); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents', '/documents']); expect(reads()[1][1]?.params).toEqual({ ...defaults, q: 'all' }); const current = location(); const query = new URLSearchParams(current.search); expect(query.get('q')).toBe('all'); expect(query.get('keep')).toBe('æ&x'); expect(current.hash).toBe('#materials'); expect(current.state).toEqual(state); }); test('[P7-07 documents] a batched sort selection and immediate search preserve both filters and navigation context', async () => { mockedApi.get.mockImplementation((_url: string, config?: any) => Promise.resolve({ data: page([item({ name: config.params.q === 'Indexed' && config.params.sort === 'name-asc' ? 'Indexed sorted metadata' : 'Initial metadata' })]) } as never)); const state = { from: '/jobs/42', draftContext: 'preserve-rapid-filter-context' }; renderPage('/documents?keep=%C3%A6%26x#context', state); await screen.findByText('Initial metadata'); const search = screen.getByRole('textbox', { name: copy('documentsSearch') }); fireEvent.mouseDown(screen.getByRole('combobox', { name: copy('documentsSort') })); const option = await screen.findByRole('option', { name: copy('documentsSortNameAsc') }); act(() => { fireEvent.click(option); fireEvent.change(search, { target: { value: 'Indexed' } }); }); const current = location(); const query = new URLSearchParams(current.search); expect(query.get('sort')).toBe('name-asc'); expect(query.get('q')).toBe('Indexed'); expect(query.get('keep')).toBe('æ&x'); expect(current.hash).toBe('#context'); expect(current.state).toEqual(state); expect(await screen.findByText('Indexed sorted metadata')).toBeVisible(); expect(reads().at(-1)?.[1]?.params).toEqual({ ...defaults, q: 'Indexed', sort: 'name-asc' }); expect(mockedApi.get.mock.calls.every(([url]) => url === '/documents')).toBe(true); fireEvent.click(screen.getByRole('button', { name: 'History Back' })); await waitFor(() => { const restored = new URLSearchParams(location().search); expect(restored.get('q')).toBeNull(); expect(restored.get('sort')).toBeNull(); expect(reads().at(-1)?.[1]?.params).toEqual(defaults); }); expect(await screen.findByText('Initial metadata')).toBeVisible(); fireEvent.change(screen.getByRole('textbox', { name: copy('documentsSearch') }), { target: { value: 'After back' } }); await waitFor(() => expect(reads().at(-1)?.[1]?.params).toEqual({ ...defaults, q: 'After back' })); const afterBack = location(); const afterBackQuery = new URLSearchParams(afterBack.search); expect(afterBackQuery.get('q')).toBe('After back'); expect(afterBackQuery.get('sort')).toBeNull(); expect(afterBackQuery.get('keep')).toBe('æ&x'); expect(afterBack.hash).toBe('#context'); expect(afterBack.state).toEqual(state); expect(screen.getByRole('textbox', { name: copy('documentsSearch') })).toHaveValue('After back'); }); test('[P7-07 documents] incompatible pending and failed queries hide old actions and late results cannot replace current metadata', async () => { const old = deferred(); mockedApi.get.mockImplementation((_url: string, config?: any) => config.params.q === 'old' ? old.promise : config.params.q === 'failed' ? Promise.reject(new Error('offline')) : Promise.resolve({ data: page([item({ name: config.params.q === 'new' ? 'New owned metadata' : 'Initial metadata' })]) }) as never); renderPage(); await screen.findByText('Initial metadata'); const search = screen.getByRole('textbox', { name: copy('documentsSearch') }); fireEvent.change(search, { target: { value: 'old' } }); await waitFor(() => expect((reads().at(-1)?.[1]?.params as Record | undefined)?.q).toBe('old')); expect(screen.queryByText('Initial metadata')).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: copy('documentsOpenSource') })).not.toBeInTheDocument(); fireEvent.change(search, { target: { value: 'new' } }); expect(await screen.findByText('New owned metadata')).toBeVisible(); await act(async () => old.resolve({ data: page([item({ name: 'Obsolete private metadata' })]) })); expect(screen.queryByText('Obsolete private metadata')).not.toBeInTheDocument(); fireEvent.change(search, { target: { value: 'failed' } }); expect(await screen.findByRole('alert')).toHaveTextContent(copy('documentsLoadFailed')); expect(screen.queryByText('New owned metadata')).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: copy('documentsOpenSource') })).not.toBeInTheDocument(); }); test('[P7-07 documents] Retry is one guarded GET and a completion after leaving cannot publish or start follow-up work', async () => { const pending = deferred(); mockedApi.get.mockRejectedValueOnce(new Error('offline')).mockImplementation(() => pending.promise); renderPage('/documents?q=owned&type=attachment'); expect(await screen.findByRole('alert')).toHaveTextContent(copy('documentsLoadFailed')); const retry = screen.getByRole('button', { name: copy('retry') }); act(() => { fireEvent.click(retry); fireEvent.click(retry); }); await waitFor(() => expect(reads()).toHaveLength(2)); expect(reads()[1][1]?.params).toEqual({ ...defaults, q: 'owned', type: 'attachment' }); fireEvent.click(screen.getByRole('button', { name: 'Leave documents' })); await screen.findByText('Other workflow'); await act(async () => pending.resolve({ data: page([item({ name: 'Late document result' })]) })); expect(screen.queryByText('Late document result')).not.toBeInTheDocument(); expect(reads()).toHaveLength(2); expect(URL.createObjectURL).not.toHaveBeenCalled(); }); test('[P7-07 documents] a true empty index, filtered no-match and request failure remain distinct', async () => { mockedApi.get.mockImplementation((_url: string, config?: any) => config.params.q === 'offline' ? Promise.reject(new Error('offline')) : Promise.resolve({ data: page([]) }) as never); renderPage(); expect(await screen.findByText(copy('documentsEmpty'))).toBeVisible(); const search = screen.getByRole('textbox', { name: copy('documentsSearch') }); fireEvent.change(search, { target: { value: 'no match æøå' } }); expect(await screen.findByText(copy('documentsNoMatches'))).toBeVisible(); expect(screen.queryByText(copy('documentsEmpty'))).not.toBeInTheDocument(); fireEvent.change(search, { target: { value: 'offline' } }); expect(await screen.findByRole('alert')).toHaveTextContent(copy('documentsLoadFailed')); expect(screen.queryByText(copy('documentsEmpty'))).not.toBeInTheDocument(); expect(screen.queryByText(copy('documentsNoMatches'))).not.toBeInTheDocument(); }); test('[P7-07 documents] English and Bokmål presentation changes preserve query context without a metadata refetch', async () => { renderPage('/documents?q=%C3%A6%C3%B8%C3%A5&state=current&visibility=public&keep=one#context', { draft: 'unsaved-source-context' }); await screen.findByText('Metadata CV ÆØÅ'); const before = location(); const count = reads().length; expect(screen.getByRole('textbox', { name: copy('documentsSearch') })).toHaveValue('æøå'); await language('nb'); expect(screen.getByRole('textbox', { name: copy('documentsSearch', {}, 'nb') })).toHaveValue('æøå'); expect(row('Metadata CV ÆØÅ').getByText(copy('documentsCurrent', {}, 'nb'))).toBeVisible(); expect(row('Metadata CV ÆØÅ').getByRole('link', { name: copy('documentsOpenSource', {}, 'nb') })).toHaveAttribute('href', '/career/builder/12'); await language('en'); expect(screen.getByRole('textbox', { name: copy('documentsSearch', {}, 'en') })).toHaveValue('æøå'); expect(location()).toEqual(before); expect(reads()).toHaveLength(count); }); test('[P7-07 documents] an out-of-range page offers explicit first-page recovery without claiming an empty index', async () => { mockedApi.get.mockImplementation((_url: string, config?: any) => Promise.resolve({ data: config.params.page === 999 ? page([], 999, 1) : page([item()]) } as never)); const state = { from: '/jobs/42', draftContext: 'preserve-out-of-range-context' }; renderPage('/documents?page=999&keep=%C3%A6%26x#materials', state); expect(await screen.findByText(copy('documentsPageEmpty'))).toBeVisible(); expect(screen.getByText(copy('documentsPageEmptyHelp'))).toBeVisible(); expect(screen.queryByText(copy('documentsEmpty'))).not.toBeInTheDocument(); expect(screen.queryByText(copy('documentsNoMatches'))).not.toBeInTheDocument(); expect(reads()).toHaveLength(1); expect(reads()[0][1]?.params).toEqual({ ...defaults, page: 999 }); fireEvent.click(screen.getByRole('button', { name: copy('documentsFirstPage') })); expect(await screen.findByText('Metadata CV ÆØÅ')).toBeVisible(); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents', '/documents']); expect(reads()[1][1]?.params).toEqual(defaults); const current = location(); const query = new URLSearchParams(current.search); expect(query.get('page')).not.toBe('999'); expect(query.get('keep')).toBe('æ&x'); expect(current.hash).toBe('#materials'); expect(current.state).toEqual(state); }); test('[P7-07 documents] seven resource kinds retain exact source actions, frozen provenance and honest date labels', async () => { const letter = item({ id: 'cover-letter:42', resourceId: '42', type: 'cover-letter', name: 'Versions-only letter', state: 'draft', visibility: null, dateKind: 'history-recorded-utc', version: 3, historyCount: 3, sourceHref: '/jobs/42?section=application&sub=letter', publicHref: null, hasCurrentContent: false }); const material = item({ id: 'application-material:42', resourceId: '42', type: 'application-material', name: 'Application answers', state: 'draft', visibility: null, recordedAt: null, dateKind: 'unknown', version: null, sourceHref: '/jobs/42?section=application&sub=answers', publicHref: null }); const tailored = item({ id: 'tailored-cv:42', resourceId: '42', type: 'tailored-cv', name: 'Tailored CV text', state: 'draft', visibility: null, recordedAt: null, dateKind: 'unknown', version: null, sourceHref: '/jobs/42?section=application&sub=cv', publicHref: null }); const upload = item({ id: 'attachment:9', resourceId: '9', type: 'attachment', name: 'Certificate æøå.pdf', visibility: null, recordedAt: '2026-02-12T09:15:00.0000000', dateKind: 'uploaded-local', version: null, fileType: 'application/pdf', fileSize: 1234, purpose: 'Certificate', useForAi: false, sourceHref: '/jobs/42?section=documents', downloadHref: '/api/attachments/download/9', publicHref: null }); const frozen = item({ id: 'submitted-package:86', resourceId: '86', type: 'submitted-package', name: 'Captured package', state: 'frozen', visibility: null, dateKind: 'captured-utc', version: 9, attachmentCount: 2, cvVariantId: 3, cvVariantName: 'Captured CV', cvVariantVersion: 2, jobIsDeleted: true, sourceHref: '/jobs/42?section=application&sub=submission', publicHref: null }); const email = item({ id: 'email-draft:11111111-1111-4111-8111-111111111111', resourceId: '11111111-1111-4111-8111-111111111111', type: 'email-draft', name: 'Saved email draft', state: 'draft', visibility: null, provider: 'gmail', version: 6, sourceHref: '/correspondence', publicHref: null }); mockedApi.get.mockResolvedValue({ data: page([item(), letter, material, tailored, upload, frozen, email]) } as never); renderPage(); await screen.findByText('Metadata CV ÆØÅ'); for (const entry of [item(), letter, material, tailored, upload]) expect(row(entry.name).getByRole('link', { name: copy('documentsOpenSource') })).toHaveAttribute('href', entry.sourceHref); expect(row('Metadata CV ÆØÅ').getByRole('link', { name: copy('documentsOpenPublicCv') })).toHaveAttribute('href', '/cv/metadata-cv'); expect(row(letter.name).getByText(copy('documentsHistoryOnly'))).toBeVisible(); expect(row(letter.name).getByText(copy('documentsHistoryVersions', { count: 3 }))).toBeVisible(); expect(row(upload.name).getByText(copy('documentsUploadedLocal'), { exact: false })).toBeVisible(); expect(row(material.name).getByText(copy('documentsDateUnknown'))).toBeVisible(); expect(row(tailored.name).getByText(copy('documentsDateUnknown'))).toBeVisible(); expect(row(frozen.name).getByText(copy('documentsFrozen'))).toBeVisible(); expect(row(frozen.name).getByText(copy('documentsCapturedCv', { name: 'Captured CV', version: 2 }))).toBeVisible(); expect(row(frozen.name).getByText(copy('documentsDeletedJob'))).toBeVisible(); expect(row(frozen.name).getByRole('link', { name: copy('documentsOpenSubmissionHistory') })).toHaveAttribute('href', frozen.sourceHref); expect(row(email.name).getByRole('link', { name: copy('documentsOpenCorrespondence') })).toHaveAttribute('href', '/correspondence'); expect(row(email.name).queryByRole('button', { name: /resume|send/i })).not.toBeInTheDocument(); expect(screen.getByText(copy('documentsDateOrderHelp'))).toBeVisible(); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents']); expect(URL.createObjectURL).not.toHaveBeenCalled(); }); test('[P7-07 documents] attachment download is explicit, API-relative and single-flight with original MIME, filename and URL disposal', async () => { const upload = item({ id: 'attachment:9', resourceId: '9', type: 'attachment', name: 'Certificate æøå.pdf', visibility: null, fileType: 'application/pdf', sourceHref: '/jobs/42?section=documents', downloadHref: '/api/attachments/download/9', publicHref: null }); const pending = deferred(); const bytes = new Blob(['%PDF uploaded bytes'], { type: 'application/pdf' }); const clicks: Array<{ href: string; filename: string }> = []; jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) { clicks.push({ href: this.href, filename: this.download }); }); mockedApi.get.mockImplementation((url: string) => url === '/documents' ? Promise.resolve({ data: page([upload]) }) : url === '/attachments/download/9' ? pending.promise : Promise.reject(new Error(`Unexpected read ${url}`)) as never); renderPage(); await screen.findByText(upload.name); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents']); const download = row(upload.name).getByRole('button', { name: copy('documentsDownloadAttachment') }); act(() => { fireEvent.click(download); fireEvent.click(download); }); expect(mockedApi.get.mock.calls.filter(([url]) => url === '/attachments/download/9')).toHaveLength(1); expect(mockedApi.get).toHaveBeenLastCalledWith('/attachments/download/9', expect.objectContaining({ responseType: 'blob' })); await act(async () => pending.resolve({ data: bytes, headers: { 'content-type': 'application/pdf' } })); await waitFor(() => expect(clicks).toEqual([{ href: 'blob:uploaded-file', filename: upload.name }])); const downloaded = (URL.createObjectURL as jest.Mock).mock.calls[0][0] as Blob; expect(downloaded.type).toBe('application/pdf'); expect(downloaded.size).toBe(bytes.size); expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:uploaded-file'); expect(reads()).toHaveLength(1); await waitFor(() => expect(download).toHaveFocus()); }); test('[P7-07 documents] a completed download preserves deliberate Search focus instead of restoring the old button', async () => { const upload = item({ id: 'attachment:9', resourceId: '9', type: 'attachment', name: 'Focus-safe uploaded file.pdf', visibility: null, fileType: 'application/pdf', sourceHref: '/jobs/42?section=documents', downloadHref: '/api/attachments/download/9', publicHref: null }); const pending = deferred(); const bytes = new Blob(['%PDF uploaded bytes'], { type: 'application/pdf' }); const click = jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); mockedApi.get.mockImplementation((url: string) => url === '/documents' ? Promise.resolve({ data: page([upload]) }) : url === '/attachments/download/9' ? pending.promise : Promise.reject(new Error(`Unexpected read ${url}`)) as never); renderPage(); await screen.findByText(upload.name); const download = row(upload.name).getByRole('button', { name: copy('documentsDownloadAttachment') }); fireEvent.click(download); expect(screen.getByRole('region', { name: copy('documentsYourDocuments') })).toHaveFocus(); const search = screen.getByRole('textbox', { name: copy('documentsSearch') }); act(() => search.focus()); expect(search).toHaveFocus(); await act(async () => pending.resolve({ data: bytes, headers: { 'content-type': 'application/pdf' } })); await waitFor(() => expect(download).toBeEnabled()); expect(click).toHaveBeenCalledTimes(1); expect(URL.createObjectURL).toHaveBeenCalledWith(bytes); expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:uploaded-file'); expect(search).toHaveFocus(); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents', '/attachments/download/9']); }); test('[P7-07 documents] download errors retry only that file and late download completion after leaving creates no URL', async () => { const upload = item({ id: 'attachment:9', resourceId: '9', type: 'attachment', name: 'Private uploaded file.pdf', visibility: null, fileType: 'application/pdf', sourceHref: '/jobs/42?section=documents', downloadHref: '/api/attachments/download/9', publicHref: null }); const pending = deferred(); let attempts = 0; mockedApi.get.mockImplementation((url: string) => url === '/documents' ? Promise.resolve({ data: page([upload]) }) : url === '/attachments/download/9' ? ++attempts === 1 ? Promise.reject({ response: { status: 403 } }) : pending.promise : Promise.reject(new Error(`Unexpected read ${url}`)) as never); renderPage(); await screen.findByText(upload.name); fireEvent.click(row(upload.name).getByRole('button', { name: copy('documentsDownloadAttachment') })); expect(await row(upload.name).findByRole('alert')).toHaveTextContent(copy('documentsDownloadFailed')); await language('nb'); const error = row(upload.name).getByRole('alert'); expect(error).toHaveTextContent(copy('documentsDownloadFailed', {}, 'nb')); const retry = within(error).getByRole('button', { name: copy('retry', {}, 'nb') }); act(() => { fireEvent.click(retry); fireEvent.click(retry); }); expect(attempts).toBe(2); expect(reads()).toHaveLength(1); fireEvent.click(screen.getByRole('button', { name: 'Leave documents' })); await screen.findByText('Other workflow'); await act(async () => pending.resolve({ data: new Blob(['private file'], { type: 'application/pdf' }) })); expect(URL.createObjectURL).not.toHaveBeenCalled(); expect(URL.revokeObjectURL).not.toHaveBeenCalled(); expect(reads()).toHaveLength(1); }); test('[P7-07 documents] a published historical CV retains its percent-encoded Nordic public slug without fetching its body', async () => { const historical = item({ name: 'Historical published CV', publicHref: '/cv/legacy-%C3%A6%C3%B8%C3%A5%20cv' }); mockedApi.get.mockResolvedValue({ data: page([historical]) } as never); renderPage(); await screen.findByText(historical.name); expect(row(historical.name).getByRole('link', { name: copy('documentsOpenPublicCv') })).toHaveAttribute('href', historical.publicHref); expect(row(historical.name).getByRole('link', { name: copy('documentsOpenSource') })).toHaveAttribute('href', '/career/builder/12'); expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents']); expect(URL.createObjectURL).not.toHaveBeenCalled(); }); test('[P7-07 documents] malformed external and mismatched resource paths never become source, public or download actions', async () => { const invalidCv = item({ name: 'Unsafe external CV', sourceHref: 'https://external.test/career/builder/12', publicHref: 'javascript:alert(1)' }); const wrongId = item({ id: 'cv:13', resourceId: '13', name: 'Mismatched CV source', sourceHref: '/career/builder/999', publicHref: '//external.test/cv/other' }); const invalidUpload = item({ id: 'attachment:9', resourceId: '9', type: 'attachment', name: 'Mismatched file', visibility: null, sourceHref: '/jobs/99?section=documents', downloadHref: '/api/attachments/download/999', publicHref: null }); mockedApi.get.mockResolvedValue({ data: page([invalidCv, wrongId, invalidUpload]) } as never); renderPage(); await screen.findByText(invalidCv.name); for (const entry of [invalidCv, wrongId, invalidUpload]) { expect(row(entry.name).queryByRole('link', { name: copy('documentsOpenSource') })).not.toBeInTheDocument(); expect(row(entry.name).queryByRole('link', { name: copy('documentsOpenPublicCv') })).not.toBeInTheDocument(); expect(row(entry.name).queryByRole('button', { name: copy('documentsDownloadAttachment') })).not.toBeInTheDocument(); } expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/documents']); expect(URL.createObjectURL).not.toHaveBeenCalled(); });