mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart builder: data-aware defaults, one-click hint fixes, canvas preview, fullscreen modal
This commit is contained in:
@@ -8,11 +8,26 @@ import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import { ChartBuilderModal } from './ChartBuilderModal';
|
||||
|
||||
// The builder preview embeds a real Vega chart in an effect; stub the renderer so
|
||||
// this render test stays a pure React/DOM check (the loop we guard against happens
|
||||
// during commit, long before any chart is drawn).
|
||||
vi.mock('../services/chart-renderer', () => ({
|
||||
renderSpec: () => Promise.resolve({ destroy() {}, resize() {} }),
|
||||
}));
|
||||
// these tests stay pure React/DOM checks. `renderSpec` is a vi.fn so a test can make
|
||||
// it reject (e.g. the canvas-too-large path); the mocked `ChartTooLargeError` is the
|
||||
// same class the component imports, so its `instanceof` check matches. The class is
|
||||
// declared inside the factory because vi.mock is hoisted above module-scope code.
|
||||
vi.mock('../services/chart-renderer', () => {
|
||||
class ChartTooLargeError extends Error {
|
||||
heightPx: number;
|
||||
limitPx: number;
|
||||
constructor(heightPx: number, limitPx: number) {
|
||||
super('too large');
|
||||
this.name = 'ChartTooLargeError';
|
||||
this.heightPx = heightPx;
|
||||
this.limitPx = limitPx;
|
||||
}
|
||||
}
|
||||
return {
|
||||
renderSpec: vi.fn(() => Promise.resolve({ destroy() {}, resize() {} })),
|
||||
ChartTooLargeError,
|
||||
};
|
||||
});
|
||||
|
||||
// React 19 wants this flag set for act() to drive effects without warnings.
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
@@ -69,6 +84,77 @@ describe('ChartBuilderModal', () => {
|
||||
expect(container.textContent).toContain('scatter'); // the guidance hint rendered
|
||||
});
|
||||
|
||||
test('a guidance hint offers a one-click fix that resolves it (actionable hints, §06)', async () => {
|
||||
const ds = createDataset({
|
||||
name: 'Nums',
|
||||
data: [
|
||||
{ a: 1, b: 2 },
|
||||
{ a: 3, b: 4 },
|
||||
],
|
||||
format: 'json',
|
||||
source: 'inline',
|
||||
now: T,
|
||||
});
|
||||
useDatasetStore.getState().add(ds);
|
||||
const id = useDatasetStore.getState().datasets[0].id;
|
||||
useChartBuilderStore.getState().init(id);
|
||||
useChartBuilderStore.getState().setMark('bar'); // two measures on a bar → scatter hint
|
||||
|
||||
await act(async () => {
|
||||
root.render(<ChartBuilderModal />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// The hint renders a [Switch to Point] button (not just prose).
|
||||
const fixButton = Array.from(container.querySelectorAll('button')).find(
|
||||
(b) => b.textContent === 'Switch to Point',
|
||||
);
|
||||
expect(fixButton).toBeDefined();
|
||||
expect(container.textContent).toContain('scatter');
|
||||
|
||||
await act(async () => {
|
||||
fixButton!.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Applying it switches the mark and the hint re-derives away.
|
||||
expect(useChartBuilderStore.getState().config.mark).toBe('point');
|
||||
expect(container.textContent).not.toContain('scatter');
|
||||
});
|
||||
|
||||
test('shows the canvas-limit message when the chart resolves too large to render', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { renderSpec, ChartTooLargeError } = await import('../services/chart-renderer');
|
||||
vi.mocked(renderSpec).mockRejectedValueOnce(new ChartTooLargeError(200_000, 16_383));
|
||||
|
||||
const ds = createDataset({
|
||||
name: 'Big',
|
||||
data: [
|
||||
{ a: 1, b: 'x' },
|
||||
{ a: 2, b: 'y' },
|
||||
],
|
||||
format: 'json',
|
||||
source: 'inline',
|
||||
now: T,
|
||||
});
|
||||
useDatasetStore.getState().add(ds);
|
||||
const id = useDatasetStore.getState().datasets[0].id;
|
||||
useChartBuilderStore.getState().init(id);
|
||||
|
||||
await act(async () => {
|
||||
root.render(<ChartBuilderModal />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
// Drive the debounced render so renderSpec runs and rejects with the limit error.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(400);
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain('larger than the browser can draw on a canvas');
|
||||
expect(container.textContent).toContain('200,000'); // the measured height
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('shows the empty state when no dataset is loaded', async () => {
|
||||
await act(async () => {
|
||||
root.render(<ChartBuilderModal />);
|
||||
|
||||
Reference in New Issue
Block a user