mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add snippet metadata panel, duplicate, and immediate-load preview (M4.5)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { createSnippet } from '@core/snippet';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import { SnippetLibrary } from './SnippetLibrary';
|
||||
|
||||
// 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;
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
useSnippetStore.getState().reset();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
/** Set a controlled input/textarea's value the way React expects, then fire input. */
|
||||
function typeInto(el: HTMLInputElement | HTMLTextAreaElement, value: string) {
|
||||
const proto =
|
||||
el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
||||
// The native value setter bypasses React 19's input value tracking so the
|
||||
// synthetic input event registers as a real change.
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set!;
|
||||
setter.call(el, value);
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
describe('SnippetLibrary metadata panel (spec §02)', () => {
|
||||
test('renders the active snippet name, comment, and linked datasets without looping', async () => {
|
||||
const s = {
|
||||
...createSnippet({ id: 'a', name: 'Bar chart', now: new Date('2026-01-01T00:00:00Z') }),
|
||||
comment: 'a note',
|
||||
datasetRefs: ['Sales'],
|
||||
};
|
||||
useSnippetStore.getState().hydrate([s], 'a');
|
||||
|
||||
// If the auto-save effect looped, this act() would throw "Maximum update depth".
|
||||
await act(async () => {
|
||||
root.render(<SnippetLibrary />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const name = container.querySelector('input') as HTMLInputElement;
|
||||
const comment = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
expect(name.value).toBe('Bar chart');
|
||||
expect(comment.value).toBe('a note');
|
||||
expect(container.textContent).toContain('Linked datasets');
|
||||
expect(container.textContent).toContain('Sales');
|
||||
});
|
||||
|
||||
test('auto-saves an inline name edit after the debounce', () => {
|
||||
vi.useFakeTimers();
|
||||
const s = createSnippet({ id: 'a', name: 'Old', now: new Date('2026-01-01T00:00:00Z') });
|
||||
useSnippetStore.getState().hydrate([s], 'a');
|
||||
|
||||
act(() => {
|
||||
root.render(<SnippetLibrary />);
|
||||
});
|
||||
|
||||
const name = container.querySelector('input') as HTMLInputElement;
|
||||
act(() => typeInto(name, 'Renamed'));
|
||||
// Before the debounce fires, the store is unchanged.
|
||||
expect(useSnippetStore.getState().snippets[0].name).toBe('Old');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
expect(useSnippetStore.getState().snippets[0].name).toBe('Renamed');
|
||||
});
|
||||
|
||||
test('Duplicate adds an independent copy and makes it active', async () => {
|
||||
const s = createSnippet({ id: 'a', name: 'Chart', now: new Date('2026-01-01T00:00:00Z') });
|
||||
useSnippetStore.getState().hydrate([s], 'a');
|
||||
|
||||
await act(async () => {
|
||||
root.render(<SnippetLibrary />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const dup = [...container.querySelectorAll('button')].find(
|
||||
(b) => b.textContent === 'Duplicate',
|
||||
)!;
|
||||
await act(async () => {
|
||||
dup.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const { snippets, activeSnippetId } = useSnippetStore.getState();
|
||||
expect(snippets).toHaveLength(2);
|
||||
const active = snippets.find((x) => x.id === activeSnippetId)!;
|
||||
expect(active.name).toBe('Chart (copy)');
|
||||
expect(active.id).not.toBe('a');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user