100 lines
7.6 KiB
TypeScript
100 lines
7.6 KiB
TypeScript
import { test, expect, type APIRequestContext } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
|
|
import os from 'node:os';
|
|
|
|
const api = 'http://localhost:5302/api';
|
|
const password = 'E2ePassword123!';
|
|
async function login(request: APIRequestContext, email: string) {
|
|
expect((await request.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
|
const me = await (await request.get(`${api}/auth/me`)).json();
|
|
const csrf = (await request.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value;
|
|
return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string };
|
|
}
|
|
|
|
test('P9 bounded sustained reads with 250 jobs and long CV while using native workspaces', async ({ page, context, playwright }, info) => {
|
|
test.setTimeout(240000);
|
|
const admin = await playwright.request.newContext();
|
|
const latency: { worker: number; endpoint: string; ms: number; bytes: number; status: number }[] = [];
|
|
const navigation: object[] = [];
|
|
const cdp = await context.newCDPSession(page);
|
|
await cdp.send('Performance.enable');
|
|
try {
|
|
const adminHeaders = await login(admin, 'e2e@example.test');
|
|
const email = `p9-sustained-${Date.now()}@example.test`;
|
|
expect((await admin.post(`${api}/users`, { headers: adminHeaders, data: { email, password, roles: ['Premium'] } })).status()).toBe(200);
|
|
const headers = await login(page.request, email);
|
|
const company = await page.request.post(`${api}/companies`, { headers, data: { name: 'P9 bounded workload' } });
|
|
expect(company.status()).toBe(201); const companyId = (await company.json()).id;
|
|
for (let index = 0; index < 250; index++) expect((await page.request.post(`${api}/jobapplications`, { headers,
|
|
data: { companyId, jobTitle: `P9 bounded role ${index}`, status: 'Applied', dateApplied: new Date().toISOString() } })).status()).toBe(201);
|
|
const profile = { version: '1', contact: { fullName: 'Åse Synthetic', email: 'synthetic@example.test' }, summary: ['Documented accessible workflows.'],
|
|
jobs: Array.from({ length: 8 }, (_, index) => ({ id: `p9-role-${index}`, title: `Platform engineer ${index}`, company: 'Nordlys synthetic',
|
|
start: '2017-01', end: '2025-12', bullets: Array.from({ length: 6 }, (_, bullet) => `Evidence ${index}.${bullet}: ${'Documented reliable workflows. Dokumenterte pålitelige arbeidsflyter. '.repeat(3).trim()}`) })) };
|
|
expect((await page.request.put(`${api}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
|
const savedProfile = (await (await page.request.get(`${api}/career/profile`)).json()).profile;
|
|
expect(savedProfile.jobs).toHaveLength(8);
|
|
expect(savedProfile.jobs.map((job: { start: string; end: string; bullets: string[] }) => ({ start: job.start, end: job.end, bullets: job.bullets })))
|
|
.toEqual(profile.jobs.map(job => ({ start: job.start, end: job.end, bullets: job.bullets })));
|
|
const cv = await page.request.post(`${api}/cv/variants`, { headers, data: { name: 'P9 long workload CV', settings: { themeId: 'minimal', language: 'nb' } } });
|
|
expect(cv.status()).toBe(200); const cvId = (await cv.json()).id;
|
|
await context.route('**/*', route => {
|
|
const url = new URL(route.request().url());
|
|
return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort();
|
|
});
|
|
const endpoints = ['/jobapplications/stats', '/jobapplications/board', '/career/profile', '/documents'];
|
|
const started = Date.now();
|
|
const readWorker = async (worker: number) => {
|
|
for (let index = 0; index < 120; index++) {
|
|
const endpoint = endpoints[index % endpoints.length]; const begin = Date.now();
|
|
const response = await page.request.get(`${api}${endpoint}`);
|
|
latency.push({ worker, endpoint, ms: Date.now() - begin, bytes: (await response.body()).length, status: response.status() });
|
|
expect(response.status(), endpoint).toBe(200);
|
|
if (endpoint.endsWith('/board')) expect(await response.json()).toHaveLength(250);
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
};
|
|
const interact = async () => {
|
|
for (let cycle = 0; cycle < 5; cycle++) for (const route of ['/dashboard', '/jobs?view=table', '/jobs?view=board', `/career/builder/${cvId}`]) {
|
|
const begin = Date.now(); await page.goto(route);
|
|
if (route === '/dashboard') await expect(page.getByRole('link', { name: 'Tracked jobs: 250', exact: true })).toBeVisible();
|
|
else if (route.endsWith('board')) await expect(page.locator('[data-kanban-card-id]')).toHaveCount(250);
|
|
else if (route.endsWith('table')) await expect(page.locator('[data-job-row-id]').first()).toBeVisible();
|
|
else {
|
|
await expect(page.getByRole('region', { name: 'Live preview', exact: true })).toHaveAttribute('aria-busy', 'false');
|
|
const measured = await page.getByTitle('CV preview', { exact: true }).evaluate(element => {
|
|
const doc = (element as HTMLIFrameElement).contentDocument!;
|
|
const pages = Number(doc.documentElement.dataset.cvPreviewPages);
|
|
const width = Number(doc.documentElement.dataset.cvPreviewWidth);
|
|
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
|
let node; let finalMarkerPage = 0;
|
|
while ((node = walker.nextNode())) {
|
|
const index = node.textContent?.indexOf('Evidence 7.5:') ?? -1;
|
|
if (index < 0) continue;
|
|
const range = doc.createRange(); range.setStart(node, index); range.setEnd(node, index + 'Evidence 7.5:'.length);
|
|
finalMarkerPage = Math.floor(range.getClientRects()[0].x / width) + 1;
|
|
}
|
|
return { pages, finalMarkerPage, ready: doc.documentElement.dataset.cvPreviewReady };
|
|
});
|
|
expect(measured.ready).toBe('true'); expect(measured.pages).toBeGreaterThan(1);
|
|
expect(measured.finalMarkerPage).toBe(measured.pages);
|
|
await expect(page.locator('[data-cv-page]')).toHaveCount(measured.pages);
|
|
await expect(page.getByTitle('CV preview page 1', { exact: true })).toBeVisible();
|
|
}
|
|
navigation.push({ cycle, route, readyMs: Date.now() - begin,
|
|
metrics: (await cdp.send('Performance.getMetrics')).metrics,
|
|
domElements: await page.locator('*').count() });
|
|
}
|
|
};
|
|
await Promise.all([readWorker(0), readWorker(1), interact()]);
|
|
const durationMs = Date.now() - started;
|
|
expect(durationMs).toBeGreaterThanOrEqual(60000);
|
|
expect(latency).toHaveLength(240);
|
|
expect((await (await page.request.get(`${api}/jobapplications/board`)).json())).toHaveLength(250);
|
|
expect((await (await page.request.get(`${api}/career/profile`)).json()).profile).toEqual(savedProfile);
|
|
const sorted = latency.map(row => row.ms).sort((a, b) => a - b);
|
|
await info.attach('phase9-sustained-performance', { body: JSON.stringify({ durationMs, fixtureJobs: 250, fixtureCareerRoles: 8,
|
|
readConcurrency: 2, reads: latency.length, p50Ms: sorted[Math.floor(sorted.length * .5)], p95Ms: sorted[Math.floor(sorted.length * .95)], maxMs: sorted.at(-1),
|
|
environment: { platform: os.platform(), cpus: os.cpus().length, totalMemoryBytes: os.totalmem(), browser: context.browser()?.version(), database: 'isolated SQLite', frontend: 'production static export' },
|
|
latency, navigation, limits: 'Finite paced two-reader workload plus 20 native navigations; no SLA/capacity, physical mobile, deployed Linux, model inference or server CPU attribution.' }, null, 2), contentType: 'application/json' });
|
|
} finally { await cdp.detach(); await admin.dispose(); }
|
|
});
|