// @vitest-environment jsdom import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@open-design/host', () => ({ isOpenDesignHostAvailable: () => true, pickAndImportHostProject: vi.fn(), })); import { pickAndImportHostProject } from '@open-design/host'; import { NewProjectModal } from '../../src/components/NewProjectModal'; import type { DesignSystemSummary, ProjectTemplate, SkillSummary, } from '../../src/types'; const skills: SkillSummary[] = [ { id: 'prototype-skill', name: 'Prototype', description: 'Build prototypes', mode: 'prototype', surface: 'web', previewType: 'html', designSystemRequired: true, defaultFor: ['prototype'], triggers: [], upstream: null, hasBody: true, examplePrompt: 'Build a prototype.', aggregatesExamples: false, }, ]; const designSystems: DesignSystemSummary[] = [ { id: 'clay', title: 'Clay', summary: 'Friendly tactile product UI.', category: 'Product', swatches: ['#f4efe7', '#25211d'], }, ]; afterEach(() => { cleanup(); globalThis.ResizeObserver = originalResizeObserver; Element.prototype.scrollIntoView = originalScrollIntoView; }); const originalResizeObserver = globalThis.ResizeObserver; const originalScrollIntoView = Element.prototype.scrollIntoView; class ResizeObserverMock { observe() {} disconnect() {} unobserve() {} } beforeEach(() => { globalThis.ResizeObserver = ResizeObserverMock as typeof ResizeObserver; Element.prototype.scrollIntoView = vi.fn(); vi.mocked(pickAndImportHostProject).mockReset(); }); describe('NewProjectModal layout', () => { it('keeps the project form inside a scrollable body region', () => { const { container } = render( {}} onClose={() => {}} />, ); const modalBody = container.querySelector('.new-project-modal__body'); const panelBody = container.querySelector('.new-project-modal__body .newproj-body'); expect(modalBody).toBeTruthy(); expect(panelBody).toBeTruthy(); expect(screen.getByTestId('new-project-panel')).toBeTruthy(); expect(screen.getByTestId('create-project')).toBeTruthy(); }); it('keeps the modal open with a waiting state until project creation finishes', async () => { let resolveCreate!: (value: boolean) => void; const onCreate = vi.fn( () => new Promise((resolve) => { resolveCreate = resolve; }), ); const onClose = vi.fn(); render( , ); fireEvent.click(screen.getByTestId('create-project')); expect(onCreate).toHaveBeenCalledTimes(1); expect(onClose).not.toHaveBeenCalled(); expect(screen.getByRole('status').textContent).toContain('Creating project…'); expect((screen.getByTestId('create-project') as HTMLButtonElement).disabled).toBe(true); resolveCreate(true); await waitFor(() => { expect(onClose).toHaveBeenCalledTimes(1); }); }); it('forwards the desktop folder import response handler to the inner panel', async () => { const importResult = { conversationId: 'conversation-host', entryFile: 'src/App.tsx', ok: true, projectId: 'project-host', } as const; vi.mocked(pickAndImportHostProject).mockResolvedValue(importResult); const onImportFolderResponse = vi.fn(); render( {}} onImportFolderResponse={onImportFolderResponse} onClose={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: 'Open folder' })); await waitFor(() => { expect(pickAndImportHostProject).toHaveBeenCalledWith({ skillId: 'prototype-skill' }); }); await waitFor(() => { expect(onImportFolderResponse).toHaveBeenCalledWith(importResult); }); }); }); describe('NewProjectModal template deletion plumbing', () => { it('forwards onDeleteTemplate to the inner panel', async () => { const templates: ProjectTemplate[] = [ { id: 'tmpl-landing', name: 'Landing Page', description: 'A saved landing page starter.', files: [{ name: 'prototype/App.jsx', content: '' }], createdAt: 1714867200000, }, ]; const onDelete = vi.fn().mockResolvedValue(true); render( {}} onClose={() => {}} />, ); fireEvent.click(screen.getByRole('tab', { name: 'From template' })); fireEvent.click(screen.getByLabelText(/delete template/i)); await screen.findByRole('alertdialog'); fireEvent.click(screen.getByRole('button', { name: 'Delete template' })); expect(onDelete).toHaveBeenCalledWith('tmpl-landing'); }); });