mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Stage B of plugin-driven-flow-plan — Home intent tabs / shortcuts.
|
|
// Covers:
|
|
// - Every chip in the catalog renders with its test id.
|
|
// - Clicking a chip forwards the full chip descriptor to onPickChip
|
|
// so the dispatcher in HomeView can route to the right flow.
|
|
// - The active + pending UI states light up the right chip and
|
|
// disable all chips while a plugin is mid-apply.
|
|
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import type { InstalledPluginRecord } from '@open-design/contracts';
|
|
|
|
import { HomeHero } from '../../src/components/HomeHero';
|
|
import {
|
|
HOME_HERO_CHIPS,
|
|
findChip,
|
|
} from '../../src/components/home-hero/chips';
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
function makePlugin(
|
|
id: string,
|
|
mode: string,
|
|
title = id,
|
|
extraTags: string[] = [],
|
|
): InstalledPluginRecord {
|
|
return {
|
|
id,
|
|
title,
|
|
version: '1.0.0',
|
|
sourceKind: 'bundled',
|
|
source: '/tmp',
|
|
trust: 'bundled',
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
manifest: {
|
|
name: id,
|
|
version: '1.0.0',
|
|
title,
|
|
description: 'Plugin preset fixture',
|
|
tags: [mode, ...extraTags],
|
|
od: {
|
|
mode,
|
|
useCase: {
|
|
query: `Create with {{topic}} using ${title}`,
|
|
},
|
|
inputs: [
|
|
{
|
|
name: 'topic',
|
|
label: 'Topic',
|
|
type: 'text',
|
|
default: 'a focused brief',
|
|
},
|
|
],
|
|
preview: { type: 'image', poster: '/preview.png' },
|
|
},
|
|
},
|
|
fsPath: '/tmp',
|
|
installedAt: 0,
|
|
updatedAt: 0,
|
|
};
|
|
}
|
|
|
|
function renderHero(overrides: Partial<React.ComponentProps<typeof HomeHero>> = {}) {
|
|
const onPickChip = vi.fn();
|
|
const onPickPlugin = vi.fn();
|
|
const onPickExamplePlugin = vi.fn();
|
|
const onClearActiveChip = vi.fn();
|
|
render(
|
|
<HomeHero
|
|
prompt=""
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={onPickPlugin}
|
|
onPickExamplePlugin={onPickExamplePlugin}
|
|
onPickChip={onPickChip}
|
|
onClearActiveChip={onClearActiveChip}
|
|
contextItemCount={0}
|
|
error={null}
|
|
{...overrides}
|
|
/>,
|
|
);
|
|
return { onPickChip, onPickPlugin, onPickExamplePlugin, onClearActiveChip };
|
|
}
|
|
|
|
describe('HomeHero intent rail', () => {
|
|
it('renders creation chips as composer tabs and collapses shortcuts behind More', () => {
|
|
renderHero();
|
|
const tabs = screen.getByTestId('home-hero-type-tabs');
|
|
for (const chip of HOME_HERO_CHIPS) {
|
|
if (chip.group === 'create') {
|
|
const node = screen.getByTestId(`home-hero-rail-${chip.id}`);
|
|
expect(node).toBeTruthy();
|
|
expect(tabs.contains(node)).toBe(true);
|
|
} else {
|
|
expect(screen.queryByTestId(`home-hero-rail-${chip.id}`)).toBeNull();
|
|
}
|
|
}
|
|
fireEvent.click(screen.getByTestId('home-hero-shortcuts-trigger'));
|
|
const menu = screen.getByTestId('home-hero-shortcuts-menu');
|
|
for (const chip of HOME_HERO_CHIPS.filter((item) => item.group === 'migrate')) {
|
|
const node = screen.getByTestId(`home-hero-rail-${chip.id}`);
|
|
expect(node).toBeTruthy();
|
|
expect(menu.contains(node)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('forwards the matching chip descriptor when clicked', () => {
|
|
const { onPickChip } = renderHero();
|
|
fireEvent.click(screen.getByTestId('home-hero-rail-image'));
|
|
expect(onPickChip).toHaveBeenCalledTimes(1);
|
|
expect(onPickChip).toHaveBeenCalledWith(findChip('image'));
|
|
});
|
|
|
|
it('moves the active creation chip into the composer and hides the tab row', () => {
|
|
renderHero({ activeChipId: 'video' });
|
|
expect(screen.queryByTestId('home-hero-type-tabs')).toBeNull();
|
|
expect(screen.queryByTestId('home-hero-rail-video')).toBeNull();
|
|
const node = screen.getByTestId('home-hero-active-type-chip');
|
|
expect(node.getAttribute('data-chip-id')).toBe('video');
|
|
expect(node.textContent).toContain('Video');
|
|
});
|
|
|
|
it('lets the active creation chip be removed from the composer', () => {
|
|
const { onClearActiveChip } = renderHero({ activeChipId: 'prototype' });
|
|
fireEvent.click(screen.getByTestId('home-hero-active-type-chip'));
|
|
expect(onClearActiveChip).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('shows prompt examples below the composer for the selected tab', () => {
|
|
const onPromptChange = vi.fn();
|
|
renderHero({ activeChipId: 'deck', onPromptChange });
|
|
|
|
expect(screen.getByTestId('home-hero-prompt-examples')).toBeTruthy();
|
|
const examples = screen.getAllByTestId('home-hero-prompt-example');
|
|
expect(examples).toHaveLength(4);
|
|
|
|
fireEvent.click(examples[0]!);
|
|
expect(onPromptChange).toHaveBeenCalledWith(
|
|
'Research the market opportunity for a product launch, including competitors, target users, pricing hypotheses, and launch narrative',
|
|
);
|
|
});
|
|
|
|
it('shows matching plugin presets in the example prompt area for the selected tab', () => {
|
|
const deckPlugin = makePlugin('example-deck-a', 'deck', 'Investor deck');
|
|
const imagePlugin = makePlugin('example-image-a', 'image', 'Product image');
|
|
const { onPickExamplePlugin } = renderHero({
|
|
activeChipId: 'deck',
|
|
pluginOptions: [deckPlugin, imagePlugin],
|
|
});
|
|
|
|
const presets = screen.getAllByTestId('home-hero-plugin-preset');
|
|
expect(presets).toHaveLength(1);
|
|
expect(presets[0]?.textContent).toContain('Investor deck');
|
|
expect(presets[0]?.textContent).toContain('a focused brief');
|
|
|
|
fireEvent.click(presets[0]!);
|
|
expect(onPickExamplePlugin).toHaveBeenCalledWith(
|
|
deckPlugin,
|
|
'deck',
|
|
'Create with a focused brief using Investor deck',
|
|
);
|
|
});
|
|
|
|
it('keeps Hatch Pet at the end of the image example presets', () => {
|
|
const hatchPet = makePlugin('example-hatch-pet', 'image', 'Hatch Pet');
|
|
const imagePoster = makePlugin('image-template-poster', 'image', 'Image Poster');
|
|
const stoneInfographic = makePlugin('image-template-stone', 'image', 'Stone Infographic');
|
|
renderHero({
|
|
activeChipId: 'image',
|
|
pluginOptions: [hatchPet, imagePoster, stoneInfographic],
|
|
});
|
|
|
|
const presets = screen.getAllByTestId('home-hero-plugin-preset');
|
|
expect(presets.map((preset) => preset.textContent)).toEqual([
|
|
expect.stringContaining('Image Poster'),
|
|
expect.stringContaining('Stone Infographic'),
|
|
expect.stringContaining('Hatch Pet'),
|
|
]);
|
|
});
|
|
|
|
it('moves live artifact presets out of Image and into Live artifact examples', () => {
|
|
const imagePoster = makePlugin('image-template-poster', 'image', 'Image Poster');
|
|
const notionDashboard = makePlugin(
|
|
'image-template-notion-team-dashboard-live-artifact',
|
|
'image',
|
|
'Notion-style Team Dashboard (Live Artifact)',
|
|
['live-artifact'],
|
|
);
|
|
renderHero({
|
|
activeChipId: 'image',
|
|
pluginOptions: [imagePoster, notionDashboard],
|
|
});
|
|
|
|
let presets = screen.getAllByTestId('home-hero-plugin-preset');
|
|
expect(presets).toHaveLength(1);
|
|
expect(presets[0]?.textContent).toContain('Image Poster');
|
|
|
|
cleanup();
|
|
renderHero({
|
|
activeChipId: 'live-artifact',
|
|
pluginOptions: [imagePoster, notionDashboard],
|
|
});
|
|
|
|
presets = screen.getAllByTestId('home-hero-plugin-preset');
|
|
expect(presets).toHaveLength(1);
|
|
expect(presets[0]?.textContent).toContain('Notion-style Team Dashboard (Live Artifact)');
|
|
});
|
|
|
|
it('disables every visible chip while a plugin apply is in flight', () => {
|
|
renderHero({ pendingPluginId: 'od-figma-migration', pendingChipId: 'figma' });
|
|
for (const chip of HOME_HERO_CHIPS.filter((item) => item.group === 'create')) {
|
|
const node = screen.getByTestId(`home-hero-rail-${chip.id}`);
|
|
expect((node as HTMLButtonElement).disabled).toBe(true);
|
|
}
|
|
const trigger = screen.getByTestId('home-hero-shortcuts-trigger') as HTMLButtonElement;
|
|
expect(trigger.disabled).toBe(true);
|
|
expect(trigger.className).toContain('is-pending');
|
|
});
|
|
|
|
it('shows plugin authoring with the starter shortcuts after More opens', () => {
|
|
renderHero();
|
|
fireEvent.click(screen.getByTestId('home-hero-shortcuts-trigger'));
|
|
const createPluginGroup = screen
|
|
.getByTestId('home-hero-rail-create-plugin')
|
|
.closest('[data-rail-group]');
|
|
|
|
expect(createPluginGroup?.getAttribute('data-rail-group')).toBe('migrate');
|
|
for (const id of ['figma', 'template']) {
|
|
expect(screen.getByTestId(`home-hero-rail-${id}`).closest('[data-rail-group]'))
|
|
.toBe(createPluginGroup);
|
|
}
|
|
expect(screen.queryByTestId('home-hero-rail-folder')).toBeNull();
|
|
});
|
|
|
|
it('keeps the generic fallback in the free-form prompt instead of an Other chip', () => {
|
|
renderHero();
|
|
|
|
expect(findChip('other')).toBeUndefined();
|
|
expect(screen.queryByTestId('home-hero-rail-other')).toBeNull();
|
|
});
|
|
|
|
it('migration chips carry the right action discriminator', () => {
|
|
expect(findChip('create-plugin')?.action).toMatchObject({ kind: 'create-plugin' });
|
|
expect(findChip('figma')?.action).toMatchObject({ kind: 'apply-figma-migration' });
|
|
expect(findChip('folder')).toBeUndefined();
|
|
expect(findChip('template')?.action).toMatchObject({ kind: 'open-template-picker' });
|
|
});
|
|
|
|
it('media chips route to od-media-generation with the matching project kind', () => {
|
|
expect(findChip('image')?.action).toMatchObject({
|
|
kind: 'apply-scenario',
|
|
pluginId: 'od-media-generation',
|
|
projectKind: 'image',
|
|
});
|
|
expect(findChip('video')?.action).toMatchObject({ pluginId: 'od-media-generation', projectKind: 'video' });
|
|
expect(findChip('audio')?.action).toMatchObject({ pluginId: 'od-media-generation', projectKind: 'audio' });
|
|
});
|
|
|
|
it('prototype and slide-deck chips route to their specialised bundled scenario plugin', () => {
|
|
// Prototype now binds to web-prototype's seed template instead of
|
|
// the generic od-new-generation router. Same for Slide deck →
|
|
// simple-deck. See packages/contracts/src/plugins/scenario-defaults.ts
|
|
// for the rationale (battle-tested seed + layouts + checklist).
|
|
expect(findChip('prototype')?.action).toMatchObject({ pluginId: 'example-web-prototype', projectKind: 'prototype' });
|
|
expect(findChip('deck')?.action).toMatchObject({ pluginId: 'example-simple-deck', projectKind: 'deck' });
|
|
});
|
|
|
|
it('specialised category chips route to their bundled scenario plugin', () => {
|
|
// HyperFrames is the motion-graphics specialisation of Video,
|
|
// surfaced as a separate chip so users can target it directly
|
|
// instead of routing through the generic Video chip.
|
|
expect(findChip('hyperframes')?.action).toMatchObject({
|
|
kind: 'apply-scenario',
|
|
pluginId: 'example-hyperframes',
|
|
projectKind: 'video',
|
|
});
|
|
expect(findChip('live-artifact')?.action).toMatchObject({
|
|
kind: 'apply-scenario',
|
|
pluginId: 'example-live-artifact',
|
|
projectKind: 'prototype',
|
|
projectMetadata: {
|
|
kind: 'prototype',
|
|
intent: 'live-artifact',
|
|
fidelity: 'high-fidelity',
|
|
},
|
|
});
|
|
});
|
|
});
|