import { describe, test, expect, vi } from 'vitest'; import React from 'react'; import { createRoot, Root } from 'react-dom/client'; import { act } from 'react-dom/test-utils'; import { PageProvider, usePages } from './PageContext'; /** * D6 regression coverage: `switchPage` and `deletePage` used to run their * side effects (actions.deserialize via loadState, setActivePageId, * activePageIdRef mutation) INSIDE a setPages/setHeaderPage/setFooterPage * functional updater. React (in , dev builds) invokes * updater functions passed to setState twice to help surface exactly this * kind of impurity — so the old code deserialized twice per page switch. * These tests mount under StrictMode and assert the side effect fires once. */ const deserializeMock = vi.fn(); vi.mock('@craftjs/core', () => ({ useEditor: () => ({ query: { serialize: () => '{}' }, actions: { deserialize: deserializeMock }, }), })); let container: HTMLDivElement; let root: Root; function render(ui: React.ReactElement) { container = document.createElement('div'); document.body.appendChild(container); act(() => { root = createRoot(container); root.render(ui); }); } function unmount() { act(() => { root.unmount(); }); container.remove(); } /** `loadState` defers `actions.deserialize` via a real setTimeout(fn, 0). */ async function flushTimers() { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }); } describe('PageContext pure updaters (D6)', () => { test('switchPage deserializes exactly once under StrictMode double-invocation', async () => { deserializeMock.mockClear(); let ctx: ReturnType | null = null; const Consumer: React.FC = () => { ctx = usePages(); return null; }; render( , ); act(() => { ctx!.addPage('About', 'about'); }); await flushTimers(); deserializeMock.mockClear(); act(() => { ctx!.switchPage('home'); }); await flushTimers(); expect(deserializeMock).toHaveBeenCalledTimes(1); expect(ctx!.activePageId).toBe('home'); unmount(); }); test('deletePage: deserialize fires exactly once when deleting the active page', async () => { deserializeMock.mockClear(); let ctx: ReturnType | null = null; const Consumer: React.FC = () => { ctx = usePages(); return null; }; render( , ); act(() => { ctx!.addPage('About', 'about'); }); await flushTimers(); const aboutId = ctx!.pages[1].id; act(() => { ctx!.switchPage(aboutId); }); await flushTimers(); deserializeMock.mockClear(); act(() => { ctx!.deletePage(aboutId); }); await flushTimers(); expect(deserializeMock).toHaveBeenCalledTimes(1); expect(ctx!.pages.length).toBe(1); expect(ctx!.activePageId).toBe('home'); unmount(); }); test("deletePage: can't delete the last remaining page", async () => { let ctx: ReturnType | null = null; const Consumer: React.FC = () => { ctx = usePages(); return null; }; render( , ); act(() => { ctx!.deletePage(ctx!.pages[0].id); }); expect(ctx!.pages.length).toBe(1); unmount(); }); });