Add an exported block registry (keywords, search, insertBlock) consumed by BlocksPanel, a NoticeProvider/NoticeHost toast stack, a guarded localStorage prefs helper, a ProjectExtras store persisted as project.json 'extras', and a CanvasOverlays mount point. Refs #38 Refs #41 Refs #49 Refs #48 Refs #57 Refs #58 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
import { describe, test, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
import React from 'react';
|
|
import { createRoot, Root } from 'react-dom/client';
|
|
import { act } from 'react-dom/test-utils';
|
|
import { NoticeProvider, useNotices } from '../state/NoticeContext';
|
|
import { NoticeHost } from './NoticeHost';
|
|
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
let api: ReturnType<typeof useNotices>;
|
|
|
|
const Probe: React.FC = () => { api = useNotices(); return null; };
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
act(() => {
|
|
root = createRoot(container);
|
|
root.render(<NoticeProvider><Probe /><NoticeHost /></NoticeProvider>);
|
|
});
|
|
});
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('NoticeHost', () => {
|
|
test('success renders role=status and disappears after timeoutMs', () => {
|
|
act(() => { api.notify({ kind: 'success', message: 'Saved', timeoutMs: 1000 }); });
|
|
const el = container.querySelector('[role="status"]');
|
|
expect(el?.textContent).toContain('Saved');
|
|
act(() => { vi.advanceTimersByTime(999); });
|
|
expect(container.textContent).toContain('Saved');
|
|
act(() => { vi.advanceTimersByTime(2); });
|
|
expect(container.textContent).not.toContain('Saved');
|
|
});
|
|
|
|
test('default timeout is 4000ms for info', () => {
|
|
act(() => { api.notify({ kind: 'info', message: 'FYI' }); });
|
|
act(() => { vi.advanceTimersByTime(3900); });
|
|
expect(container.textContent).toContain('FYI');
|
|
act(() => { vi.advanceTimersByTime(200); });
|
|
expect(container.textContent).not.toContain('FYI');
|
|
});
|
|
|
|
test('error is role=alert and sticky until dismissed', () => {
|
|
let id = '';
|
|
act(() => { id = api.notify({ kind: 'error', message: 'Boom' }); });
|
|
expect(container.querySelector('[role="alert"]')?.textContent).toContain('Boom');
|
|
act(() => { vi.advanceTimersByTime(60000); });
|
|
expect(container.textContent).toContain('Boom');
|
|
act(() => { (container.querySelector('.notice-dismiss') as HTMLButtonElement).click(); });
|
|
expect(container.textContent).not.toContain('Boom');
|
|
expect(id).toBeTruthy();
|
|
});
|
|
|
|
test('action button invokes onAction', () => {
|
|
const onAction = vi.fn();
|
|
act(() => { api.notify({ kind: 'info', message: 'Undo?', actionLabel: 'Undo', onAction, timeoutMs: 0 }); });
|
|
act(() => { (container.querySelector('.notice-action') as HTMLButtonElement).click(); });
|
|
expect(onAction).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('without a provider notify is a console-only no-op', () => {
|
|
const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
let r: Root; const c = document.createElement('div');
|
|
let a!: ReturnType<typeof useNotices>;
|
|
const P: React.FC = () => { a = useNotices(); return null; };
|
|
act(() => { r = createRoot(c); r.render(<P />); });
|
|
expect(() => a.notify({ kind: 'info', message: 'x' })).not.toThrow();
|
|
expect(spy).toHaveBeenCalled();
|
|
spy.mockRestore();
|
|
act(() => r.unmount());
|
|
});
|
|
});
|