// @vitest-environment jsdom import { cleanup, render, screen, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { RecentProjectsStrip } from '../../src/components/RecentProjectsStrip'; import type { Project } from '../../src/types'; vi.mock('../../src/providers/registry', () => ({ fetchProjectFileText: vi.fn(async (projectId: string, name: string) => { if (projectId === 'project-ds' && name === 'brand.json') { return JSON.stringify({ logo: { primary: 'logos/favicon-1.png' }, imagery: { samples: [{ file: 'imagery/cover-0.png', kind: 'cover' }] }, }); } if (projectId === 'project-ds-fallback' && name === 'brand.json') { return JSON.stringify({ logo: { primary: 'logos/favicon-1.png', alternates: ['logos/wordmark.svg'], }, }); } return null; }), fetchProjectFiles: vi.fn(async (projectId: string) => { if (projectId === 'project-ds') { return [ { name: 'favicon-1.png', path: 'logos/favicon-1.png', kind: 'image', mtime: 4 }, { name: 'cover-0.png', path: 'imagery/cover-0.png', kind: 'image', mtime: 3 }, ]; } if (projectId === 'project-ds-fallback') { return [ { name: 'favicon-1.png', path: 'logos/favicon-1.png', kind: 'image', mtime: 4 }, { name: 'wordmark.svg', path: 'logos/wordmark.svg', kind: 'image', mtime: 3 }, ]; } if (projectId === 'project-html') { return [{ name: 'index.html', kind: 'html', mtime: 2 }]; } if (projectId === 'project-deck') { return [{ name: 'index.html', kind: 'html', mtime: 2 }]; } return []; }), projectFileUrl: (projectId: string, fileName: string) => `/api/projects/${projectId}/files/${fileName}`, })); afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); function project(overrides: Partial): Project { return { id: 'project-1', name: 'Project', skillId: null, designSystemId: null, createdAt: 1, updatedAt: 2, status: { value: 'not_started' }, ...overrides, }; } function projects(count: number): Project[] { return Array.from({ length: count }, (_, index) => project({ id: `project-${index + 1}`, name: `Project ${index + 1}`, updatedAt: count - index, }), ); } describe('RecentProjectsStrip', () => { it('shows seven projects when the row has room for a seventh card', async () => { vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function getRect(this: HTMLElement) { return { x: 0, y: 0, width: this.classList.contains('recent-projects__row') ? 1332 : 180, height: 100, top: 0, right: 0, bottom: 0, left: 0, toJSON: () => ({}), }; }); const { container } = render( {}} onViewAll={() => {}} />, ); await waitFor(() => { expect(container.querySelectorAll('.recent-projects__card')).toHaveLength(7); }); }); it('keeps six projects when the row is below the wide-card threshold', () => { vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function getRect(this: HTMLElement) { return { x: 0, y: 0, width: this.classList.contains('recent-projects__row') ? 1331 : 180, height: 100, top: 0, right: 0, bottom: 0, left: 0, toJSON: () => ({}), }; }); const { container } = render( {}} onViewAll={() => {}} />, ); expect(container.querySelectorAll('.recent-projects__card')).toHaveLength(6); }); it('remeasures when projects arrive after the initial empty render', () => { Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1400, }); vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function getRect(this: HTMLElement) { return { x: 0, y: 0, width: this.classList.contains('recent-projects__row') ? 1331 : 180, height: 100, top: 0, right: 0, bottom: 0, left: 0, toJSON: () => ({}), }; }); const { container, rerender } = render( {}} onViewAll={() => {}} />, ); rerender( {}} onViewAll={() => {}} />, ); expect(container.querySelectorAll('.recent-projects__card')).toHaveLength(6); }); it('matches project cards with previews and design-system tags', async () => { const { container } = render( {}} onViewAll={() => {}} />, ); expect(screen.getByText('Design System')).toBeTruthy(); expect(screen.getAllByText('Prototype').length).toBeGreaterThan(0); const designSystemCard = container.querySelector('.recent-projects__card.is-design-system-project'); expect(designSystemCard).toBeTruthy(); expect(designSystemCard?.querySelectorAll('.design-card-tag')).toHaveLength(1); await waitFor(() => { expect(designSystemCard?.querySelector('.recent-projects__card-thumb-image img')).toBeTruthy(); expect(designSystemCard?.querySelector('img')?.getAttribute('src')).toBe( '/api/projects/project-ds/files/imagery/cover-0.png', ); expect(container.querySelector('.recent-projects__card-thumb-html iframe')).toBeNull(); expect(container.querySelector('.recent-projects__card-thumb-html .recent-projects__card-glyph')).toBeTruthy(); }); }); it('uses non-favicon design-system logo alternates when no cover exists', async () => { const { container } = render( {}} onViewAll={() => {}} />, ); const designSystemCard = container.querySelector('.recent-projects__card.is-design-system-project'); await waitFor(() => { expect(designSystemCard?.querySelector('.recent-projects__card-thumb-logo img')).toBeTruthy(); expect(designSystemCard?.querySelector('img')?.getAttribute('src')).toBe( '/api/projects/project-ds-fallback/files/logos/wordmark.svg', ); }); }); it('does not run HTML or deck previews inside recent project cards', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); const { container } = render( {}} onViewAll={() => {}} />, ); const deckCard = container.querySelector('[data-project-id="project-deck"]'); const htmlCard = container.querySelector('[data-project-id="project-html"]'); await waitFor(() => { expect(deckCard?.querySelector('iframe')).toBeNull(); expect(htmlCard?.querySelector('iframe')).toBeNull(); expect(deckCard?.querySelector('.recent-projects__card-glyph')).toBeTruthy(); expect(htmlCard?.querySelector('.recent-projects__card-glyph')).toBeTruthy(); }); expect(fetchMock).not.toHaveBeenCalled(); }); });