Replace the first-run placeholder seed with an onboarding canvas

This commit is contained in:
2026-06-09 14:03:47 +03:00
parent 7d247e8d2c
commit 81f563a3e7
16 changed files with 852 additions and 93 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* 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() {} }),
}));
(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(<Onboarding />));
});
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('greets the user and offers the primary create action', () => {
expect(container.textContent).toContain('Welcome to Astrolabe');
expect(container.textContent).toContain('Create your first snippet');
});
test('renders one card per example, each with an Add control', () => {
for (const example of CHART_EXAMPLES) {
expect(container.textContent).toContain(example.name);
expect(container.textContent).toContain(example.description);
expect(container.querySelector(`button[aria-label="Add ${example.name}"]`)).not.toBeNull();
}
});
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 examples 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);
});
});