mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(web): redesign Designs tab cards — covers, tags, overflow menu, multi-select - Render real previews on project cards: HTML iframe / image / video / hashed gradient fallback with project initial; lazily fetches the project's primary file when metadata.entryFile is unset, prefers index.html → newest html → image → video. - Live artifact card thumbnails embed the rendered artifact URL via sandboxed iframe. - Replace the per-card close button with a `…` overflow menu (Rename, Delete) that opens on hover/click; click-outside and Esc close it. - Add multi-select mode (toolbar toggle → checkbox per card → "N selected · Delete · Cancel" pill) with batch delete via the existing onDelete prop. - Add a category tag to every card (Prototype / Live Artifact / Slide / Media) derived from project.metadata.intent / kind / skillId. - Replace browser prompt() and confirm() with custom modals (rename input + danger-confirm) reusing the existing .modal shell. - Add `more-horizontal` icon and 16 new i18n keys across all 18 locales (zh-CN/zh-TW localized; others fall back to English). * test(e2e): update home delete flow for overflow menu + custom confirm modal The previous flow targeted a per-card X button labelled "delete project <name>" and asserted on a native `dialog` event. The card UI now exposes a `…` overflow menu and a styled confirm modal, so reach delete via the menu and assert against the modal's Cancel / Delete buttons instead. * fix(web): harden Designs tab preview sandbox * fix(web): hide Designs select mode in kanban
80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { DesignsTab } from '../../src/components/DesignsTab';
|
|
import type { Project } from '../../src/types';
|
|
|
|
vi.mock('../../src/providers/registry', () => ({
|
|
deleteLiveArtifact: vi.fn(),
|
|
fetchLiveArtifacts: vi.fn(async () => []),
|
|
fetchProjectFiles: vi.fn(async () => []),
|
|
liveArtifactPreviewUrl: (projectId: string, artifactId: string) =>
|
|
`/api/projects/${projectId}/live-artifacts/${artifactId}/preview`,
|
|
projectFileUrl: (projectId: string, fileName: string) =>
|
|
`/api/projects/${projectId}/files/${fileName}`,
|
|
}));
|
|
|
|
const project: Project = {
|
|
id: 'project-1',
|
|
name: 'Landing refresh',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
status: { value: 'not_started' },
|
|
};
|
|
|
|
describe('DesignsTab select mode', () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
it('only exposes select mode in grid view', () => {
|
|
render(
|
|
<DesignsTab
|
|
projects={[project]}
|
|
skills={[]}
|
|
designSystems={[]}
|
|
onOpen={vi.fn()}
|
|
onOpenLiveArtifact={vi.fn()}
|
|
onDelete={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByRole('button', { name: 'Select' })).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByTestId('designs-view-kanban'));
|
|
|
|
expect(screen.queryByRole('button', { name: 'Select' })).toBeNull();
|
|
});
|
|
|
|
it('exits select mode when switching to kanban view', () => {
|
|
render(
|
|
<DesignsTab
|
|
projects={[project]}
|
|
skills={[]}
|
|
designSystems={[]}
|
|
onOpen={vi.fn()}
|
|
onOpenLiveArtifact={vi.fn()}
|
|
onDelete={vi.fn()}
|
|
onRename={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Select' }));
|
|
expect(screen.getByText('0 selected')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByTestId('designs-view-kanban'));
|
|
fireEvent.click(screen.getByTestId('designs-view-grid'));
|
|
|
|
expect(screen.queryByText('0 selected')).toBeNull();
|
|
expect(screen.getByRole('button', { name: 'Select' })).toBeTruthy();
|
|
});
|
|
});
|