style(ui): reduce initial dashboard gaps and flatten discovery rows
This commit is contained in:
@@ -4,12 +4,18 @@ import AxeBuilder from '@axe-core/playwright';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence/dashboard/browser');
|
||||
const visualMatrix = process.env.A3_VISUAL_MATRIX === '1';
|
||||
const widths = process.env.A3_PILOT ? [320] : visualMatrix ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440];
|
||||
const languages = process.env.A3_PILOT ? ['en'] : ['en', 'nb'];
|
||||
const modes = process.env.A3_PILOT ? ['light'] : ['light', 'dark'];
|
||||
const expectedJourneys = widths.length * languages.length * modes.length;
|
||||
const out = path.resolve(import.meta.dirname, visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/dashboard' : '../../docs/ui-modernisation/evidence/dashboard/browser');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], blockedExternal: [] };
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
for (const width of [320, 390, 768, 1440]) for (const language of ['en', 'nb']) for (const mode of ['light', 'dark']) {
|
||||
for (const width of widths) for (const language of languages) for (const mode of modes) {
|
||||
console.log(`Checking ${width}/${language}/${mode}`);
|
||||
const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' });
|
||||
const page = await context.newPage(); page.on('pageerror', error => report.errors.push(error.message));
|
||||
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
|
||||
@@ -58,7 +64,7 @@ try {
|
||||
assert.equal(await week.locator('time').first().getAttribute('datetime'), deadline);
|
||||
assert.equal(await week.getByRole('link', { name: 'Community nurse' }).count(), 0);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false);
|
||||
if (width === 1440) await page.waitForFunction(() => {
|
||||
if (width >= 1440) await page.waitForFunction(() => {
|
||||
const a = document.querySelector('[data-dashboard-widget=agenda]').getBoundingClientRect(), b = document.querySelector('[data-dashboard-widget=funnel]').getBoundingClientRect();
|
||||
return Math.abs(a.top - b.top) < 2 && a.left < b.left;
|
||||
});
|
||||
@@ -94,9 +100,11 @@ try {
|
||||
}
|
||||
assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []);
|
||||
assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
|
||||
assert.equal(report.mockedWrites.length, 16);
|
||||
console.log('PASS: 16 dashboard journeys,32 scoped axe sweeps,32 captures; controlled API persistence only.');
|
||||
assert.equal(report.mockedWrites.length, expectedJourneys);
|
||||
assert.equal(report.journeys.length, expectedJourneys);
|
||||
assert.equal(report.screens.length, expectedJourneys * 2);
|
||||
console.log(`PASS:${report.journeys.length} dashboard journeys,${report.screens.length} scoped axe sweeps/captures; controlled API persistence only.`);
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(out, 'BROWSER.json'), JSON.stringify({ ...report, limits: ['synthetic API fixtures, not real backend end-to-end','no native device/screen reader/model/provider/security/release qualification'] }, null, 2));
|
||||
fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['synthetic API fixtures, not real backend end-to-end','no native device/screen reader/model/provider/security/release qualification'] }, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
@@ -4,12 +4,18 @@ import AxeBuilder from '@axe-core/playwright';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence/discovery/browser');
|
||||
const visualMatrix = process.env.A3_VISUAL_MATRIX === '1';
|
||||
const widths = process.env.A3_PILOT ? [320] : visualMatrix ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440];
|
||||
const languages = process.env.A3_PILOT ? ['en'] : ['en', 'nb'];
|
||||
const modes = process.env.A3_PILOT ? ['light'] : ['light', 'dark'];
|
||||
const expectedJourneys = widths.length * languages.length * modes.length;
|
||||
const out = path.resolve(import.meta.dirname, visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/discovery' : '../../docs/ui-modernisation/evidence/discovery/browser');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], blockedExternal: [] };
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
for (const width of [320, 390, 768, 1440]) for (const language of ['en', 'nb']) for (const mode of ['light', 'dark']) {
|
||||
for (const width of widths) for (const language of languages) for (const mode of modes) {
|
||||
console.log(`Checking ${width}/${language}/${mode}`);
|
||||
const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' });
|
||||
const page = await context.newPage(); page.on('pageerror', error => report.errors.push(error.message));
|
||||
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
|
||||
@@ -50,7 +56,7 @@ try {
|
||||
const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze();
|
||||
report.screens.push({ width, language, mode, state, violations: result.violations }); assert.deepEqual(result.violations, []);
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: true });
|
||||
await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: width >= 768 });
|
||||
};
|
||||
await page.goto('http://localhost:4338/discover');
|
||||
const filters = page.getByRole('complementary', { name: language === 'en' ? 'Search filters' : 'Søkefiltre' }); await filters.waitFor();
|
||||
@@ -84,11 +90,11 @@ try {
|
||||
report.journeys.push({ width, language, mode, searchSavePauseRunDismissConfirmedDelete: 'PASS' });
|
||||
await context.close();
|
||||
}
|
||||
assert.equal(report.screens.length, 48); assert.equal(report.journeys.length, 16); assert.equal(report.mockedWrites.length, 80);
|
||||
assert.equal(report.screens.length, expectedJourneys * 3); assert.equal(report.journeys.length, expectedJourneys); assert.equal(report.mockedWrites.length, expectedJourneys * 5);
|
||||
assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []);
|
||||
assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
|
||||
console.log('PASS:16 controlled discovery journeys,48 axe scans/captures,80 intercepted fixture mutations.');
|
||||
console.log(`PASS:${report.journeys.length} controlled discovery journeys,${report.screens.length} axe scans/captures,${report.mockedWrites.length} intercepted fixture mutations.`);
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(out, 'BROWSER.json'), JSON.stringify({ ...report, limits: ['synthetic provider/API fixtures, not live NAV or backend qualification', 'no new providers/geography or native/security/release acceptance'] }, null, 2));
|
||||
fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['synthetic provider/API fixtures, not live NAV or backend qualification', 'no new providers/geography or native/security/release acceptance'] }, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,21 @@ test('default catalogue preserves every section and supported widths', () => {
|
||||
expect(defaultDashboardLayout().widgets[0].visible).toBe(true);
|
||||
});
|
||||
|
||||
test('initial dashboard keeps every widget in a denser bounded layout without resizing saved geometry', () => {
|
||||
const fresh = defaultDashboardLayout();
|
||||
expect(fresh.widgets).toHaveLength(10);
|
||||
expect(fresh.widgets.every(widget => widget.visible)).toBe(true);
|
||||
expect(Math.max(...fresh.widgets.map(widget => widget.y + widget.h))).toBeLessThanOrEqual(42);
|
||||
const saved = { version: 2, revision: 4, widgets: [
|
||||
{ id: 'sources', visible: true, size: 'standard', x: 0, y: 14, w: 6, h: 8 },
|
||||
{ id: 'salary', visible: false, size: 'full', x: 0, y: 26, w: 12, h: 12 },
|
||||
] };
|
||||
const normalized = normalizeDashboardLayout(saved);
|
||||
expect(normalized.revision).toBe(4);
|
||||
expect(normalized.widgets.slice(0, 2)).toEqual(saved.widgets);
|
||||
expect(normalized.widgets.slice(2).every(widget => !widget.visible)).toBe(true);
|
||||
});
|
||||
|
||||
test.each([1, 2])('new schema %s preferences use task-first A1 defaults without omitting widgets', version => {
|
||||
const layout = normalizeDashboardLayout({ version, revision: 0, widgets: [] });
|
||||
expect(layout.widgets[0]).toMatchObject({ id: 'attention', visible: true, x: 0, y: 0, w: 12 });
|
||||
|
||||
@@ -34,7 +34,8 @@ export function defaultDashboardLayout(): DashboardLayout {
|
||||
return { version: 2, revision: 0, widgets: arrangeDashboardWidgets(order.map(id => {
|
||||
const definition = WIDGETS.find(widget => widget.id === id)!;
|
||||
const w = id === 'attention' ? 12 : id === 'agenda' ? 8 : id === 'funnel' ? 4 : definition.defaultSize === 'full' ? 12 : 6;
|
||||
return { id, visible: true, size: w === 12 ? 'full' : 'standard', x: 0, y: 0, w, h: id === 'cards' ? 5 : id === 'attention' ? 9 : 8 };
|
||||
const h = id === 'cards' ? 5 : id === 'attention' ? 9 : ['stageAge', 'sources', 'salary', 'companies', 'skills'].includes(id) ? 4 : 8;
|
||||
return { id, visible: true, size: w === 12 ? 'full' : 'standard', x: 0, y: 0, w, h };
|
||||
})) };
|
||||
}
|
||||
|
||||
|
||||
@@ -192,8 +192,8 @@ export default function JobDiscoveryPage() {
|
||||
<TextField label={t("jobDiscoveryMunicipality")} value={location} disabled={busy} onChange={(event) => setLocation(event.target.value)} />
|
||||
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress aria-hidden="true" size={18} color="inherit" /> : <SearchIcon />} disabled={busy}>{t("jobDiscoverySearchNav")}</Button>
|
||||
</Box>
|
||||
<Card variant="outlined" aria-busy={savedSearchState === "loading"}>
|
||||
<CardContent sx={{ display: "grid", gap: 1.5, p: "var(--app-card-padding)", "&:last-child": { pb: "var(--app-card-padding)" } }}>
|
||||
<Box aria-busy={savedSearchState === "loading"}>
|
||||
<Box sx={{ display: "grid", gap: 1.5 }}>
|
||||
<Box>
|
||||
<Typography component="h2" variant="sectionTitle">{t("jobDiscoverySavedSearches")}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{t("jobDiscoverySavedSearchesHelp")}</Typography>
|
||||
@@ -226,8 +226,8 @@ export default function JobDiscoveryPage() {
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box role="region" aria-label={t("jobDiscoveryResults")} tabIndex={-1} ref={resultsRef} aria-busy={loading} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
<PageHeader level="h2" title={t("jobDiscoveryResults")} actions={<Button disabled={busy} onClick={() => queryRef.current?.focus()}>{t("jobDiscoveryReturnFilters")}</Button>} />
|
||||
@@ -252,8 +252,8 @@ export default function JobDiscoveryPage() {
|
||||
) : null}
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gap: 2 }}>
|
||||
{sortedJobs.map((job) => (
|
||||
<Card key={job.id} component="article" aria-label={job.title} variant="outlined" sx={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
<CardContent sx={{ p: "var(--app-card-padding)" }}>
|
||||
<Card key={job.id} component="article" aria-label={job.title} variant="outlined" sx={{ display: "flex", flexDirection: "column", minWidth: 0, border: 0, borderBottom: 1, borderColor: "divider", borderRadius: 0, bgcolor: "transparent", overflow: "visible" }}>
|
||||
<CardContent sx={{ px: 0, py: 1.5 }}>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1, mb: 1 }}>
|
||||
<Chip label={job.sourceName || job.source.toUpperCase()} size="small" color="primary" variant="outlined" />
|
||||
{job.isNew ? <Chip label={t("jobDiscoveryNewResult")} size="small" color="success" /> : null}
|
||||
@@ -270,7 +270,7 @@ export default function JobDiscoveryPage() {
|
||||
<Typography variant="caption" color="text.secondary">{t("jobDiscoveryWorkArrangementMissing")}</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<CardActions sx={{ mt: "auto", p: "var(--app-card-padding)", pt: 0, flexWrap: "wrap", gap: 1, "& .MuiButton-root": { ml: 0, whiteSpace: "normal", maxWidth: "100%" } }}>
|
||||
<CardActions sx={{ mt: "auto", px: 0, pt: 0, pb: 1.5, flexWrap: "wrap", gap: 1, "& .MuiButton-root": { ml: 0, whiteSpace: "normal", maxWidth: "100%" } }}>
|
||||
<Button href={job.url} target="_blank" rel="noreferrer">{t("jobDiscoveryViewListing")}</Button>
|
||||
<Button startIcon={<AddIcon />} onClick={() => navigate("/jobs?add=" + encodeURIComponent(job.url))}>{t("jobDiscoverySaveTracker")}</Button>
|
||||
{activeSavedSearchId !== null ? <Button color="inherit" disabled={busy} onClick={() => void dismissJob(job.id)}>{t("jobDiscoveryDismiss")}</Button> : null}
|
||||
|
||||
Reference in New Issue
Block a user