mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Editor: composition structure wireframe (read-only)
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* CompositionWireframe — the read-only structure tree (arch 08 / arch 10 §5).
|
||||
*
|
||||
* Guards the load-bearing behavior: the affordance is hidden for a single-view
|
||||
* spec (nothing to schematize), the disclosed panel is an APG tree with one
|
||||
* roving tab stop, arrow keys walk it in document order, and activating a box
|
||||
* asks the editor to reveal that view's exact source range. The viewTree model
|
||||
* itself is covered in core (spec-view-tree.test.ts).
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { useAppStore } from '../stores/AppStore';
|
||||
import { usePopoverStore } from '../stores/PopoverStore';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import { CompositionWireframe } from './CompositionWireframe';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const COMPOSED = JSON.stringify({ vconcat: [{ mark: 'point' }, { mark: 'bar' }] }, null, 2);
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const setSpec = (text: string) => {
|
||||
useSnippetStore.getState().reset();
|
||||
useSnippetStore.setState({ draftText: text });
|
||||
};
|
||||
|
||||
const treeItems = () =>
|
||||
Array.from(document.body.querySelectorAll<HTMLElement>('[role="treeitem"]'));
|
||||
const item = (key: string) => document.body.querySelector<HTMLElement>(`[data-key="${key}"]`)!;
|
||||
|
||||
async function renderOpen() {
|
||||
await act(async () => {
|
||||
root.render(<CompositionWireframe />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
await act(async () => {
|
||||
usePopoverStore.getState().show('composition-wireframe');
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
usePopoverStore.setState({ openId: null });
|
||||
useAppStore.setState({ revealTarget: null });
|
||||
setSpec(COMPOSED);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
usePopoverStore.setState({ openId: null });
|
||||
useAppStore.setState({ revealTarget: null });
|
||||
useSnippetStore.getState().reset();
|
||||
});
|
||||
|
||||
describe('CompositionWireframe', () => {
|
||||
test('renders nothing for a single-view spec', () => {
|
||||
setSpec('{"mark":"point"}');
|
||||
act(() => root.render(<CompositionWireframe />));
|
||||
expect(container.querySelector('button')).toBeNull();
|
||||
});
|
||||
|
||||
test('shows a structure trigger for a composed spec', () => {
|
||||
act(() => root.render(<CompositionWireframe />));
|
||||
expect(container.querySelector('button[aria-controls="composition-wireframe"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('discloses an APG tree: the vconcat root and its two leaf views', async () => {
|
||||
await renderOpen();
|
||||
expect(document.body.querySelector('[role="tree"]')).not.toBeNull();
|
||||
expect(treeItems().map((el) => el.dataset.key)).toEqual(['root', 'vconcat|0', 'vconcat|1']);
|
||||
});
|
||||
|
||||
test('roving tabindex: exactly one treeitem is in the tab order', async () => {
|
||||
await renderOpen();
|
||||
expect(treeItems().filter((el) => el.tabIndex === 0)).toHaveLength(1);
|
||||
expect(item('root').tabIndex).toBe(0); // the root holds it on open
|
||||
});
|
||||
|
||||
test('ArrowDown moves the roving tab stop in document order', async () => {
|
||||
await renderOpen();
|
||||
act(() => {
|
||||
item('root').dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
});
|
||||
expect(item('root').tabIndex).toBe(-1);
|
||||
expect(item('vconcat|0').tabIndex).toBe(0);
|
||||
});
|
||||
|
||||
test('activating a box asks the editor to reveal that view’s source range', async () => {
|
||||
await renderOpen();
|
||||
act(() => item('vconcat|1').click());
|
||||
const target = useAppStore.getState().revealTarget!;
|
||||
expect(COMPOSED.slice(target.offset, target.offset + target.length)).toContain('"bar"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user