Files
site-builder/craft/src/editor/NoticeHost.test.tsx
T
shadowdaoandClaude Sonnet 5.5 21c2776c88 feat(site-builder): block registry, notices, local prefs, project extras, overlay slot
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>
2026-10-07 11:14:51 -07:00

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());
});
});