feat(ui): redesign personal dashboard presentation
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { expect, test, type APIRequestContext, type Page, type TestInfo } from '@playwright/test';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { translations } from '../src/i18n/translations';
|
||||
|
||||
const apiUrl = 'http://localhost:5302/api';
|
||||
const password = 'E2ePassword123!';
|
||||
async function login(api: APIRequestContext, email: string) {
|
||||
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
||||
const me = await (await api.get(`${apiUrl}/auth/me`)).json();
|
||||
const csrf = (await api.storageState()).cookies.find(c => c.name === 'XSRF-TOKEN')!.value;
|
||||
return { me, headers: { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string } };
|
||||
}
|
||||
async function capture(page: Page, info: TestInfo, name: string) {
|
||||
const path = info.outputPath(`${name}.png`); await page.screenshot({ path, fullPage: true });
|
||||
await info.attach(name, { path, contentType: 'image/png' });
|
||||
}
|
||||
|
||||
test('[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsive presentation', async ({ page, playwright }, info) => {
|
||||
test.setTimeout(240000);
|
||||
const admin = await playwright.request.newContext();
|
||||
const { headers: adminHeaders } = await login(admin, 'e2e@example.test');
|
||||
const email = `dashboard-${Date.now()}@example.test`;
|
||||
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
|
||||
const { headers } = await login(page.request, email);
|
||||
await page.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort());
|
||||
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys teknologi og fellestjenester æøå', source: 'NAV', location: 'Tromsø' } });
|
||||
expect(companyResponse.status()).toBe(201);
|
||||
const companyId = (await companyResponse.json()).id;
|
||||
const now = new Date();
|
||||
for (let index = 0; index < 8; index++) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - index % 3, 3).toISOString();
|
||||
const data = { companyId, jobTitle: `Plattformutvikler og rådgiver ${index}`, status: ['Applied', 'Waiting', 'Interview', 'Offer', 'Rejected', 'Faglig vurdering', 'Saved', 'Preparing'][index], dateApplied: index < 6 ? date : null, tags: 'TypeScript, Azure', salaryMin: 600000, salaryMax: 800000, salaryCurrency: 'NOK', salaryPeriod: 'year' };
|
||||
const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data });
|
||||
expect(created.status()).toBe(201);
|
||||
if (index === 2 || index === 3) {
|
||||
const id = (await created.json()).id;
|
||||
expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { ...data, responseReceived: true, responseDate: date } })).status()).toBe(204);
|
||||
}
|
||||
}
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto('/dashboard');
|
||||
await expect(page.getByRole('link', { name: 'Nordlys teknologi og fellestjenester æøå', exact: true })).toBeVisible();
|
||||
const dismiss = page.getByRole('button', { name: 'Dismiss setup checklist' });
|
||||
if (await dismiss.isVisible()) await dismiss.click();
|
||||
await capture(page, info, 'dashboard-populated-light-en');
|
||||
const stats = await (await page.request.get(`${apiUrl}/jobapplications/stats`)).json();
|
||||
expect(stats.active).toBe(8);
|
||||
const monthly = await (await page.request.get(`${apiUrl}/jobapplications/analytics?months=12`)).json();
|
||||
const applied = monthly.reduce((sum: number, point: { applied: number }) => sum + point.applied, 0);
|
||||
const responses = monthly.reduce((sum: number, point: { responses: number }) => sum + point.responses, 0);
|
||||
await expect(page.getByRole('link', { name: 'Tracked jobs: 8' })).toBeVisible();
|
||||
await expect(page.getByText(`${responses} / ${applied} recorded responses`, { exact: false })).toBeVisible();
|
||||
const activity = page.getByRole('region', { name: 'Application activity' });
|
||||
const monthlyControl = activity.getByText('View monthly values');
|
||||
await monthlyControl.focus(); await page.keyboard.press('Enter');
|
||||
await expect(activity.getByRole('table')).toBeVisible();
|
||||
await expect(monthlyControl).toBeFocused();
|
||||
const ring = await monthlyControl.evaluate(element => getComputedStyle(element).outlineWidth);
|
||||
expect(parseFloat(ring)).toBeGreaterThanOrEqual(3);
|
||||
await capture(page, info, 'dashboard-monthly-values-keyboard');
|
||||
for (const point of monthly) {
|
||||
const month = new Intl.DateTimeFormat('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${point.month}-01T12:00:00Z`));
|
||||
await expect(activity.getByRole('row', { name: `${month} ${point.applied} ${point.responses}`, exact: true })).toBeVisible();
|
||||
}
|
||||
await activity.getByText('View monthly values').click();
|
||||
await page.getByText('Recorded status breakdown', { exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Faglig vurdering', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/jobs\?status=Faglig\+vurdering/);
|
||||
await expect(page.locator('[data-job-row-id]')).toHaveCount(1);
|
||||
await page.goBack();
|
||||
await page.getByRole('link', { name: 'Nordlys teknologi og fellestjenester æøå', exact: true }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/jobs\\?companyId=${companyId}`));
|
||||
await expect(page.locator('[data-job-row-id]')).toHaveCount(8);
|
||||
await page.goBack();
|
||||
|
||||
for (const mode of ['light', 'dark'] as const) for (const language of ['en', 'nb'] as const) {
|
||||
const t = translations[language];
|
||||
await page.evaluate(({ mode, language }) => { localStorage.setItem('jobtracker.themeMode', mode); localStorage.setItem('uiLanguage', language); }, { mode, language });
|
||||
// Configure the native preference before mounting the next visual fixture.
|
||||
await page.goto('about:blank');
|
||||
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
|
||||
await page.goto('/dashboard');
|
||||
await expect(page.getByText(t.dashboardTrackedJobs, { exact: true })).toBeVisible();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', mode);
|
||||
for (const [width, height, name] of [[1440, 900, 'desktop'], [390, 844, 'phone']] as const) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
|
||||
await capture(page, info, `dashboard-${name}-${mode}-${language}`);
|
||||
}
|
||||
const customize = page.getByRole('button', { name: t.dashboardCustomize, exact: true });
|
||||
await customize.focus(); await page.keyboard.press('Enter');
|
||||
await expect(page.getByRole('menuitemcheckbox').first()).toBeFocused();
|
||||
if (language === 'en') {
|
||||
const menuScan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
|
||||
await info.attach(`dashboard-${mode}-menu-axe`, { body: JSON.stringify({ violations: menuScan.violations, incomplete: menuScan.incomplete }), contentType: 'application/json' });
|
||||
expect(menuScan.violations).toEqual([]);
|
||||
}
|
||||
await page.keyboard.press('Escape'); await expect(customize).toBeFocused();
|
||||
await expect(page.locator('[role="menuitemcheckbox"]')).toHaveCount(0);
|
||||
if (language === 'en') {
|
||||
const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
|
||||
await info.attach(`dashboard-${mode}-axe`, { body: JSON.stringify({ violations: result.violations, incomplete: result.incomplete }), contentType: 'application/json' });
|
||||
expect(result.violations).toEqual([]);
|
||||
}
|
||||
}
|
||||
for (const [width, height] of [[320, 740], [768, 1024], [1024, 768], [1920, 1080]]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
|
||||
await capture(page, info, `dashboard-${width}-dark-nb`);
|
||||
}
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: 'en' } })).status()).toBe(204);
|
||||
await page.evaluate(() => localStorage.setItem('uiLanguage', 'en')); await page.reload();
|
||||
await page.getByRole('button', { name: 'Customize dashboard' }).click();
|
||||
for (const item of await page.getByRole('menuitemcheckbox').all()) await item.click();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible();
|
||||
for (const name of ['Application activity', 'Current pipeline', 'Top companies by activity', 'Job tags']) await expect(page.getByRole('region', { name })).toHaveCount(0);
|
||||
await capture(page, info, 'dashboard-all-hidden');
|
||||
|
||||
const emailB = `dashboard-b-${Date.now()}@example.test`;
|
||||
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email: emailB, password, roles: [] } })).status()).toBe(200);
|
||||
await page.getByRole('button', { name: 'User', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'Sign out', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/login\?sessionChanged=1$/);
|
||||
await page.getByLabel('Username or email', { exact: true }).fill(emailB);
|
||||
await page.getByLabel('Current password', { exact: true }).fill(password);
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
await expect(page.getByText('Your search starts here')).toBeVisible();
|
||||
await expect(page.getByText('Your dashboard sections are hidden.')).toHaveCount(0);
|
||||
await expect(page.getByText('Nordlys teknologi og fellestjenester æøå', { exact: true })).toHaveCount(0);
|
||||
await capture(page, info, 'dashboard-new-account');
|
||||
await page.getByRole('button', { name: 'Customize dashboard' }).click();
|
||||
for (const item of await page.getByRole('menuitemcheckbox').all()) await expect(item).toHaveAttribute('aria-checked', 'true');
|
||||
await page.keyboard.press('Escape');
|
||||
const b = await login(page.request, emailB);
|
||||
const bCompany = await (await page.request.post(`${apiUrl}/companies`, { headers: b.headers, data: { name: 'New search' } })).json();
|
||||
expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: b.headers, data: { companyId: bCompany.id, jobTitle: 'First prospect', status: 'Saved' } })).status()).toBe(201);
|
||||
await page.reload();
|
||||
await expect(page.getByRole('link', { name: 'Tracked jobs: 1' })).toBeVisible();
|
||||
await expect(page.getByText('No applications or dated responses in this period.')).toBeVisible();
|
||||
await capture(page, info, 'dashboard-one-prospect-zero-activity');
|
||||
await page.getByRole('button', { name: 'User', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'Sign out', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/login\?sessionChanged=1$/);
|
||||
await page.getByLabel('Username or email', { exact: true }).fill(email);
|
||||
await page.getByLabel('Current password', { exact: true }).fill(password);
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||
await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible();
|
||||
await expect(page.getByText('First prospect', { exact: true })).toHaveCount(0);
|
||||
await admin.dispose();
|
||||
});
|
||||
|
||||
test('[PRES-S01 E08] Dashboard loading and independent failed resources retain retry and exact values', async ({ page }, info) => {
|
||||
await login(page.request, 'e2e@example.test');
|
||||
let failSummary = false;
|
||||
let failTrends = true;
|
||||
let release: (() => void) | undefined;
|
||||
const hold = new Promise<void>(resolve => { release = resolve; });
|
||||
await page.route(`${apiUrl}/jobapplications/stats`, async route => {
|
||||
await hold;
|
||||
return failSummary ? route.fulfill({ status: 503, json: { message: 'Summary unavailable' } }) : route.fulfill({ json: { total: 4, active: 4, deleted: 0, byStatus: { Applied: 4 }, appliedLast30Days: 4 } });
|
||||
});
|
||||
await page.route(`${apiUrl}/jobapplications/analytics-overview`, route => route.fulfill({ json: { funnel: [{ label: 'Applied', count: 4 }], topCompanies: [], responseRateBySource: [], totalActive: 4, totalResponses: 1 } }));
|
||||
await page.route(`${apiUrl}/jobapplications/analytics?*`, async route => {
|
||||
await hold;
|
||||
return failTrends ? route.fulfill({ status: 503, json: { message: 'Trends unavailable' } }) : route.fulfill({ json: [{ month: '2026-08', applied: 4, responses: 1 }] });
|
||||
});
|
||||
await page.goto('/dashboard');
|
||||
await expect(page.getByRole('progressbar').first()).toBeVisible();
|
||||
await capture(page, info, 'dashboard-loading');
|
||||
release!();
|
||||
await expect(page.getByText('Unable to load dashboard trends')).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Tracked jobs: 4' })).toBeVisible();
|
||||
await capture(page, info, 'dashboard-trends-error');
|
||||
failTrends = false;
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(page.getByText('1 / 4 recorded responses', { exact: false })).toBeVisible();
|
||||
failSummary = true; await page.reload();
|
||||
await expect(page.getByText('Unable to load dashboard summary')).toBeVisible();
|
||||
await expect(page.getByRole('region', { name: 'Application activity' })).toBeVisible();
|
||||
await capture(page, info, 'dashboard-summary-error');
|
||||
failSummary = false;
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(page.getByRole('link', { name: 'Tracked jobs: 4' })).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user