Files
jobtrackingapp/job-tracker-ui/src/admin-system-page.test.tsx
T
cesnimda acf60c2a07
CI and Deploy / test (pull_request) Failing after 50s
CI and Deploy / deploy (pull_request) Has been skipped
build(frontend): migrate CRA to Next.js (CSR lift-and-shift)
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.
2026-07-12 00:50:45 +02:00

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,
}));
});
});
});