fix: refine chart evidence layouts

This commit is contained in:
2026-09-05 00:22:02 +03:00
parent 2fce0cedee
commit 0f6349507b
4 changed files with 170 additions and 13 deletions

View File

@@ -77,49 +77,70 @@ test('failed project images leave titles, captions, chart explanations, and alt
await expect(page.locator('[data-project-id="currency-converter"] img')).toHaveAttribute('alt', /conversion interface/iu);
});
test('desktop chart descriptions and exact values occupy sequential full-width rows', async ({ page, distUrl }, testInfo) => {
test('desktop chart descriptions keep readable value columns and exact values in sequential full-width rows', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'Desktop chart grid geometry is verified once.');
await page.setViewportSize({ width: 1440, height: 900 });
await page.addInitScript(() => localStorage.setItem('portfolio.motion.maximum-quality', 'static'));
await page.goto(distUrl, { waitUntil: 'networkidle' });
const chart = page.locator('[data-project-id="opinion-dynamics-simulation"] .project-media--chart').first();
const geometry = await chart.evaluate((figure) => {
const charts = page.locator('[data-project-id="opinion-dynamics-simulation"] .project-media--chart');
const geometry = await charts.evaluateAll((figures) => figures.map((figure) => {
const description = figure.querySelector<HTMLElement>('.chart-description')!;
const table = figure.querySelector<HTMLElement>('.chart-values')!;
const table = figure.querySelector<HTMLElement>('.chart-values');
const values = [...description.querySelectorAll<HTMLElement>('dd')];
const figureRect = figure.getBoundingClientRect();
const descriptionStyle = getComputedStyle(description);
const tableStyle = getComputedStyle(table);
const tableStyle = table === null ? null : getComputedStyle(table);
return {
descriptionColumn: descriptionStyle.gridColumnStart,
descriptionRow: descriptionStyle.gridRowStart,
tableColumn: tableStyle.gridColumnStart,
tableRow: tableStyle.gridRowStart,
tableColumn: tableStyle?.gridColumnStart ?? null,
tableRow: tableStyle?.gridRowStart ?? null,
descriptionWidthRatio: description.getBoundingClientRect().width / figureRect.width,
tableWidthRatio: table.getBoundingClientRect().width / figureRect.width,
tableWidthRatio: table === null ? null : table.getBoundingClientRect().width / figureRect.width,
minimumValueWidth: Math.min(...values.map((value) => value.getBoundingClientRect().width)),
descriptionHeight: description.getBoundingClientRect().height,
};
});
}));
const opinionHeight = await page.locator('[data-project-id="opinion-dynamics-simulation"]')
.evaluate((article) => article.getBoundingClientRect().height);
expect(geometry).toEqual({
expect(geometry[0]).toEqual({
descriptionColumn: '1',
descriptionRow: '3',
tableColumn: '1',
tableRow: '4',
descriptionWidthRatio: expect.any(Number),
tableWidthRatio: expect.any(Number),
minimumValueWidth: expect.any(Number),
descriptionHeight: expect.any(Number),
});
expect(geometry.descriptionWidthRatio).toBeGreaterThanOrEqual(0.8);
expect(geometry.tableWidthRatio).toBeGreaterThanOrEqual(0.8);
expect(geometry[1]).toMatchObject({
descriptionColumn: '1',
descriptionRow: '3',
tableColumn: null,
tableRow: null,
});
expect(geometry.every(({ descriptionWidthRatio }) => descriptionWidthRatio >= 0.8)).toBe(true);
expect(geometry[0]!.tableWidthRatio).toBeGreaterThanOrEqual(0.8);
expect.soft(
Math.min(...geometry.map(({ minimumValueWidth }) => minimumValueWidth)),
JSON.stringify(geometry),
).toBeGreaterThanOrEqual(160);
expect.soft(Math.max(...geometry.map(({ descriptionHeight }) => descriptionHeight))).toBeLessThan(600);
expect(opinionHeight).toBeLessThan(3_000);
});
test('print project captions and evidence text retain readable contrast', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'Print contrast is verified once.');
await page.setViewportSize({ width: 1440, height: 900 });
await page.emulateMedia({ media: 'print' });
await page.goto(distUrl, { waitUntil: 'networkidle' });
const opinionCharts = page.locator('[data-project-id="opinion-dynamics-simulation"] .project-media--chart img');
await opinionCharts.first().scrollIntoViewIfNeeded();
await expect.poll(() => opinionCharts.evaluateAll((images) => images.every((image) => (
(image as HTMLImageElement).complete && (image as HTMLImageElement).naturalWidth > 0
)))).toBe(true);
await page.emulateMedia({ media: 'print' });
const result = await page.locator('.project-media').evaluateAll((figures) => {
const channel = (value: number): number => {
@@ -139,12 +160,34 @@ test('print project captions and evidence text retain readable contrast', async
return [...figure.querySelectorAll<HTMLElement>('figcaption, .media-description, .chart-description, .chart-values')]
.map((element) => contrast(getComputedStyle(element).color, background));
});
const chartImages = figures
.filter((figure) => figure.matches('.project-media--chart'))
.map((figure) => figure.querySelector<HTMLImageElement>('img')!);
return {
backgrounds: [...new Set(figures.map((figure) => getComputedStyle(figure).backgroundColor))],
minimumContrast: Math.min(...samples),
chartImages: chartImages.map((image) => {
const rendered = image.getBoundingClientRect();
const renderedAspect = rendered.width / rendered.height;
const intrinsicAspect = Number(image.getAttribute('width')) / Number(image.getAttribute('height'));
return {
background: getComputedStyle(image).backgroundColor,
renderedWidth: rendered.width,
renderedHeight: rendered.height,
renderedAspect,
intrinsicAspect,
aspectError: Math.abs(renderedAspect - intrinsicAspect) / intrinsicAspect,
};
}),
};
});
expect(result.backgrounds).toEqual(['rgb(255, 255, 255)']);
expect(result.minimumContrast).toBeGreaterThanOrEqual(4.5);
expect(result.chartImages).toHaveLength(2);
expect.soft(result.chartImages.every(({ background }) => background === 'rgb(255, 255, 255)')).toBe(true);
expect.soft(
Math.max(...result.chartImages.map(({ aspectError }) => aspectError)),
JSON.stringify(result.chartImages),
).toBeLessThanOrEqual(0.02);
});