/** * Onboarding canvas — the empty-workspace actions (spec §02). * * The live chart rendering is integration (vega-embed) and is mocked away here; * what these tests own is the wiring: each affordance creates the right snippets * in the store. Card previews are exercised only insofar as they don't crash. */ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { CHART_EXAMPLES, exampleSpecText } from '@core/examples'; import { sampleSpecText } from '@core/snippet'; import { useSnippetStore } from '../stores/SnippetStore'; import { Onboarding } from './Onboarding'; // The gallery renders one chart per card; stub the shared renderer so the test // never touches vega-embed. A resolved no-op handle is enough — Onboarding only // finalizes it on unmount. vi.mock('../services/chart-renderer', () => ({ renderSpec: () => Promise.resolve({ destroy() {}, resize() {}, inspectData: () => null }), })); (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; let container: HTMLDivElement; let root: Root; beforeEach(() => { useSnippetStore.getState().reset(); container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); act(() => root.render()); }); afterEach(() => { act(() => root.unmount()); container.remove(); useSnippetStore.getState().reset(); vi.clearAllMocks(); }); /** Click the button whose accessible name (aria-label, else text) matches. */ function click(name: string) { const button = Array.from(container.querySelectorAll('button')).find((b) => (b.getAttribute('aria-label') ?? b.textContent ?? '').includes(name), ); if (!button) throw new Error(`button not found: ${name}`); act(() => button.click()); } describe('Onboarding', () => { test('"Create your first snippet" creates one snippet from the sample template', () => { click('Create your first snippet'); const { snippets, activeSnippetId } = useSnippetStore.getState(); expect(snippets).toHaveLength(1); expect(snippets[0].spec).toBe(sampleSpecText()); expect(activeSnippetId).toBe(snippets[0].id); }); test('an example’s Add creates that snippet, named and active', () => { const scatter = CHART_EXAMPLES.find((e) => e.id === 'scatter')!; click(`Add ${scatter.name}`); const { snippets, activeSnippetId } = useSnippetStore.getState(); expect(snippets).toHaveLength(1); expect(snippets[0].name).toBe(scatter.name); expect(snippets[0].spec).toBe(exampleSpecText(scatter)); expect(activeSnippetId).toBe(snippets[0].id); }); test('"Add all" adds every example and makes the bar chart active', () => { click('Add all'); const { snippets, activeSnippetId } = useSnippetStore.getState(); expect(snippets).toHaveLength(CHART_EXAMPLES.length); // Every example name is present. const names = new Set(snippets.map((s) => s.name)); for (const example of CHART_EXAMPLES) expect(names.has(example.name)).toBe(true); // The first example (bar) is the active one (staggered newest). const active = snippets.find((s) => s.id === activeSnippetId); expect(active?.name).toBe(CHART_EXAMPLES[0].name); }); });