148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
|
|
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 <React.StrictMode>, 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<typeof usePages> | null = null;
|
||
|
|
const Consumer: React.FC = () => {
|
||
|
|
ctx = usePages();
|
||
|
|
return null;
|
||
|
|
};
|
||
|
|
|
||
|
|
render(
|
||
|
|
<React.StrictMode>
|
||
|
|
<PageProvider>
|
||
|
|
<Consumer />
|
||
|
|
</PageProvider>
|
||
|
|
</React.StrictMode>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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<typeof usePages> | null = null;
|
||
|
|
const Consumer: React.FC = () => {
|
||
|
|
ctx = usePages();
|
||
|
|
return null;
|
||
|
|
};
|
||
|
|
|
||
|
|
render(
|
||
|
|
<React.StrictMode>
|
||
|
|
<PageProvider>
|
||
|
|
<Consumer />
|
||
|
|
</PageProvider>
|
||
|
|
</React.StrictMode>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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<typeof usePages> | null = null;
|
||
|
|
const Consumer: React.FC = () => {
|
||
|
|
ctx = usePages();
|
||
|
|
return null;
|
||
|
|
};
|
||
|
|
|
||
|
|
render(
|
||
|
|
<React.StrictMode>
|
||
|
|
<PageProvider>
|
||
|
|
<Consumer />
|
||
|
|
</PageProvider>
|
||
|
|
</React.StrictMode>,
|
||
|
|
);
|
||
|
|
|
||
|
|
act(() => {
|
||
|
|
ctx!.deletePage(ctx!.pages[0].id);
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(ctx!.pages.length).toBe(1);
|
||
|
|
|
||
|
|
unmount();
|
||
|
|
});
|
||
|
|
});
|