Files
jobtrackingapp/job-tracker-ui/src/settings-view.test.tsx
T

134 lines
4.4 KiB
TypeScript

import React from 'react';
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import SettingsView from './components/SettingsView';
import { I18nProvider } from './i18n/I18nProvider';
import { ToastProvider } from './toast';
import { api } from './api';
jest.mock('./api', () => ({
api: {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
patch: jest.fn(),
delete: jest.fn(),
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
},
getApiErrorMessage: (error: any, fallback?: string) => fallback || 'Request failed.',
}));
jest.mock('./components/ImportExportJobs', () => () => <div>Import Export Stub</div>);
jest.mock('./components/BackupCard', () => () => <div>Backup Stub</div>);
const mockedApi = api as jest.Mocked<typeof api>;
function renderView() {
return render(
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ToastProvider>
<I18nProvider>
<SettingsView
pageSize={20}
onPageSizeChange={jest.fn()}
columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }}
onColumnsChange={jest.fn()}
themeMode="dark"
onThemeModeChange={jest.fn()}
/>
</I18nProvider>
</ToastProvider>
</MemoryRouter>,
);
}
beforeEach(() => {
mockedApi.get.mockImplementation((url: string) => {
if (url === '/ai/settings') {
return Promise.resolve({
data: {
enabled: true,
externalProcessingAllowed: false,
externalProcessingAvailable: false,
effectiveExternalProcessing: false,
provider: 'local',
},
} as any);
}
if (url === '/ai/usage') {
return Promise.resolve({ data: {
currentMonth: { calls: 0, estimatedTokens: 0 },
plan: 'free',
monthlyCallLimit: 0,
monthlyTokenLimit: 0,
storageUsedBytes: 0,
storageLimitBytes: 250_000_000,
} } as any);
}
if (url === '/billing/status') {
return Promise.resolve({ data: { enabled: false, canCheckout: false, canManage: false } } as any);
}
if (url === '/rules') {
return Promise.resolve({
data: {
id: 1,
appliedFollowUpDays: 14,
appliedGhostDays: 30,
offerFollowUpDays: 7,
offerGhostDays: 14,
feedbackFollowUpDays: 7,
feedbackGhostDays: 14,
},
} as any);
}
return Promise.resolve({ data: {} } as any);
});
window.localStorage.clear();
mockedApi.put.mockImplementation((url: string, body: any) => Promise.resolve({
data: {
...body,
externalProcessingAvailable: false,
effectiveExternalProcessing: false,
provider: 'local',
},
} as any));
});
afterEach(() => {
jest.clearAllMocks();
});
test('settings view has no accent picker and uses one follow-up section, one notification section', async () => {
renderView();
expect(screen.queryByText(/accent/i)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /follow-ups/i }));
expect(await screen.findByText(/follow-up rules by scenario/i)).toBeInTheDocument();
expect(screen.queryAllByText(/open reminders/i)).toHaveLength(0);
fireEvent.click(screen.getByRole('tab', { name: /notifications/i }));
expect(screen.getByText(/notification settings/i)).toBeInTheDocument();
// SMTP status now lives under Admin → System → Settings (the old "check system status" link was removed).
expect(screen.getAllByText(/smtp delivery and test mail live under/i).length).toBe(1);
expect(screen.getByLabelText(/email reminders for follow-ups/i)).toBeInTheDocument();
expect(screen.getByLabelText(/email alerts for ghosted jobs/i)).toBeInTheDocument();
});
test('AI privacy settings are server-backed and external processing is local-only by default', async () => {
renderView();
expect(await screen.findByText(/AI privacy/i)).toBeInTheDocument();
const external = screen.getByLabelText(/allow approved external AI processing/i);
expect(external).toBeDisabled();
fireEvent.click(screen.getByLabelText(/enable AI features for my account/i));
fireEvent.click(screen.getByRole('button', { name: /save AI privacy settings/i }));
expect(mockedApi.put).toHaveBeenCalledWith('/ai/settings', {
enabled: false,
externalProcessingAllowed: false,
});
});