237 lines
9.7 KiB
TypeScript
237 lines
9.7 KiB
TypeScript
import React from 'react';
|
|
import '@testing-library/jest-dom';
|
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
import { ConfirmProvider } from './confirm';
|
|
import { PromptProvider } from './prompt';
|
|
import { ToastProvider } from './toast';
|
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
import DashboardView from './components/DashboardView';
|
|
import RemindersView from './components/RemindersView';
|
|
import JobTable from './components/JobTable';
|
|
import { api } from './api';
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
const pagedJobs = {
|
|
items: [
|
|
{
|
|
id: 42,
|
|
jobTitle: 'Backend Developer',
|
|
status: 'Waiting',
|
|
dateApplied: new Date().toISOString(),
|
|
daysSince: 10,
|
|
company: { name: 'Acme' },
|
|
companyId: 1,
|
|
needsFollowUp: true,
|
|
followUpReason: 'Follow-up due soon',
|
|
shortSummary: 'Strong backend match',
|
|
tailoredCvText: 'Saved CV',
|
|
notes: 'Notes\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>',
|
|
workflowSignal: {
|
|
actionKey: 'follow-up',
|
|
reason: 'Follow-up due soon',
|
|
workspaceTab: 'follow-up',
|
|
followMode: 'waiting-update',
|
|
needsAttention: true,
|
|
hasPackageGap: false,
|
|
needsInterviewPrep: false,
|
|
needsFollowUpAction: true,
|
|
hasTailoredCv: true,
|
|
hasSavedApplicationAnswerDraft: true,
|
|
hasInterviewPrepNotes: true,
|
|
},
|
|
},
|
|
{
|
|
id: 43,
|
|
jobTitle: 'Platform Engineer',
|
|
status: 'Applied',
|
|
dateApplied: new Date().toISOString(),
|
|
daysSince: 4,
|
|
company: { name: 'Beta' },
|
|
companyId: 2,
|
|
needsFollowUp: false,
|
|
followUpReason: 'Tailored CV missing',
|
|
shortSummary: 'Platform work',
|
|
tailoredCvText: null,
|
|
notes: null,
|
|
workflowSignal: {
|
|
actionKey: 'package-work',
|
|
reason: 'Tailored CV missing for this role.',
|
|
workspaceTab: 'tailored-cv',
|
|
followMode: null,
|
|
needsAttention: true,
|
|
hasPackageGap: true,
|
|
needsInterviewPrep: false,
|
|
needsFollowUpAction: false,
|
|
hasTailoredCv: false,
|
|
hasSavedApplicationAnswerDraft: false,
|
|
hasInterviewPrepNotes: false,
|
|
},
|
|
},
|
|
],
|
|
total: 2,
|
|
page: 1,
|
|
pageSize: 15,
|
|
};
|
|
|
|
const reminderItems = [
|
|
{
|
|
id: 42,
|
|
jobTitle: 'Backend Developer',
|
|
status: 'Waiting',
|
|
dateApplied: new Date().toISOString(),
|
|
daysSince: 10,
|
|
company: { name: 'Acme' },
|
|
needsFollowUp: true,
|
|
followUpReason: 'Follow-up due soon',
|
|
tailoredCvText: 'Saved CV',
|
|
followUpAt: new Date().toISOString(),
|
|
workflowSignal: {
|
|
actionKey: 'follow-up',
|
|
reason: 'Follow-up due soon',
|
|
workspaceTab: 'follow-up',
|
|
followMode: 'waiting-update',
|
|
needsAttention: true,
|
|
hasPackageGap: false,
|
|
needsInterviewPrep: false,
|
|
needsFollowUpAction: true,
|
|
hasTailoredCv: true,
|
|
hasSavedApplicationAnswerDraft: true,
|
|
hasInterviewPrepNotes: true,
|
|
},
|
|
},
|
|
{
|
|
id: 43,
|
|
jobTitle: 'Platform Engineer',
|
|
status: 'Applied',
|
|
dateApplied: new Date().toISOString(),
|
|
daysSince: 4,
|
|
company: { name: 'Beta' },
|
|
needsFollowUp: true,
|
|
followUpReason: 'Tailored CV missing',
|
|
tailoredCvText: null,
|
|
followUpAt: new Date().toISOString(),
|
|
workflowSignal: {
|
|
actionKey: 'package-work',
|
|
reason: 'Tailored CV missing for this role.',
|
|
workspaceTab: 'tailored-cv',
|
|
followMode: null,
|
|
needsAttention: true,
|
|
hasPackageGap: true,
|
|
needsInterviewPrep: false,
|
|
needsFollowUpAction: true,
|
|
hasTailoredCv: false,
|
|
hasSavedApplicationAnswerDraft: false,
|
|
hasInterviewPrepNotes: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
function setupApiMocks() {
|
|
mockedApi.get.mockImplementation((url: string) => {
|
|
if (url === '/companies') return Promise.resolve({ data: [{ id: 1, name: 'Acme' }, { id: 2, name: 'Beta' }] } as any);
|
|
if (url === '/jobapplications/reminders') return Promise.resolve({ data: reminderItems } as any);
|
|
if (url === '/jobapplications') return Promise.resolve({ data: pagedJobs } as any);
|
|
if (url === '/jobapplications/stats') return Promise.resolve({ data: { total: 2, active: 2, deleted: 0, byStatus: {}, appliedLast30Days: 2, averageDaysSinceApplied: 7 } } as any);
|
|
if (url === '/jobapplications/analytics-overview') return Promise.resolve({ data: { funnel: [], responseRateBySource: [], topCompanies: [], totalResponses: 1, totalActive: 2 } } as any);
|
|
if (url === '/jobapplications/analytics' || url === '/jobapplications/tags') return Promise.resolve({ data: [] } as any);
|
|
if (url === '/jobapplications/tag-trends') return Promise.resolve({ data: { months: [], series: [] } } as any);
|
|
if (url === '/jobapplications/42') {
|
|
return Promise.resolve({ data: { id: 42, jobTitle: 'Backend Developer', status: 'Waiting', dateApplied: new Date().toISOString(), daysSince: 10, company: { name: 'Acme', recruiterEmail: 'maria@acme.test' }, tailoredCvText: 'Saved CV', coverLetterText: 'Saved cover letter', recruiterMessageDraft: 'Saved recruiter message', notes: 'Notes\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>', shortSummary: 'Strong backend match' } } as any);
|
|
}
|
|
if (url === '/jobapplications/43') {
|
|
return Promise.resolve({ data: { id: 43, jobTitle: 'Platform Engineer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 4, company: { name: 'Beta', recruiterEmail: 'recruiter@beta.test' }, tailoredCvText: '', coverLetterText: '', recruiterMessageDraft: '', notes: '', shortSummary: 'Platform work' } } as any);
|
|
}
|
|
if (url === '/jobapplications/42/history' || url === '/jobapplications/43/history') return Promise.resolve({ data: [] } as any);
|
|
if (url === '/attachments/42' || url === '/attachments/43') return Promise.resolve({ data: [] } as any);
|
|
if (url === '/jobapplications/42/followup-draft') {
|
|
return Promise.resolve({ data: { subject: 'Re: Backend Developer application update', body: 'Hi Maria,\n\nFollow-up draft.\n\nThanks,\nDemo', reason: 'Follow-up due soon', suggestedSendOn: new Date().toISOString(), contextSummary: 'Saved application package material is available for reuse.', contextSignals: ['Saved cover letter available'], threadSubject: 'Backend Developer application update', lastCorrespondenceFrom: 'Maria Recruiter <maria@acme.test>', lastCorrespondenceAt: new Date().toISOString() } } as any);
|
|
}
|
|
return Promise.resolve({ data: [] } as any);
|
|
});
|
|
}
|
|
|
|
function LocationIndicator() {
|
|
const location = useLocation();
|
|
return <div data-testid="location-indicator">{location.pathname}{location.search}</div>;
|
|
}
|
|
|
|
function renderLoop(initialPath: string) {
|
|
return render(
|
|
<ToastProvider>
|
|
<I18nProvider>
|
|
<ConfirmProvider>
|
|
<PromptProvider>
|
|
<MemoryRouter initialEntries={[initialPath]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
|
<LocationIndicator />
|
|
<Routes>
|
|
<Route path="/dashboard" element={<DashboardView />} />
|
|
<Route path="/reminders" element={<RemindersView />} />
|
|
<Route path="/jobs" element={<JobTable refreshToken={0} pageSize={15} onPageSizeChange={() => {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} />
|
|
</Routes>
|
|
</MemoryRouter>
|
|
</PromptProvider>
|
|
</ConfirmProvider>
|
|
</I18nProvider>
|
|
</ToastProvider>,
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
setupApiMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('dashboard attention card opens follow-up workspace', async () => {
|
|
renderLoop('/dashboard');
|
|
|
|
expect(await screen.findByText(/needs follow-up/i)).toBeInTheDocument();
|
|
fireEvent.click(await screen.findByRole('button', { name: /follow up/i }));
|
|
|
|
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
|
expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument();
|
|
});
|
|
|
|
test('reminders open action routes tailored-cv gaps into the tailored cv workspace', async () => {
|
|
renderLoop('/reminders');
|
|
|
|
expect(await screen.findByText(/missing tailored cv/i)).toBeInTheDocument();
|
|
const platformJob = await screen.findByText(/platform engineer/i);
|
|
const platformCard = platformJob.closest('.MuiPaper-root') ?? platformJob.parentElement?.parentElement;
|
|
expect(platformCard).toBeTruthy();
|
|
fireEvent.click(within(platformCard as HTMLElement).getByRole('button', { name: /build package/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs');
|
|
});
|
|
expect(await screen.findByText(/saved working material/i)).toBeInTheDocument();
|
|
});
|
|
|
|
test('job table urgency signals and next actions route into the shared workspace flow', async () => {
|
|
const firstRender = renderLoop('/jobs');
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: /backend developer — follow up signal/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs');
|
|
});
|
|
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
|
expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument();
|
|
|
|
firstRender.unmount();
|
|
renderLoop('/jobs');
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: /next action: platform engineer — build package/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs');
|
|
});
|
|
expect(await screen.findByText(/saved working material/i)).toBeInTheDocument();
|
|
expect(await screen.findByText(/platform work/i)).toBeInTheDocument();
|
|
});
|