mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart builder: intent front door, Heatmap mark, role-aware guidance
This commit is contained in:
@@ -475,4 +475,117 @@ describe('ChartBuilderModal', () => {
|
||||
// A colour picker renders for the constant.
|
||||
expect(container.querySelector('input[type="color"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
// The intent front door's core logic (which chip lights, what each applies, gating)
|
||||
// is covered in core/store; these check only the parts that live in the component —
|
||||
// the toolbar's roving tabindex and arrow navigation (a hand-rolled handler, not a
|
||||
// shared primitive), and that a click reshapes the chart.
|
||||
describe('intent front door (the "What do you want to show?" strip)', () => {
|
||||
// Two categories + one date + two measures → every intent applies; enough shape to
|
||||
// light a default and to exercise an applied intent (Heatmap needs two categories).
|
||||
const seedSuperstore = () => {
|
||||
const ds = createDataset({
|
||||
name: 'Superstore',
|
||||
data: [
|
||||
{ region: 'E', segment: 'A', date: '2026-01-01', sales: 5, profit: 1 },
|
||||
{ region: 'W', segment: 'B', date: '2026-02-01', sales: 9, profit: 3 },
|
||||
],
|
||||
format: 'json',
|
||||
source: 'inline',
|
||||
now: T,
|
||||
});
|
||||
useDatasetStore.getState().add(ds);
|
||||
const id = useDatasetStore.getState().datasets[0].id;
|
||||
useChartBuilderStore.getState().init(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
const chips = (): HTMLButtonElement[] =>
|
||||
Array.from(container.querySelectorAll<HTMLButtonElement>('[role="toolbar"] button'));
|
||||
|
||||
test('renders one tab stop and lights the active intent (roving tabindex)', async () => {
|
||||
seedSuperstore();
|
||||
await act(async () => {
|
||||
root.render(<ChartBuilderModal />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const all = chips();
|
||||
expect(all.length).toBe(7); // every intent shows (Tableau Show Me: never hidden)
|
||||
// Exactly one chip is in the tab order; the rest are roving (-1).
|
||||
expect(all.filter((b) => b.tabIndex === 0)).toHaveLength(1);
|
||||
// The smart default for a category+count shape is Compare, so its chip is pressed.
|
||||
const pressed = all.filter((b) => b.getAttribute('aria-pressed') === 'true');
|
||||
expect(pressed).toHaveLength(1);
|
||||
expect(pressed[0].textContent).toBe('Compare');
|
||||
});
|
||||
|
||||
test('an inapplicable intent is disabled and names its reason', async () => {
|
||||
// One category, one measure, no date and no second measure → Correlation/Time/
|
||||
// Heatmap/Part-to-whole cannot apply.
|
||||
const ds = createDataset({
|
||||
name: 'Thin',
|
||||
data: [{ region: 'E', sales: 5 }],
|
||||
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();
|
||||
});
|
||||
|
||||
const correlation = chips().find((b) => b.textContent === 'Correlation')!;
|
||||
expect(correlation.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(correlation.getAttribute('aria-label')).toMatch(/needs two number columns/);
|
||||
});
|
||||
|
||||
test('clicking an enabled chip reshapes the chart to that intent', async () => {
|
||||
seedSuperstore();
|
||||
await act(async () => {
|
||||
root.render(<ChartBuilderModal />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const heatmap = chips().find((b) => b.textContent === 'Heatmap')!;
|
||||
await act(async () => {
|
||||
heatmap.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(useChartBuilderStore.getState().config.mark).toBe('rect');
|
||||
expect(useChartBuilderStore.getState().config.encodings.color).toEqual({
|
||||
type: 'quantitative',
|
||||
aggregate: 'count',
|
||||
});
|
||||
});
|
||||
|
||||
test('ArrowRight moves focus along the toolbar without applying (focus-only)', async () => {
|
||||
seedSuperstore();
|
||||
await act(async () => {
|
||||
root.render(<ChartBuilderModal />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const all = chips();
|
||||
const start = all.findIndex((b) => b.tabIndex === 0);
|
||||
const markBefore = useChartBuilderStore.getState().config.mark;
|
||||
|
||||
await act(async () => {
|
||||
all[start].focus();
|
||||
all[start].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }),
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Focus moved to the next chip; the chart is untouched (arrows navigate, Enter applies).
|
||||
expect(document.activeElement).toBe(all[(start + 1) % all.length]);
|
||||
expect(useChartBuilderStore.getState().config.mark).toBe(markBefore);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user