mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
196 lines
5.5 KiB
TypeScript
196 lines
5.5 KiB
TypeScript
// @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(
|
|
<NewProjectModal
|
|
open
|
|
skills={skills}
|
|
designSystems={designSystems}
|
|
defaultDesignSystemId={null}
|
|
templates={[]}
|
|
promptTemplates={[]}
|
|
onCreate={() => {}}
|
|
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<boolean>((resolve) => {
|
|
resolveCreate = resolve;
|
|
}),
|
|
);
|
|
const onClose = vi.fn();
|
|
|
|
render(
|
|
<NewProjectModal
|
|
open
|
|
skills={skills}
|
|
designSystems={designSystems}
|
|
defaultDesignSystemId={null}
|
|
templates={[]}
|
|
promptTemplates={[]}
|
|
onCreate={onCreate}
|
|
onClose={onClose}
|
|
/>,
|
|
);
|
|
|
|
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(
|
|
<NewProjectModal
|
|
open
|
|
skills={skills}
|
|
designSystems={designSystems}
|
|
defaultDesignSystemId={null}
|
|
templates={[]}
|
|
promptTemplates={[]}
|
|
onCreate={() => {}}
|
|
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(
|
|
<NewProjectModal
|
|
open
|
|
skills={skills}
|
|
designSystems={designSystems}
|
|
defaultDesignSystemId="clay"
|
|
templates={templates}
|
|
promptTemplates={[]}
|
|
onDeleteTemplate={onDelete}
|
|
onCreate={() => {}}
|
|
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');
|
|
});
|
|
});
|