feat(ui): migrate recorded offer comparison
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect, test as base, type APIRequestContext, type APIResponse, type Locator, type Page, type Response, type TestInfo } from '@playwright/test';
|
||||
import { translations } from '../src/i18n/translations';
|
||||
import type { JobApplication } from '../src/types';
|
||||
|
||||
const apiUrl = 'http://localhost:5302/api';
|
||||
const jobsUrl = `${apiUrl}/jobapplications`;
|
||||
const password = 'E2ePassword123!';
|
||||
type Owner = { id: string; headers: Record<string, string> };
|
||||
type Sample = { language: 'en' | 'nb'; theme: 'light' | 'dark'; width: number; height: number };
|
||||
type PagedJobs = { items: JobApplication[]; total: number; page: number; pageSize: number };
|
||||
type RequestRecord = { method: string; path: string; query: Record<string, string> };
|
||||
type Session = { owner: Owner; admin: APIRequestContext; administrator: Owner; requests: RequestRecord[] };
|
||||
const desktop: Sample = { language: 'en', theme: 'light', width: 1440, height: 900 };
|
||||
const params = { status: 'Offer', page: '1', pageSize: '25', sortBy: 'dateApplied', sortDir: 'asc' };
|
||||
const boundary = 'Offers, companies, non-Offer/deleted/foreign rows, dates and salary facts are created through the actual owner HTTP API in disposable isolated SQLite. Free account entitlements prohibit model use; the worker is disabled by the native configuration. Controlled GET failure/hold establish presentation and explicit recovery only. One explicitly marked legacy salary-period projection derives from an actual owned GET because the current write API normalizes unknown periods to null. No provider, model, external service, raw SQL, schema write, decision deadline, paging feature or salary conversion is introduced. Browser records exclude credentials and session/CSRF headers.';
|
||||
|
||||
async function json<T>(response: APIResponse | Response, status = 200): Promise<T> {
|
||||
expect(response.status(), `${response.url()} returns ${status}`).toBe(status);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function login(api: APIRequestContext, email: string): Promise<Owner> {
|
||||
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
||||
const me = await json<{ id: string; sessionContext: string }>(await api.get(`${apiUrl}/auth/me`));
|
||||
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN');
|
||||
expect(csrf).toBeDefined();
|
||||
return { id: me.id, headers: { 'X-CSRF-TOKEN': csrf!.value, 'X-Session-Context': me.sessionContext } };
|
||||
}
|
||||
|
||||
async function account(admin: APIRequestContext, administrator: Owner) {
|
||||
const email = `p7-offers-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
|
||||
expect((await admin.post(`${apiUrl}/users`, { headers: administrator.headers, data: { email, password, roles: [] } })).status()).toBe(200);
|
||||
return email;
|
||||
}
|
||||
|
||||
const test = base.extend<{ session: Session }>({
|
||||
session: async ({ page, context, playwright }, provide, info) => {
|
||||
expect(process.env.PLAYWRIGHT_REUSE_SERVERS).not.toBe('true');
|
||||
expect(info.config.metadata.isolatedDataRoot).toEqual(expect.any(String));
|
||||
const admin = await playwright.request.newContext();
|
||||
const requests: RequestRecord[] = [];
|
||||
const blockedExternalRequests: string[] = [];
|
||||
page.on('request', request => {
|
||||
const url = new URL(request.url());
|
||||
if (!/^\/api\/jobapplications(?:\/|$)/.test(url.pathname)) return;
|
||||
requests.push({ method: request.method(), path: url.pathname, query: Object.fromEntries(url.searchParams) });
|
||||
});
|
||||
try {
|
||||
const administrator = await login(admin, 'e2e@example.test');
|
||||
const owner = await login(page.request, await account(admin, administrator));
|
||||
expect((await json<{ entitlements: { ai: boolean } }>(await page.request.get(`${apiUrl}/auth/me`))).entitlements.ai).toBe(false);
|
||||
await context.route('**/*', route => {
|
||||
const url = new URL(route.request().url());
|
||||
if (!['localhost', '127.0.0.1'].includes(url.hostname) && !['data:', 'blob:', 'about:'].includes(url.protocol)) {
|
||||
blockedExternalRequests.push(url.origin + url.pathname);
|
||||
return route.abort();
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
await provide({ owner, admin, administrator, requests });
|
||||
} finally {
|
||||
await info.attach('offers-request-boundary', { body: JSON.stringify({ boundary, requests, blockedExternalRequests }), contentType: 'application/json' });
|
||||
await admin.dispose();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function company(api: APIRequestContext, owner: Owner, name: string) {
|
||||
return json<{ id: number; name: string }>(await api.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name, location: 'Oslo', source: 'User' } }), 201);
|
||||
}
|
||||
|
||||
async function job(api: APIRequestContext, owner: Owner, companyId: number, jobTitle: string, fields: Partial<JobApplication> = {}) {
|
||||
return json<JobApplication>(await api.post(jobsUrl, { headers: owner.headers, data: { companyId, jobTitle, status: 'Offer', ...fields } }), 201);
|
||||
}
|
||||
|
||||
function results(page: Page, language: Sample['language'] = 'en') {
|
||||
return page.getByRole('region', { name: translations[language].offerComparisonTitle, exact: true });
|
||||
}
|
||||
|
||||
function card(page: Page, title: string) { return page.getByRole('article', { name: title, exact: true }); }
|
||||
function fact(article: Locator, label: string) { return article.getByText(label, { exact: true }).locator('..'); }
|
||||
function sampleLabel(language: Sample['language'], shown: number, total: number) {
|
||||
return translations[language].offerComparisonSampleCount.replace('{shown}', String(shown)).replace('{total}', String(total));
|
||||
}
|
||||
function offerRequests(requests: RequestRecord[]) {
|
||||
return requests.filter(request => request.method === 'GET' && request.path === '/api/jobapplications' && request.query.status === 'Offer' && request.query.pageSize === '25');
|
||||
}
|
||||
function hold() { let release!: () => void; const pending = new Promise<void>(resolve => { release = resolve; }); return { pending, release }; }
|
||||
async function keyboard(page: Page, element: Locator) { await element.focus(); await expect(element).toBeFocused(); await page.keyboard.press('Enter'); }
|
||||
|
||||
async function enter(page: Page, owner: Owner, sample: Sample = desktop) {
|
||||
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers: owner.headers,
|
||||
data: { language: sample.language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
|
||||
await page.addInitScript(({ language, theme }) => {
|
||||
if (localStorage.getItem('uiLanguage') === null) localStorage.setItem('uiLanguage', language);
|
||||
if (localStorage.getItem('jobtracker.themeMode') === null) localStorage.setItem('jobtracker.themeMode', theme);
|
||||
}, sample);
|
||||
await page.setViewportSize({ width: sample.width, height: sample.height });
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto('/offers?source=phase7-offers#comparison');
|
||||
const stored = await page.evaluate(() => ({ language: localStorage.getItem('uiLanguage'), theme: localStorage.getItem('jobtracker.themeMode') }));
|
||||
if (stored.language !== sample.language || stored.theme !== sample.theme) {
|
||||
await page.evaluate(({ language, theme }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', theme); }, sample);
|
||||
await page.reload();
|
||||
}
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', sample.language === 'nb' ? 'nb-NO' : 'en');
|
||||
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', sample.theme);
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1);
|
||||
await expect(page.getByRole('heading', { level: 1, name: translations[sample.language].offerComparisonTitle, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { level: 2, name: translations[sample.language].offerComparisonTitle, exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
async function capture(page: Page, info: TestInfo, sample: Sample, state: string) {
|
||||
await page.evaluate(async () => { await document.fonts.ready; await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); });
|
||||
const geometry = await page.evaluate(() => ({ width: innerWidth, height: innerHeight,
|
||||
rootOverflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
|
||||
outsideViewport: Array.from(document.querySelectorAll('body *')).filter(element => element.getClientRects().length > 0
|
||||
&& getComputedStyle(element).visibility !== 'hidden' && !element.closest('[hidden],[aria-hidden="true"]'))
|
||||
.map(element => { const box = element.getBoundingClientRect(); return { tag: element.tagName, role: element.getAttribute('role'), label: element.getAttribute('aria-label'),
|
||||
text: element.textContent?.slice(0, 100), left: box.left, right: box.right, width: box.width }; })
|
||||
.filter(element => element.width > 0 && (element.left < -1 || element.right > innerWidth + 1)).slice(0, 30),
|
||||
}));
|
||||
const axe = await new AxeBuilder({ page }).options({ rules: { 'target-size': { enabled: true } } }).analyze();
|
||||
const name = `p7-offers-${sample.language}-${sample.theme}-${sample.width}-${state}`;
|
||||
await info.attach(`${name}-audit`, { body: JSON.stringify({ sample, state, geometry, boundary, ariaSnapshot: await page.locator('body').ariaSnapshot(),
|
||||
axe: { scope: 'All enabled axe rules plus target-size', violations: axe.violations, incomplete: axe.incomplete,
|
||||
passes: axe.passes.map(rule => ({ id: rule.id, nodes: rule.nodes.length })) },
|
||||
qualifications: ['Finite Chromium states; not all combinations of width, language, theme and state.',
|
||||
'Complete violations and incomplete node vectors are retained; incomplete findings require contextual review.',
|
||||
'Keyboard and axe checks do not constitute human screen-reader, physical touch/software-keyboard, real zoom, production font/photo or release acceptance.'],
|
||||
}), contentType: 'application/json' });
|
||||
await info.attach(name, { body: await page.screenshot({ fullPage: true, animations: 'disabled' }), contentType: 'image/png' });
|
||||
expect(geometry.rootOverflow, `${name} app chrome reflows`).toBe(0);
|
||||
expect(axe.violations, `${name} complete axe violations`).toEqual([]);
|
||||
}
|
||||
|
||||
test('[P7-05 Offers] actual 25-of-27 ordered sample excludes other/deleted/foreign jobs and opens the existing full list and job', async ({ page, playwright, session }, info) => {
|
||||
test.setTimeout(120_000);
|
||||
const source = await company(page.request, session.owner, 'Actual recorded offers');
|
||||
const offers: JobApplication[] = [];
|
||||
for (let index = 0; index < 27; index++) offers.push(await job(page.request, session.owner, source.id, `Recorded offer ${String(index).padStart(2, '0')}`, {
|
||||
dateApplied: new Date(Date.UTC(2026, 0, index + 1, 12)).toISOString(), deadline: new Date(Date.UTC(2027, 0, 27 - index, 12)).toISOString(),
|
||||
}));
|
||||
const other = await job(page.request, session.owner, source.id, 'Applied job is outside Offer sample', { status: 'Applied' });
|
||||
const deleted = await job(page.request, session.owner, source.id, 'Deleted Offer must remain absent');
|
||||
expect((await page.request.delete(`${jobsUrl}/${deleted.id}`, { headers: session.owner.headers })).status()).toBe(204);
|
||||
const foreignApi = await playwright.request.newContext();
|
||||
const anonymous = await playwright.request.newContext();
|
||||
try {
|
||||
const foreign = await login(foreignApi, await account(session.admin, session.administrator));
|
||||
const foreignCompany = await company(foreignApi, foreign, 'Foreign private company');
|
||||
const foreignJob = await job(foreignApi, foreign, foreignCompany.id, 'Foreign private Offer must remain absent');
|
||||
const readback = await json<PagedJobs>(await page.request.get(jobsUrl, { params }));
|
||||
expect(readback).toMatchObject({ total: 27, page: 1, pageSize: 25 });
|
||||
expect(readback.items.map(item => item.id)).toEqual(offers.slice(0, 25).map(item => item.id));
|
||||
const secondPage = await json<PagedJobs>(await page.request.get(jobsUrl, { params: { ...params, page: '2' } }));
|
||||
expect(secondPage.items.map(item => item.id)).toEqual(offers.slice(25).map(item => item.id));
|
||||
expect((await page.request.get(`${jobsUrl}/${foreignJob.id}`)).status()).toBe(404);
|
||||
expect((await anonymous.get(jobsUrl, { params })).status()).toBe(401);
|
||||
await enter(page, session.owner);
|
||||
await expect(results(page).getByText(sampleLabel('en', 25, 27), { exact: true })).toBeVisible();
|
||||
await expect(results(page).getByText(translations.en.offerComparisonSampleHelp, { exact: true })).toBeVisible();
|
||||
await expect(results(page).getByRole('article')).toHaveCount(25);
|
||||
expect(await results(page).getByRole('heading', { level: 3 }).allTextContents()).toEqual(offers.slice(0, 25).map(item => item.jobTitle));
|
||||
for (const excluded of [other, deleted, foreignJob, offers[25], offers[26]]) await expect(card(page, excluded.jobTitle)).toHaveCount(0);
|
||||
expect(offerRequests(session.requests)[0].query).toEqual(params);
|
||||
const fullList = page.waitForResponse(response => {
|
||||
const url = new URL(response.url());
|
||||
return url.pathname === '/api/jobapplications' && url.searchParams.get('status') === 'Offer' && url.searchParams.get('sortBy') === 'dateApplied'
|
||||
&& url.searchParams.get('sortDir') === 'asc';
|
||||
});
|
||||
await keyboard(page, page.getByRole('button', { name: translations.en.offerComparisonAllOffers, exact: true }));
|
||||
await expect(page).toHaveURL(/\/jobs\?status=Offer&sortBy=dateApplied&sortDir=asc$/);
|
||||
expect((await json<PagedJobs>(await fullList)).total).toBe(27);
|
||||
await page.goBack(); await expect(page).toHaveURL(/\/offers\?source=phase7-offers#comparison$/);
|
||||
await expect(card(page, offers[0].jobTitle)).toBeVisible();
|
||||
const detail = page.waitForResponse(response => response.url() === `${jobsUrl}/${offers[0].id}/workspace` && response.request().method() === 'GET');
|
||||
await keyboard(page, card(page, offers[0].jobTitle).getByRole('button', { name: translations.en.workspaceOpenFull, exact: true }));
|
||||
await expect(page).toHaveURL(new RegExp(`/jobs/${offers[0].id}$`));
|
||||
expect(await json<{ id: number; jobTitle: string }>(await detail)).toMatchObject({ id: offers[0].id, jobTitle: offers[0].jobTitle });
|
||||
await page.goBack(); await expect(card(page, offers[0].jobTitle)).toBeVisible();
|
||||
await capture(page, info, desktop, 'actual-sample-and-back');
|
||||
await info.attach('offers-real-http-sample', { body: JSON.stringify({ boundary, ownedOfferIds: offers.map(item => item.id), firstPageIds: readback.items.map(item => item.id),
|
||||
omittedButAvailableIds: secondPage.items.map(item => item.id), excludedOwnedIds: [other.id, deleted.id], foreignJobId: foreignJob.id,
|
||||
qualification: 'The UI displays exactly one existing API page. The second API-page read is fixture evidence, not an added comparison paging feature. Full-list and Back navigation use existing Jobs routes.' }), contentType: 'application/json' });
|
||||
} finally { await foreignApi.dispose(); await anonymous.dispose(); }
|
||||
});
|
||||
|
||||
test('[P7-05 Offers] actual empty, zero/equal/missing/raw salary and recorded date facts remain honest with a qualified legacy period projection', async ({ page, session }, info) => {
|
||||
test.setTimeout(90_000);
|
||||
const source = await company(page.request, session.owner, 'Recorded salary facts');
|
||||
await job(page.request, session.owner, source.id, 'Saved job outside offers', { status: 'Saved' });
|
||||
await enter(page, session.owner);
|
||||
await expect(results(page).getByText(translations.en.offerComparisonEmpty, { exact: true })).toBeVisible();
|
||||
await expect(results(page).getByRole('article')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: translations.en.offerComparisonAllOffers, exact: true })).toHaveCount(0);
|
||||
await capture(page, info, desktop, 'actual-empty');
|
||||
const applied = '2026-01-04T12:00:00Z'; const deadline = '2027-02-05T12:00:00Z';
|
||||
const zero = await job(page.request, session.owner, source.id, 'Recorded zero compensation', { salaryMin: 0, salaryMax: 0, salaryCurrency: 'NOK', salaryPeriod: 'year', dateApplied: applied, deadline });
|
||||
const equal = await job(page.request, session.owner, source.id, 'Recorded equal compensation', { salaryMin: 50_000, salaryMax: 50_000, salaryCurrency: 'EUR', salaryPeriod: 'month' });
|
||||
const missing = await job(page.request, session.owner, source.id, 'Missing salary facts');
|
||||
const raw = await job(page.request, session.owner, source.id, 'Candidate written salary', { salary: 'Negotiable — recorded by candidate' });
|
||||
const legacy = await job(page.request, session.owner, source.id, 'Legacy unknown-period projection', { salaryMin: 70_000, salaryCurrency: 'QXD', salaryPeriod: 'fortnight' });
|
||||
expect(legacy.salaryPeriod).toBeNull();
|
||||
await page.route(`${jobsUrl}?**`, async route => {
|
||||
if (new URL(route.request().url()).searchParams.get('status') !== 'Offer') return route.continue();
|
||||
const response = await route.fetch(); const data: PagedJobs = await response.json();
|
||||
const owned = data.items.find(item => item.id === legacy.id); expect(owned).toBeDefined();
|
||||
owned!.salaryPeriod = 'fortnight';
|
||||
await route.fulfill({ response, json: data });
|
||||
});
|
||||
await page.reload();
|
||||
await expect(card(page, zero.jobTitle)).toBeVisible();
|
||||
const amount = await page.evaluate(() => ({ equal: (50_000).toLocaleString('en'), legacy: (70_000).toLocaleString('en') }));
|
||||
await expect(fact(card(page, zero.jobTitle), translations.en.offerComparisonCompensation)).toContainText('0 – 0 NOK / Year');
|
||||
await expect(fact(card(page, equal.jobTitle), translations.en.offerComparisonCompensation)).toContainText(`${amount.equal} – ${amount.equal} EUR / Month`);
|
||||
await expect(fact(card(page, missing.jobTitle), translations.en.offerComparisonCompensation)).toContainText(translations.en.offerComparisonNotRecorded);
|
||||
await expect(fact(card(page, raw.jobTitle), translations.en.offerComparisonCompensation)).toContainText(raw.salary!);
|
||||
await expect(fact(card(page, legacy.jobTitle), translations.en.offerComparisonCompensation)).toContainText(`${amount.legacy} QXD / fortnight`);
|
||||
const dates = await page.evaluate(({ applied, deadline }) => ({ applied: new Date(applied).toLocaleDateString('en'), deadline: new Date(deadline).toLocaleDateString('en') }), { applied, deadline });
|
||||
await expect(fact(card(page, zero.jobTitle), translations.en.jobTableDateApplied)).toContainText(dates.applied);
|
||||
await expect(fact(card(page, zero.jobTitle), translations.en.offerComparisonDecisionDeadline)).toContainText(dates.deadline);
|
||||
await expect(results(page).getByText('Decision deadline', { exact: true })).toHaveCount(0);
|
||||
await expect(results(page).getByText(sampleLabel('en', 5, 5), { exact: true })).toBeVisible();
|
||||
await expect(results(page).getByText(translations.en.offerComparisonSampleHelp, { exact: true })).toHaveCount(0);
|
||||
await capture(page, info, desktop, 'recorded-salary-with-legacy-projection');
|
||||
await info.attach('offers-legacy-projection-boundary', { body: JSON.stringify({ boundary, id: legacy.id, actualPersistedSalaryPeriod: legacy.salaryPeriod,
|
||||
browserProjectionSalaryPeriod: 'fortnight', qualification: 'Only this legacy field is replaced in an actual owned GET projection. Unknown-period persistence is not claimed; actual API readback remains null. Zero/equal/missing/free-text salaries and recorded dates come from actual HTTP records. Number formatting follows the preserved Offers local formatter and current UI locale. Both recorded bounds remain displayed; recognized periods are localized and unknown periods remain verbatim. No currency conversion is performed.' }), contentType: 'application/json' });
|
||||
expect((await json<JobApplication>(await page.request.get(`${jobsUrl}/${legacy.id}`))).salaryPeriod).toBeNull();
|
||||
});
|
||||
|
||||
test('[P7-05 Offers] held actual reads and controlled failure recover through explicit keyboard Retry without locale refetch', async ({ page, session }, info) => {
|
||||
test.setTimeout(90_000);
|
||||
const source = await company(page.request, session.owner, 'Offers recovery source');
|
||||
const offer = await job(page.request, session.owner, source.id, 'Retained actual recovery offer');
|
||||
const initial = hold(); const retry = hold(); let attempts = 0;
|
||||
await page.route(`${jobsUrl}?**`, async route => {
|
||||
if (new URL(route.request().url()).searchParams.get('status') !== 'Offer') return route.continue();
|
||||
attempts += 1;
|
||||
if (attempts === 1) { await initial.pending; await route.fulfill({ status: 500, json: {} }); }
|
||||
else { await retry.pending; await route.continue(); }
|
||||
});
|
||||
await enter(page, session.owner);
|
||||
await expect.poll(() => attempts).toBe(1);
|
||||
await expect(results(page).getByRole('status')).toContainText(translations.en.loading);
|
||||
await expect(results(page).getByRole('article')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: translations.en.offerComparisonAllOffers, exact: true })).toHaveCount(0);
|
||||
await capture(page, info, desktop, 'controlled-held-initial');
|
||||
initial.release();
|
||||
await expect(results(page).getByRole('alert')).toContainText(translations.en.offerComparisonLoadFailed);
|
||||
await capture(page, info, desktop, 'controlled-load-failure');
|
||||
const preference = page.waitForResponse(response => response.url() === `${apiUrl}/auth/preferences/language` && response.request().method() === 'PUT');
|
||||
await keyboard(page, page.locator('header').getByRole('button', { name: 'Norsk', exact: true }));
|
||||
expect((await preference).status()).toBe(204);
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'nb-NO');
|
||||
await expect(results(page, 'nb').getByRole('alert')).toContainText(translations.nb.offerComparisonLoadFailed);
|
||||
expect(attempts).toBe(1); expect(offerRequests(session.requests)).toHaveLength(1);
|
||||
await keyboard(page, results(page, 'nb').getByRole('button', { name: translations.nb.retry, exact: true }));
|
||||
await expect.poll(() => attempts).toBe(2);
|
||||
await expect(results(page, 'nb')).toBeFocused();
|
||||
await expect(results(page, 'nb').getByRole('status')).toContainText(translations.nb.loading);
|
||||
await expect(results(page, 'nb').getByRole('button', { name: translations.nb.retry, exact: true })).toHaveCount(0);
|
||||
await capture(page, info, { ...desktop, language: 'nb' }, 'controlled-held-explicit-retry');
|
||||
retry.release();
|
||||
await expect(card(page, offer.jobTitle)).toBeVisible();
|
||||
await expect(results(page, 'nb').getByRole('alert')).toHaveCount(0);
|
||||
await expect(results(page, 'nb').getByText(sampleLabel('nb', 1, 1), { exact: true })).toBeVisible();
|
||||
expect(attempts).toBe(2); expect(offerRequests(session.requests).map(request => request.query)).toEqual([params, params]);
|
||||
const english = page.waitForResponse(response => response.url() === `${apiUrl}/auth/preferences/language` && response.request().method() === 'PUT');
|
||||
await keyboard(page, page.locator('header').getByRole('button', { name: 'English', exact: true }));
|
||||
expect((await english).status()).toBe(204);
|
||||
await expect(page.getByRole('heading', { level: 2, name: translations.en.offerComparisonTitle, exact: true })).toBeVisible();
|
||||
expect(attempts).toBe(2); expect(offerRequests(session.requests)).toHaveLength(2);
|
||||
expect((await json<JobApplication>(await page.request.get(`${jobsUrl}/${offer.id}`))).jobTitle).toBe(offer.jobTitle);
|
||||
});
|
||||
|
||||
test('[P7-05 Offers] eight finite bilingual theme widths preserve long recorded facts and reachable keyboard actions', async ({ page, session }, info) => {
|
||||
test.setTimeout(240_000);
|
||||
const source = await company(page.request, session.owner, 'Samfunnskritiske systemer og langsiktige muligheter for nordnorske virksomheter Æ Ø Å');
|
||||
const offer = await job(page.request, session.owner, source.id, 'Senior utvikler med ansvar for samfunnskritiske tjenester og grundig kvalitetssikring Æ Ø Å', {
|
||||
location: 'Tromsø og flere samarbeidende kommuner i Nord-Norge', salaryMin: 850_000, salaryMax: 950_000, salaryCurrency: 'NOK', salaryPeriod: 'year',
|
||||
dateApplied: '2026-01-10T12:00:00Z', deadline: '2027-02-20T12:00:00Z', followUpAt: '2027-02-10T12:00:00Z',
|
||||
nextAction: 'Avklar oppgavene og arbeidstiden med rekruttereren før du vurderer det registrerte tilbudet.',
|
||||
notes: 'Dette er kandidatens egne registrerte notater. Ingen lønnsvurdering eller kunstig svarfrist er lagt til.\nSammenlign de dokumenterte opplysningene og les vilkårene grundig.',
|
||||
});
|
||||
await job(page.request, session.owner, source.id, 'Another recorded offer without compensation facts', { notes: 'Missing facts remain unknown.' });
|
||||
const samples: Sample[] = [
|
||||
{ language: 'en', theme: 'light', width: 320, height: 740 }, { language: 'nb', theme: 'dark', width: 360, height: 844 },
|
||||
{ language: 'en', theme: 'dark', width: 390, height: 844 }, { language: 'nb', theme: 'light', width: 768, height: 1024 },
|
||||
{ language: 'en', theme: 'dark', width: 1024, height: 768 }, { language: 'nb', theme: 'light', width: 1280, height: 320 },
|
||||
{ language: 'en', theme: 'light', width: 1440, height: 900 }, { language: 'nb', theme: 'dark', width: 1920, height: 1080 },
|
||||
];
|
||||
for (const sample of samples) {
|
||||
const t = translations[sample.language];
|
||||
await enter(page, session.owner, sample);
|
||||
await expect(card(page, offer.jobTitle)).toBeVisible();
|
||||
await expect(card(page, offer.jobTitle).getByRole('heading', { level: 3, name: offer.jobTitle, exact: true })).toBeVisible();
|
||||
await expect(card(page, offer.jobTitle)).toContainText(source.name);
|
||||
await expect(fact(card(page, offer.jobTitle), t.offerComparisonNotes)).toContainText(offer.notes!);
|
||||
await expect(results(page, sample.language).getByText(sampleLabel(sample.language, 2, 2), { exact: true })).toBeVisible();
|
||||
const open = card(page, offer.jobTitle).getByRole('button', { name: t.workspaceOpenFull, exact: true });
|
||||
await open.scrollIntoViewIfNeeded(); await open.focus(); await expect(open).toBeFocused();
|
||||
await page.evaluate(() => scrollTo(0, 0));
|
||||
await capture(page, info, sample, 'long-recorded-facts');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user