feat: add evidence-led project media layouts

This commit is contained in:
2026-09-04 21:18:50 +03:00
parent 242d79599a
commit 9cec17e139
9 changed files with 352 additions and 15 deletions

View File

@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { productionInputsKey } from '@/app';
import AccessibleVideo from '@/components/AccessibleVideo.vue';
import ProjectMedia from '@/components/ProjectMedia.vue';
import ProjectMediaGallery from '@/components/ProjectMediaGallery.vue';
import { useLazyMedia } from '@/composables/useLazyMedia';
import { parsePortfolioContent, parsePublicAssetManifest } from '@/content/schemas';
import type { MediaPresentation, ProductionInputs, PublicAssetManifest } from '@/content/types';
@@ -21,6 +22,11 @@ const manifest: PublicAssetManifest = {
width: 1000, height: 700, durationMs: null, rightsHolder: 'Luca Pîslaru',
license: 'All rights reserved', attribution: 'Luca Pîslaru',
},
{
id: 'project-portrait', fileName: 'project-portrait.webp', sha256: sha, mediaType: 'image/webp',
width: 800, height: 1200, durationMs: null, rightsHolder: 'Luca Pîslaru',
license: 'All rights reserved', attribution: 'Luca Pîslaru',
},
{
id: 'project-loop', fileName: 'project-loop.webm', sha256: sha, mediaType: 'video/webm',
width: 1280, height: 720, durationMs: 5_000, rightsHolder: 'Luca Pîslaru',
@@ -83,6 +89,11 @@ const mountMedia = (presentation: MediaPresentation, allowAutomaticVideo = false
global: { provide: { [productionInputsKey as symbol]: injectedInputs } },
});
const mountGallery = (media: readonly MediaPresentation[], layout: 'single' | 'portrait-strip' | 'chart-pair') => mount(ProjectMediaGallery, {
props: { media, layout },
global: { provide: { [productionInputsKey as symbol]: injectedInputs } },
});
describe('ProjectMedia', () => {
it('resolves only manifest assets and keeps contextual wording in content', () => {
const wrapper = mountMedia({
@@ -214,6 +225,52 @@ describe('ProjectMedia', () => {
});
});
describe('ProjectMediaGallery', () => {
it('keeps three portrait images lazy, contextual, and in their supplied order', () => {
const wrapper = mountGallery([
{ kind: 'image', assetId: 'project-image', alt: 'Synthetic profile screen.', caption: 'Fictional demo profile.' },
{ kind: 'image', assetId: 'project-chart', alt: 'Synthetic chart screen.', caption: 'Fictional demo chart.' },
{ kind: 'image', assetId: 'project-portrait', alt: 'Synthetic portrait screen.', caption: 'Fictional demo profile settings.' },
], 'portrait-strip');
expect(wrapper.attributes('data-media-layout')).toBe('portrait-strip');
expect(wrapper.findAll('figure')).toHaveLength(3);
expect(wrapper.findAll('img').map((image) => image.attributes('loading'))).toEqual(['lazy', 'lazy', 'lazy']);
expect(wrapper.text()).toContain('Fictional demo profile');
});
it('preserves image and chart media semantics across single and chart-pair layouts', () => {
const single = mountGallery([
{ kind: 'image', assetId: 'project-image', alt: 'Synthetic application screen.', caption: 'Fictional demo profile.' },
], 'single');
const chartPair = mountGallery([
{
kind: 'chart', assetId: 'project-chart', alt: 'Synthetic trend chart.', caption: 'Fictional demo chart.',
chart: {
variables: 'Horizontal: month. Vertical: completion rate.',
trend: 'The modeled rate rises.',
conclusion: 'The modeled run finishes above its starting point.',
},
},
{ kind: 'image', assetId: 'project-image', alt: 'Synthetic supporting screen.', caption: 'Fictional supporting screen.' },
], 'chart-pair');
expect(single.attributes('data-media-layout')).toBe('single');
expect(single.get('img').attributes('loading')).toBe('lazy');
expect(single.get('figcaption').text()).toBe('Fictional demo profile.');
expect(chartPair.attributes('data-media-layout')).toBe('chart-pair');
expect(chartPair.findAll('figure')).toHaveLength(2);
expect(chartPair.get('[data-testid="chart-variables"]').text()).toContain('completion rate');
expect(chartPair.get('img').attributes('loading')).toBe('lazy');
});
it('renders no gallery container for an empty media list', () => {
const wrapper = mountGallery([], 'single');
expect(wrapper.find('.project-media-gallery').exists()).toBe(false);
});
});
describe('useLazyMedia', () => {
it('fails closed without an observer and supports an explicit request', async () => {
vi.stubGlobal('IntersectionObserver', undefined);