feat: add evidence-led project media layouts
This commit is contained in:
@@ -27,14 +27,50 @@ type InputValidator = { validateProductionInputs: (content: unknown, manifest: u
|
||||
let vite: ViteDevServer;
|
||||
let renderedPortfolio = '';
|
||||
|
||||
const syntheticProjectMedia = [
|
||||
{ kind: 'image', assetId: 'synthetic-portrait-one', alt: 'Synthetic profile evidence.', caption: 'Fictional demo profile.' },
|
||||
{ kind: 'image', assetId: 'synthetic-portrait-two', alt: 'Synthetic settings evidence.', caption: 'Fictional demo settings.' },
|
||||
{ kind: 'image', assetId: 'synthetic-portrait-three', alt: 'Synthetic schedule evidence.', caption: 'Fictional demo schedule.' },
|
||||
] as const;
|
||||
|
||||
function withSyntheticProjectMedia(contentInput: unknown, manifestInput: unknown): { content: unknown; manifest: unknown } {
|
||||
const content = structuredClone(contentInput) as {
|
||||
projects: { selected: Array<{ assetIds: string[]; media?: unknown; mediaLayout?: unknown }> };
|
||||
};
|
||||
const manifest = structuredClone(manifestInput) as { assets: Array<Record<string, unknown>> };
|
||||
const project = content.projects.selected[0]!;
|
||||
|
||||
project.assetIds = syntheticProjectMedia.map((media) => media.assetId);
|
||||
project.media = syntheticProjectMedia;
|
||||
project.mediaLayout = 'portrait-strip';
|
||||
manifest.assets.push(...syntheticProjectMedia.map((media, index) => ({
|
||||
id: media.assetId,
|
||||
fileName: `${media.assetId}.webp`,
|
||||
sha256: `${index + 1}`.repeat(64),
|
||||
mediaType: 'image/webp',
|
||||
width: 800,
|
||||
height: 1200,
|
||||
durationMs: null,
|
||||
rightsHolder: 'Synthetic test fixture',
|
||||
license: 'Test-only synthetic asset',
|
||||
attribution: 'Synthetic test fixture',
|
||||
})));
|
||||
|
||||
return { content, manifest };
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
vite = await createServer({ server: { middlewareMode: true, hmr: false, ws: false }, appType: 'custom' });
|
||||
const { render } = await vite.ssrLoadModule('/src/entry-server.ts') as ServerRenderer;
|
||||
const { validateProductionInputs } = await vite.ssrLoadModule('/src/content/schemas.ts') as InputValidator;
|
||||
const fixture = (file: string): unknown => JSON.parse(readFileSync(resolve(process.cwd(), file), 'utf8')) as unknown;
|
||||
const inputs = validateProductionInputs(
|
||||
const syntheticFixture = withSyntheticProjectMedia(
|
||||
fixture('content/public/portfolio-content.json'),
|
||||
fixture('content/public/asset-manifest.json'),
|
||||
);
|
||||
const inputs = validateProductionInputs(
|
||||
syntheticFixture.content,
|
||||
syntheticFixture.manifest,
|
||||
fixture('config/site.preview.json'),
|
||||
new URL('file:///prepared-assets/'),
|
||||
);
|
||||
@@ -63,7 +99,7 @@ async function loadInformationLayer(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
async function capture(page: Page, testInfo: TestInfo, name: string): Promise<void> {
|
||||
await page.screenshot({ path: testInfo.outputPath(name), fullPage: true });
|
||||
await page.screenshot({ path: testInfo.outputPath(name) });
|
||||
}
|
||||
|
||||
test('renders the validated eight-section portfolio structure before responsive checks', async ({ page }) => {
|
||||
@@ -128,6 +164,30 @@ test('uses explicit desktop layout families while preserving the primary-project
|
||||
await capture(page, testInfo, 'information-layer-desktop-corridor.png');
|
||||
});
|
||||
|
||||
test('lays out synthetic portrait evidence in a desktop strip and preserves mobile reflow', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await loadInformationLayer(page);
|
||||
|
||||
const gallery = page.locator('.project-media-gallery[data-media-layout="portrait-strip"]');
|
||||
await expect(gallery).toHaveCount(1);
|
||||
const desktopFigures = await gallery.locator('figure').evaluateAll((figures) => figures.map((figure) => {
|
||||
const rect = figure.getBoundingClientRect();
|
||||
return { left: rect.left, width: rect.width };
|
||||
}));
|
||||
expect(desktopFigures).toHaveLength(3);
|
||||
expect(desktopFigures[1]!.left).toBeGreaterThan(desktopFigures[0]!.left);
|
||||
expect(desktopFigures[2]!.left).toBeGreaterThan(desktopFigures[1]!.left);
|
||||
expect(desktopFigures.every((figure) => figure.width < 480)).toBe(true);
|
||||
|
||||
await page.setViewportSize({ width: 320, height: 800 });
|
||||
await loadInformationLayer(page);
|
||||
await expect(gallery).toHaveCount(1);
|
||||
expect(await gallery.evaluate((node) => node.scrollWidth <= node.clientWidth || getComputedStyle(node).overflowX === 'auto')).toBe(true);
|
||||
expect(await gallery.locator('figure').evaluateAll((figures) => figures.map((figure) => figure.textContent)))
|
||||
.toEqual(['Fictional demo profile.', 'Fictional demo settings.', 'Fictional demo schedule.']);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true);
|
||||
});
|
||||
|
||||
test('uses a stable tablet rhythm and a single readable mobile column with usable navigation targets', async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await loadInformationLayer(page);
|
||||
@@ -260,6 +320,6 @@ test('creates a clean print portfolio with a visible label but no resume packagi
|
||||
await expect(page.locator('.print-portfolio-label')).toBeVisible();
|
||||
await expect(page.locator('.portfolio-section').first()).toHaveCSS('break-inside', 'avoid');
|
||||
const printCopy = await page.locator('body').innerText();
|
||||
expect(printCopy).not.toMatch(/résumé|resume/iu);
|
||||
expect(printCopy).not.toMatch(/\b(?:Résumé|Resume)\b/u);
|
||||
await expect(page.locator('a[download]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user