mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Clear the opposing selection when the user picks a skill or scenario chip, and omit skillId when a scenario plugin is active on submit.
376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
|
type InstalledPluginRecord,
|
|
type SkillSummary,
|
|
} from '@open-design/contracts';
|
|
import { HomeView } from '../../src/components/HomeView';
|
|
|
|
const SKILL: SkillSummary = {
|
|
id: 'prototype-lab',
|
|
name: 'Prototype Lab',
|
|
description: 'Create a focused prototype.',
|
|
triggers: ['prototype', 'flow'],
|
|
mode: 'prototype',
|
|
previewType: 'html',
|
|
designSystemRequired: false,
|
|
defaultFor: [],
|
|
upstream: null,
|
|
hasBody: true,
|
|
examplePrompt: 'Design a focused onboarding prototype.',
|
|
aggregatesExamples: false,
|
|
};
|
|
|
|
const DECK_SKILL: SkillSummary = {
|
|
...SKILL,
|
|
id: 'deck-lab',
|
|
name: 'Deck Lab',
|
|
description: 'Create a focused slide deck.',
|
|
triggers: ['deck', 'slides'],
|
|
mode: 'deck',
|
|
examplePrompt: 'Design a focused investor deck.',
|
|
};
|
|
|
|
const WEB_PROTOTYPE_PLUGIN = makePlugin('example-web-prototype', 'Web Prototype');
|
|
|
|
function makePlugin(id: string, title: string): InstalledPluginRecord {
|
|
return {
|
|
id,
|
|
title,
|
|
version: '1.0.0',
|
|
sourceKind: 'bundled',
|
|
source: `/tmp/${id}`,
|
|
trust: 'bundled',
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
fsPath: `/tmp/${id}`,
|
|
installedAt: 0,
|
|
updatedAt: 0,
|
|
manifest: {
|
|
name: id,
|
|
title,
|
|
version: '1.0.0',
|
|
description: `${title} fixture`,
|
|
tags: ['fixture'],
|
|
od: {
|
|
kind: 'scenario',
|
|
taskKind: 'new-generation',
|
|
useCase: {
|
|
query: `Hydrated query from ${title}`,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('HomeView context picker', () => {
|
|
it('stages pasted files on Home and submits them as first-turn context', async () => {
|
|
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
|
if (typeof url === 'string' && url === '/api/plugins') {
|
|
return new Response(JSON.stringify({ plugins: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
|
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch ${url}`);
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
cb(0);
|
|
return 0;
|
|
});
|
|
const onSubmit = vi.fn();
|
|
const file = new File(['brief'], 'brief.pdf', { type: 'application/pdf' });
|
|
|
|
render(
|
|
<HomeView
|
|
projects={[]}
|
|
onSubmit={onSubmit}
|
|
onOpenProject={() => undefined}
|
|
onViewAllProjects={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const input = await screen.findByTestId('home-hero-input');
|
|
expect(screen.getByTestId('home-hero-attach')).toBeTruthy();
|
|
fireEvent.paste(input, {
|
|
clipboardData: {
|
|
items: [
|
|
{
|
|
kind: 'file',
|
|
getAsFile: () => file,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await waitFor(() => expect(screen.getByText('brief.pdf')).toBeTruthy());
|
|
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
prompt: '',
|
|
pluginId: DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
|
attachments: [file],
|
|
}));
|
|
});
|
|
|
|
it('adds multiple @ plugins as context without applying or hydrating their query', async () => {
|
|
const plugins = [
|
|
makePlugin('chart-plugin', 'Chart Plugin'),
|
|
makePlugin('deck-plugin', 'Deck Plugin'),
|
|
];
|
|
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
|
if (typeof url === 'string' && url === '/api/plugins') {
|
|
return new Response(JSON.stringify({ plugins }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
|
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch ${url}`);
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
cb(0);
|
|
return 0;
|
|
});
|
|
const onSubmit = vi.fn();
|
|
|
|
render(
|
|
<HomeView
|
|
projects={[]}
|
|
onSubmit={onSubmit}
|
|
onOpenProject={() => undefined}
|
|
onViewAllProjects={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const input = await screen.findByTestId('home-hero-input');
|
|
fireEvent.change(input, { target: { value: 'Build @chart' } });
|
|
fireEvent.mouseDown(await screen.findByRole('option', { name: /chart plugin/i }));
|
|
|
|
await waitFor(() => {
|
|
expect((input as HTMLTextAreaElement).value).toBe('Build @Chart Plugin');
|
|
expect(screen.getByTestId('home-hero-context-plugin-chart-plugin')).toBeTruthy();
|
|
});
|
|
|
|
fireEvent.change(input, { target: { value: `${(input as HTMLTextAreaElement).value} @deck` } });
|
|
fireEvent.mouseDown(await screen.findByRole('option', { name: /deck plugin/i }));
|
|
|
|
await waitFor(() => {
|
|
expect((input as HTMLTextAreaElement).value).toBe('Build @Chart Plugin @Deck Plugin');
|
|
expect(screen.getByTestId('home-hero-context-plugin-chart-plugin')).toBeTruthy();
|
|
expect(screen.getByTestId('home-hero-context-plugin-deck-plugin')).toBeTruthy();
|
|
});
|
|
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/apply'))).toBe(false);
|
|
expect((input as HTMLTextAreaElement).value).not.toContain('Hydrated query');
|
|
|
|
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
prompt: 'Build @Chart Plugin @Deck Plugin',
|
|
pluginId: DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
|
contextPlugins: [
|
|
expect.objectContaining({ id: 'chart-plugin', title: 'Chart Plugin' }),
|
|
expect.objectContaining({ id: 'deck-plugin', title: 'Deck Plugin' }),
|
|
],
|
|
}));
|
|
});
|
|
|
|
it('binds a selected home skill to the created project payload', async () => {
|
|
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
|
if (typeof url === 'string' && url === '/api/plugins') {
|
|
return new Response(JSON.stringify({ plugins: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
|
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch ${url}`);
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
cb(0);
|
|
return 0;
|
|
});
|
|
const onSubmit = vi.fn();
|
|
|
|
render(
|
|
<HomeView
|
|
projects={[]}
|
|
skills={[SKILL]}
|
|
onSubmit={onSubmit}
|
|
onOpenProject={() => undefined}
|
|
onViewAllProjects={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const input = await screen.findByTestId('home-hero-input');
|
|
fireEvent.change(input, { target: { value: '@proto' } });
|
|
fireEvent.mouseDown(screen.getByRole('option', { name: /prototype lab/i }));
|
|
|
|
await waitFor(() => {
|
|
expect((input as HTMLTextAreaElement).value).toBe('@Prototype Lab');
|
|
expect(screen.getByTestId('home-hero-active-skill')).toBeTruthy();
|
|
});
|
|
|
|
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
prompt: '@Prototype Lab',
|
|
pluginId: DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
|
skillId: SKILL.id,
|
|
projectKind: 'prototype',
|
|
}));
|
|
});
|
|
|
|
it('clears an active type chip when the user picks a skill (#2972)', async () => {
|
|
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
|
if (typeof url === 'string' && url === '/api/plugins') {
|
|
return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
|
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch ${url}`);
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
cb(0);
|
|
return 0;
|
|
});
|
|
const onSubmit = vi.fn();
|
|
|
|
render(
|
|
<HomeView
|
|
projects={[]}
|
|
skills={[DECK_SKILL, SKILL]}
|
|
onSubmit={onSubmit}
|
|
onOpenProject={() => undefined}
|
|
onViewAllProjects={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
|
|
});
|
|
|
|
const input = screen.getByTestId('home-hero-input');
|
|
fireEvent.change(input, { target: { value: '@deck' } });
|
|
fireEvent.mouseDown(screen.getByRole('option', { name: /deck lab/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('home-hero-active-skill')).toBeTruthy();
|
|
expect(screen.queryByTestId('home-hero-active-type-chip')).toBeNull();
|
|
});
|
|
|
|
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
pluginId: DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
|
skillId: DECK_SKILL.id,
|
|
projectKind: 'deck',
|
|
}));
|
|
expect(onSubmit.mock.calls[0]?.[0]?.pluginId).not.toBe('example-web-prototype');
|
|
});
|
|
|
|
it('clears an active skill when the user picks a type chip (#2972)', async () => {
|
|
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
|
if (typeof url === 'string' && url === '/api/plugins') {
|
|
return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (typeof url === 'string' && url.includes('/apply')) {
|
|
return new Response(JSON.stringify({
|
|
appliedPlugin: {
|
|
snapshotId: 'snap-web-prototype',
|
|
pluginId: 'example-web-prototype',
|
|
pluginVersion: '1.0.0',
|
|
inputs: {},
|
|
},
|
|
contextItems: [],
|
|
}), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
|
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch ${url}`);
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
cb(0);
|
|
return 0;
|
|
});
|
|
const onSubmit = vi.fn();
|
|
|
|
render(
|
|
<HomeView
|
|
projects={[]}
|
|
skills={[SKILL]}
|
|
onSubmit={onSubmit}
|
|
onOpenProject={() => undefined}
|
|
onViewAllProjects={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const input = await screen.findByTestId('home-hero-input');
|
|
fireEvent.change(input, { target: { value: '@proto' } });
|
|
fireEvent.mouseDown(screen.getByRole('option', { name: /prototype lab/i }));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('home-hero-active-skill')).toBeTruthy();
|
|
});
|
|
|
|
fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
|
|
expect(screen.queryByTestId('home-hero-active-skill')).toBeNull();
|
|
});
|
|
|
|
fireEvent.change(input, { target: { value: 'Build a pricing-page prototype.' } });
|
|
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
|
|
|
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
pluginId: 'example-web-prototype',
|
|
skillId: null,
|
|
projectKind: 'prototype',
|
|
})));
|
|
});
|
|
});
|