106 lines
6.4 KiB
TypeScript
106 lines
6.4 KiB
TypeScript
import React from 'react';
|
|
import '@testing-library/jest-dom';
|
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
|
|
import { api } from './api';
|
|
import { AccountPlanProvider } from './accountPlan';
|
|
import { ApplicationStatusSuggestion, ApplicationStrategySnapshot } from './components/ApplicationWorkflowAssist';
|
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
import { ToastProvider } from './toast';
|
|
|
|
jest.mock('./api', () => ({
|
|
api: { get: jest.fn(), post: jest.fn(), patch: jest.fn() },
|
|
getApiErrorMessage: (_error: unknown, fallback?: string) => fallback || 'Request failed.',
|
|
}));
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); });
|
|
|
|
test('recruiter status suggestions require an explicit apply action', async () => {
|
|
const applied = jest.fn();
|
|
mockedApi.get.mockResolvedValue({ data: { hasSuggestion: true, currentStatus: 'Applied', suggestedStatus: 'Interview' } } as any);
|
|
mockedApi.patch.mockResolvedValue({ data: {} } as any);
|
|
render(<I18nProvider><ToastProvider><ApplicationStatusSuggestion jobId={42} onApplied={applied} /></ToastProvider></I18nProvider>);
|
|
|
|
const button = await screen.findByRole('button', { name: 'Apply Interview' });
|
|
expect(mockedApi.patch).not.toHaveBeenCalled();
|
|
fireEvent.click(button);
|
|
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith('/jobapplications/42/status', { status: 'Interview' }));
|
|
expect(applied).toHaveBeenCalled();
|
|
});
|
|
|
|
test('strategy snapshot shows saved output and queues regeneration only on request', async () => {
|
|
mockedApi.get.mockImplementation((url: string) => {
|
|
if (url.endsWith('/focus-plan')) return Promise.resolve({ data: {
|
|
strategicSummary: 'Lead with delivery evidence.',
|
|
immediatePriorities: ['Tailor the summary'],
|
|
proofPointsToLeadWith: ['Reduced lead time'],
|
|
cvBulletIdeas: ['Quantify the migration'],
|
|
coverLetterAngles: ['Public-service impact'],
|
|
followUpApproach: ['Follow up after five days'],
|
|
} } as any);
|
|
return Promise.reject(new Error('no operation'));
|
|
});
|
|
mockedApi.post.mockResolvedValue({ data: {
|
|
created: true,
|
|
statusUrl: '/operations/op-1',
|
|
operation: { id: 'op-1', taskType: 'focus-plan', status: 'queued', createdAtUtc: '', canCancel: true, canRetry: false },
|
|
} } as any);
|
|
|
|
render(<I18nProvider><ToastProvider><AccountPlanProvider value={{ plan: 'pro', canUseAi: true, canUseProThemes: true }}><ApplicationStrategySnapshot jobId={42} /></AccountPlanProvider></ToastProvider></I18nProvider>);
|
|
expect(await screen.findByText('Lead with delivery evidence.')).toBeInTheDocument();
|
|
expect(screen.getByRole('heading', { name: 'Strategy snapshot', level: 2 })).toBeVisible();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByRole('button', { name: 'Regenerate' }));
|
|
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/42/focus-plan/operations', { attachmentIds: null }));
|
|
expect(await screen.findByText('queued')).toBeInTheDocument();
|
|
});
|
|
|
|
function strategy(jobId = 42) {
|
|
return <I18nProvider><ToastProvider><AccountPlanProvider value={{ plan: 'free', canUseAi: false, canUseProThemes: false }}><ApplicationStrategySnapshot jobId={jobId} /></AccountPlanProvider></ToastProvider></I18nProvider>;
|
|
}
|
|
|
|
test('failed strategy reads stay distinct from empty, localize without refetching and retry only the read', async () => {
|
|
let finishOperation!: (result: any) => void;
|
|
mockedApi.get.mockImplementation((url: string) => url.endsWith('/operation') ? new Promise(resolve => { finishOperation = resolve; }) : Promise.reject({ response: { status: 503 } }));
|
|
render(strategy());
|
|
expect(await screen.findByText('Strategy snapshot could not be loaded.')).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled();
|
|
await act(async () => finishOperation({ data: null }));
|
|
expect(screen.queryByText('No strategy snapshot yet. Generate one when you want AI-assisted planning.')).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
const calls = mockedApi.get.mock.calls.length;
|
|
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
|
|
expect(await screen.findByText('Kunne ikke hente strategisammendraget.')).toBeVisible();
|
|
expect(mockedApi.get).toHaveBeenCalledTimes(calls);
|
|
mockedApi.get.mockResolvedValueOnce({ data: { strategicSummary: 'Saved plan', immediatePriorities: [], proofPointsToLeadWith: [], cvBulletIdeas: [], coverLetterAngles: [], followUpApproach: [] } } as any);
|
|
const retry = screen.getByRole('button', { name: 'Prøv igjen' });
|
|
act(() => retry.focus()); fireEvent.click(retry);
|
|
expect(await screen.findByDisplayValue('Saved plan')).toBeVisible();
|
|
expect(mockedApi.get.mock.calls.slice(calls).map(call => call[0])).toEqual(['/jobapplications/42/focus-plan']);
|
|
expect(screen.getByRole('heading', { name: 'Strategioversikt', level: 2 })).toHaveFocus();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
expect(screen.getByRole('button', { name: 'Krever Pro' })).toBeDisabled();
|
|
});
|
|
|
|
test('only the explicit not-generated response is an intentional empty strategy', async () => {
|
|
mockedApi.get.mockRejectedValue({ response: { status: 404, data: { code: 'strategy_not_generated' } } });
|
|
render(strategy());
|
|
expect(await screen.findByText('No strategy snapshot yet. Generate one when you want AI-assisted planning.')).toBeVisible();
|
|
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('a late strategy response cannot replace another job and denial never offers read retry', async () => {
|
|
let finish!: (result: any) => void;
|
|
mockedApi.get.mockImplementation((url: string) => url === '/jobapplications/42/focus-plan' ? new Promise(resolve => { finish = resolve; }) : Promise.reject({ response: { status: 403 } }));
|
|
const view = render(strategy());
|
|
view.rerender(strategy(43));
|
|
expect(await screen.findByText('Strategy snapshot could not be loaded.')).toBeVisible();
|
|
await act(async () => finish({ data: { strategicSummary: 'Do not show old owner plan', immediatePriorities: [], proofPointsToLeadWith: [], cvBulletIdeas: [], coverLetterAngles: [], followUpApproach: [] } }));
|
|
expect(screen.queryByDisplayValue('Do not show old owner plan')).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|