070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
280 lines
8.2 KiB
TypeScript
280 lines
8.2 KiB
TypeScript
// @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>): 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(
|
|
<RecentProjectsStrip
|
|
projects={projects(8)}
|
|
onOpen={() => {}}
|
|
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(
|
|
<RecentProjectsStrip
|
|
projects={projects(8)}
|
|
onOpen={() => {}}
|
|
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(
|
|
<RecentProjectsStrip
|
|
projects={[]}
|
|
onOpen={() => {}}
|
|
onViewAll={() => {}}
|
|
/>,
|
|
);
|
|
|
|
rerender(
|
|
<RecentProjectsStrip
|
|
projects={projects(8)}
|
|
onOpen={() => {}}
|
|
onViewAll={() => {}}
|
|
/>,
|
|
);
|
|
|
|
expect(container.querySelectorAll('.recent-projects__card')).toHaveLength(6);
|
|
});
|
|
|
|
it('matches project cards with previews and design-system tags', async () => {
|
|
const { container } = render(
|
|
<RecentProjectsStrip
|
|
projects={[
|
|
project({
|
|
id: 'project-ds',
|
|
name: 'Acme Design System',
|
|
updatedAt: 4,
|
|
metadata: {
|
|
kind: 'other',
|
|
importedFrom: 'design-system',
|
|
},
|
|
}),
|
|
project({
|
|
id: 'project-html',
|
|
name: 'Web Prototype',
|
|
updatedAt: 3,
|
|
}),
|
|
]}
|
|
onOpen={() => {}}
|
|
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(
|
|
<RecentProjectsStrip
|
|
projects={[
|
|
project({
|
|
id: 'project-ds-fallback',
|
|
name: 'Acme Design System',
|
|
updatedAt: 4,
|
|
metadata: {
|
|
kind: 'other',
|
|
importedFrom: 'design-system',
|
|
},
|
|
}),
|
|
]}
|
|
onOpen={() => {}}
|
|
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(
|
|
<RecentProjectsStrip
|
|
projects={[
|
|
project({
|
|
id: 'project-deck',
|
|
name: 'Simple Deck',
|
|
updatedAt: 4,
|
|
metadata: { kind: 'deck' },
|
|
}),
|
|
project({
|
|
id: 'project-html',
|
|
name: 'Web Prototype',
|
|
updatedAt: 3,
|
|
}),
|
|
]}
|
|
onOpen={() => {}}
|
|
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();
|
|
});
|
|
});
|