acf60c2a07
Wave 6. Swaps react-scripts' build/dev tooling for Next.js while keeping the app's actual routing/rendering model unchanged -- the app is almost entirely behind auth with no proven SSR/SEO need, so a real App Router rewrite would touch ~90 files for zero user-visible benefit. - next.config.js: output:'export' (static HTML+JS, same "single index.html served by nginx with try_files fallback" deploy as CRA). - app/layout.tsx + app/page.tsx: root shell ports public/index.html's <head>, mounts the whole existing App tree client-only (ssr:false) since it reads window/localStorage during initial render and Next's static prerender would otherwise execute that on the server. - Renamed src/pages/ -> src/views/ (Next's Pages Router auto-detects any `pages/` dir under the app root and tried to build our React Router page components as its own routes). - REACT_APP_* -> NEXT_PUBLIC_* across code, .env.development, Dockerfile, docker-compose.yml build args. - Replaced the CRA SVGR import (`ReactComponent` from .svg, unsupported under Turbopack) with a small inline JobbjaktMark component. - TypeScript 4.9 -> 5.9 (MUI v8's type-checked build needs syntax 4.9's parser rejects; CRA never hit this because babel doesn't type-check). - Dropped CRA-only files (index.tsx, reportWebVitals, react-app-env.d.ts, public/index.html); kept react-scripts as the Jest test runner only (next/jest migration not needed -- the existing config already works). Verified: `next build` static export succeeds, `next dev` serves the landing page and client-side routes (login etc.) correctly, all 57 frontend tests + 172 backend tests still green. Known caveat: deep-linking straight to a sub-route (e.g. /login) 404s in `next dev` since there's no server route for it -- the app only ever mounts at "/". Production is unaffected: nginx's existing try_files fallback still serves index.html for any path.
203 lines
7.3 KiB
TypeScript
203 lines
7.3 KiB
TypeScript
import React from 'react';
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
|
|
import AdminSystemPage from './views/AdminSystemPage';
|
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
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.',
|
|
}));
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
describe('AdminSystemPage', () => {
|
|
beforeEach(() => {
|
|
mockedApi.get.mockImplementation((url: string) => {
|
|
if (url === '/admin/system') {
|
|
return Promise.resolve({
|
|
data: {
|
|
environment: 'Production',
|
|
contentRoot: '/app',
|
|
version: '1.2.3',
|
|
commitSha: 'abc1234',
|
|
buildStamp: '2026-03-23 11:00 UTC',
|
|
storage: { dataRoot: '/data', dbPath: '/data/jobtracker.db', dbExists: true, dbSizeBytes: 2048, companyCount: 3, jobCount: 7, deletedCount: 1 },
|
|
email: { enabled: true, host: 'smtp.example.test', port: 587, enableSsl: true, from: 'noreply@example.test', fromName: 'Jobbjakt' },
|
|
database: { provider: 'mariadb', looksConfigured: true, canConnect: true, target: 'server=db', usesFileStorage: false, warning: null },
|
|
runtime: { framework: '.NET 9', osDescription: 'Linux', processArchitecture: 'X64', machineName: 'app-01' },
|
|
auth: { required: true, hasJwtKey: true, googleConfigured: true, gmailConfigured: true },
|
|
ai: {
|
|
healthy: true,
|
|
model: 'distilbart',
|
|
device: 'cpu',
|
|
gpuAvailable: false,
|
|
gpuName: null,
|
|
ocrAvailable: true,
|
|
ocrLanguages: 'eng',
|
|
ollamaConfigured: true,
|
|
ollamaReachable: true,
|
|
ollamaModel: 'qwen2.5:7b',
|
|
ollamaModelAvailable: true,
|
|
ollamaVersion: '0.7.0',
|
|
ollamaInstalledModels: ['qwen2.5:7b', 'nomic-embed-text'],
|
|
ollamaLoadedModels: ['qwen2.5:7b'],
|
|
ollamaLoadedCount: 1,
|
|
healthLatencyMs: 12.4,
|
|
probeLatencyMs: 25.8,
|
|
lastProbeAt: '2026-03-23T10:00:00Z',
|
|
lastProbeSuccessAt: '2026-03-23T10:00:00Z',
|
|
lastProbeFailureAt: null,
|
|
probeFailures: 0,
|
|
requests: 18,
|
|
cacheHits: 9,
|
|
cacheMisses: 9,
|
|
failures: 0,
|
|
averageLatencyMs: 42.2,
|
|
ocrRequests: 5,
|
|
ocrFailures: 0,
|
|
averageOcrLatencyMs: 88.4,
|
|
lastOcrSuccessAt: '2026-03-23T10:05:00Z',
|
|
lastOcrFailureAt: null,
|
|
lastSuccessAt: '2026-03-23T10:04:00Z',
|
|
lastFailureAt: null,
|
|
lastError: null,
|
|
},
|
|
},
|
|
} as any);
|
|
}
|
|
if (url === '/admin/system/email-settings') {
|
|
return Promise.resolve({
|
|
data: {
|
|
enabled: true,
|
|
host: 'smtp.example.test',
|
|
port: 587,
|
|
user: 'mailer@example.test',
|
|
from: 'noreply@example.test',
|
|
fromName: 'Jobbjakt',
|
|
enableSsl: true,
|
|
timeoutMs: 15000,
|
|
usesOverrides: false,
|
|
hasPassword: true,
|
|
},
|
|
} as any);
|
|
}
|
|
if (url === '/admin/system/cv-benchmark') {
|
|
return Promise.resolve({
|
|
data: {
|
|
rootPath: '/data/CvBenchmarks/latest',
|
|
lastUpdatedAtUtc: '2026-03-23T10:10:00Z',
|
|
reportMarkdown: '# CV benchmark report\n\n- Files: 4',
|
|
indexJson: JSON.stringify({
|
|
CorpusRoot: '/home/pi/cvs',
|
|
OutputRoot: '/data/CvBenchmarks/latest',
|
|
GeneratedAtUtc: '2026-03-23T10:10:00Z',
|
|
TotalFiles: 4,
|
|
AverageCoverage: 0.72,
|
|
AverageConfidence: 0.66,
|
|
AverageConsistency: 0.94,
|
|
FilesWithSuspiciousLocations: 1,
|
|
MissingApprovedFixtures: 4,
|
|
Entries: [
|
|
{
|
|
FileName: 'cv.txt',
|
|
Slug: 'cv-txt',
|
|
Extension: '.txt',
|
|
Characters: 2000,
|
|
OutputPath: '/data/CvBenchmarks/latest/outputs/cv-txt.json',
|
|
ApprovedFixturePath: null,
|
|
CandidateFixturePath: '/data/CvBenchmarks/latest/candidate-fixtures/cv-txt.json',
|
|
ContactLocation: 'San Francisco, Hobbies',
|
|
FirstJob: '* July',
|
|
FirstJobLocation: null,
|
|
FirstEducation: '* September',
|
|
FirstEducationLocation: null,
|
|
QualificationLevels: ['Other'],
|
|
SuspiciousLocations: [],
|
|
CoverageScore: 0.5,
|
|
ConfidenceScore: 0.65,
|
|
ConsistencyScore: 0.8,
|
|
DiffSummary: 'No approved fixture yet — candidate fixture written.',
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
} as any);
|
|
}
|
|
return Promise.resolve({ data: {} } as any);
|
|
});
|
|
mockedApi.put.mockResolvedValue({
|
|
data: {
|
|
enabled: true,
|
|
host: 'smtp.changed.test',
|
|
port: 2525,
|
|
user: 'mailer@example.test',
|
|
from: 'noreply@example.test',
|
|
fromName: 'Jobbjakt',
|
|
enableSsl: true,
|
|
timeoutMs: 15000,
|
|
usesOverrides: true,
|
|
hasPassword: true,
|
|
},
|
|
} as any);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('renders AI service health, latency, and OCR readiness', async () => {
|
|
render(
|
|
<I18nProvider>
|
|
<AdminSystemPage />
|
|
</I18nProvider>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Production')).toBeTruthy();
|
|
});
|
|
|
|
expect(screen.getByText(/25.8 ms probe/i)).toBeTruthy();
|
|
expect(screen.getByText('OCR eng')).toBeTruthy();
|
|
expect(screen.getAllByText(/ollama configured/i).length).toBeGreaterThan(0);
|
|
expect(screen.getByText(/ollama version/i)).toBeTruthy();
|
|
expect(screen.getByText(/model · qwen2.5:7b/i)).toBeTruthy();
|
|
expect(screen.getByText(/cv benchmark review/i)).toBeTruthy();
|
|
expect(screen.getByText(/top parser findings/i)).toBeTruthy();
|
|
expect(screen.getByText(/suspicious contact location: san francisco, hobbies/i)).toBeTruthy();
|
|
expect(screen.getByText('OCR avg latency')).toBeTruthy();
|
|
expect(screen.getByText('88.4 ms')).toBeTruthy();
|
|
});
|
|
|
|
it('loads and saves smtp settings from the settings tab', async () => {
|
|
render(
|
|
<I18nProvider>
|
|
<AdminSystemPage />
|
|
</I18nProvider>,
|
|
);
|
|
|
|
expect(await screen.findByText('AI service')).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('tab', { name: /settings/i }));
|
|
|
|
const hostInput = await screen.findByLabelText(/host/i);
|
|
fireEvent.change(hostInput, { target: { value: 'smtp.changed.test' } });
|
|
fireEvent.change(screen.getByLabelText(/port/i), { target: { value: '2525' } });
|
|
fireEvent.click(screen.getByRole('button', { name: /save settings/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApi.put).toHaveBeenCalledWith('/admin/system/email-settings', expect.objectContaining({
|
|
host: 'smtp.changed.test',
|
|
port: 2525,
|
|
}));
|
|
});
|
|
});
|
|
});
|