132 lines
5.4 KiB
TypeScript
132 lines
5.4 KiB
TypeScript
import React from 'react';
|
|
import '@testing-library/jest-dom';
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { ConfirmProvider } from './confirm';
|
|
import { PromptProvider } from './prompt';
|
|
import Attachments from './components/Attachments';
|
|
import { ToastProvider } from './toast';
|
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
import { api } from './api';
|
|
|
|
jest.mock('./api', () => ({
|
|
getApiErrorMessage: (_error: unknown, fallback: string) => fallback,
|
|
api: {
|
|
get: jest.fn(),
|
|
post: jest.fn(),
|
|
patch: jest.fn(),
|
|
delete: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
test.each([
|
|
['en', 'Preview: resume.pdf', 'Preview: resume.pdf'],
|
|
['nb', 'Forhåndsvis: resume.pdf', 'Forhåndsvisning: resume.pdf'],
|
|
])('PDF preview identifies the document in %s', async (language, buttonName, frameTitle) => {
|
|
const previousLanguage = localStorage.getItem('uiLanguage');
|
|
const originalCreate = URL.createObjectURL;
|
|
const originalRevoke = URL.revokeObjectURL;
|
|
localStorage.setItem('uiLanguage', language);
|
|
URL.createObjectURL = jest.fn(() => 'blob:attachment-preview');
|
|
URL.revokeObjectURL = jest.fn();
|
|
mockedApi.get.mockResolvedValueOnce({ data: [{ id: 7, fileName: 'resume.pdf', fileType: 'application/pdf', fileSize: 2048, uploadDate: '2026-11-02T10:00:00Z' }] } as any);
|
|
mockedApi.get.mockResolvedValueOnce({ data: new Blob(['sample'], { type: 'application/pdf' }) } as any);
|
|
const view = render(<ToastProvider><I18nProvider><ConfirmProvider><PromptProvider><Attachments jobId={42} /></PromptProvider></ConfirmProvider></I18nProvider></ToastProvider>);
|
|
try {
|
|
fireEvent.click(await screen.findByRole('button', { name: buttonName }));
|
|
const frame = await screen.findByTitle(frameTitle);
|
|
expect(frame.tagName).toBe('IFRAME');
|
|
expect(frame).toHaveAttribute('src', 'blob:attachment-preview');
|
|
} finally {
|
|
view.unmount();
|
|
URL.createObjectURL = originalCreate;
|
|
URL.revokeObjectURL = originalRevoke;
|
|
if (previousLanguage === null) localStorage.removeItem('uiLanguage');
|
|
else localStorage.setItem('uiLanguage', previousLanguage);
|
|
}
|
|
});
|
|
|
|
test('attachments empty state renders drag and drop guidance', async () => {
|
|
mockedApi.get.mockResolvedValueOnce({ data: [] } as any);
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<I18nProvider>
|
|
<ConfirmProvider>
|
|
<PromptProvider>
|
|
<Attachments jobId={42} />
|
|
</PromptProvider>
|
|
</ConfirmProvider>
|
|
</I18nProvider>
|
|
</ToastProvider>,
|
|
);
|
|
|
|
expect(await screen.findByText(/drag and drop files here/i)).toBeInTheDocument();
|
|
expect(await screen.findByText(/no attachments yet/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/max 10 mb each/i)).toBeInTheDocument();
|
|
});
|
|
|
|
test('attachments metadata controls update purpose and ai usage', async () => {
|
|
mockedApi.get.mockResolvedValueOnce({
|
|
data: [
|
|
{
|
|
id: 7,
|
|
fileName: 'resume.pdf',
|
|
uploadDate: new Date().toISOString(),
|
|
fileType: 'application/pdf',
|
|
fileSize: 2048,
|
|
purpose: 'resume',
|
|
useForAi: true,
|
|
},
|
|
],
|
|
} as any);
|
|
mockedApi.patch.mockResolvedValue({ data: {} } as any);
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<I18nProvider>
|
|
<ConfirmProvider>
|
|
<PromptProvider>
|
|
<Attachments jobId={42} />
|
|
</PromptProvider>
|
|
</ConfirmProvider>
|
|
</I18nProvider>
|
|
</ToastProvider>,
|
|
);
|
|
|
|
expect(await screen.findByDisplayValue(/resume/i)).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Preview: resume.pdf' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Download: resume.pdf' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Rename: resume.pdf' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Delete: resume.pdf' })).toBeInTheDocument();
|
|
expect(screen.getAllByRole('columnheader')).toHaveLength(8);
|
|
expect(screen.getByRole('columnheader', { name: 'Uploaded' })).toBeInTheDocument();
|
|
|
|
fireEvent.mouseDown(screen.getByRole('combobox', { name: /purpose/i }));
|
|
fireEvent.click(await screen.findByRole('option', { name: /portfolio/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApi.patch).toHaveBeenCalledWith('/attachments/7', expect.objectContaining({ purpose: 'portfolio', useForAi: true }));
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('switch', { name: 'Use for AI: resume.pdf' }));
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApi.patch).toHaveBeenCalledWith('/attachments/7', expect.objectContaining({ purpose: 'portfolio', useForAi: false }));
|
|
});
|
|
});
|
|
|
|
test('attachment failures remain explicit and retry restores the real empty state', async () => {
|
|
mockedApi.get.mockRejectedValueOnce({ response: { status: 503 } }).mockResolvedValueOnce({ data: [] } as any);
|
|
render(<ToastProvider><I18nProvider><ConfirmProvider><PromptProvider><Attachments jobId={42} /></PromptProvider></ConfirmProvider></I18nProvider></ToastProvider>);
|
|
expect(await screen.findByRole('alert')).toHaveTextContent("Could not load this application's attachments.");
|
|
expect(screen.queryByText(/no attachments yet/i)).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
|
expect(await screen.findByText(/no attachments yet/i)).toBeInTheDocument();
|
|
expect(mockedApi.get).toHaveBeenCalledTimes(2);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|