139 lines
6.0 KiB
TypeScript
139 lines
6.0 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';
|
||
|
|
|
||
|
|
/* -------------------------------------------------------------------------
|
||
|
|
* Mocked-hook tests -- pure UI-logic coverage: the domain-match guard, that
|
||
|
|
* confirming calls the right context functions with tree-shaped arguments,
|
||
|
|
* and that standalone mode (empty siteDomain) hides the entry point
|
||
|
|
* entirely. The real `replaceAllPages`/`setHeader`/`setFooter`/
|
||
|
|
* `resetToDefaults` implementations (and whether the blank trees this
|
||
|
|
* component builds actually survive `treeToCraftState`/`sanitizeAiTree`) are
|
||
|
|
* covered separately, WITHOUT mocks, in
|
||
|
|
* `SiteDesignPanel.reset.integration.test.tsx` -- `vi.mock` is hoisted and
|
||
|
|
* file-scoped, so it can't be selectively "undone" partway through one file
|
||
|
|
* for a real-provider test.
|
||
|
|
* ---------------------------------------------------------------------- */
|
||
|
|
|
||
|
|
const replaceAllPages = vi.fn();
|
||
|
|
const setHeader = vi.fn();
|
||
|
|
const setFooter = vi.fn();
|
||
|
|
const resetToDefaults = vi.fn();
|
||
|
|
let mockSiteDomain = 'example.com';
|
||
|
|
|
||
|
|
vi.mock('../../state/PageContext', () => ({
|
||
|
|
usePages: () => ({ replaceAllPages, setHeader, setFooter, pages: [], siteDesign: {} }),
|
||
|
|
}));
|
||
|
|
vi.mock('../../state/SiteDesignContext', () => ({
|
||
|
|
useSiteDesign: () => ({ design: {}, updateDesign: vi.fn(), resetToDefaults }),
|
||
|
|
DEFAULT_SITE_DESIGN: {},
|
||
|
|
}));
|
||
|
|
vi.mock('../../state/EditorConfigContext', () => ({
|
||
|
|
useEditorConfig: () => ({ whpConfig: mockSiteDomain ? { siteDomain: mockSiteDomain } : null, isWHP: !!mockSiteDomain }),
|
||
|
|
}));
|
||
|
|
|
||
|
|
import { SiteDesignPanel } from './SiteDesignPanel';
|
||
|
|
|
||
|
|
let container: HTMLDivElement;
|
||
|
|
let root: Root;
|
||
|
|
|
||
|
|
function render() {
|
||
|
|
container = document.createElement('div');
|
||
|
|
document.body.appendChild(container);
|
||
|
|
act(() => {
|
||
|
|
root = createRoot(container);
|
||
|
|
root.render(<SiteDesignPanel />);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function unmount() {
|
||
|
|
act(() => { root.unmount(); });
|
||
|
|
container.remove();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** React tracks a controlled `<input>`'s value via a wrapped native setter --
|
||
|
|
* a plain `input.value = x` assignment doesn't go through it, so React never
|
||
|
|
* sees the change and skips onChange. Using the real native setter (same
|
||
|
|
* pattern as `MediaStylePanel.slides.test.tsx`) makes the subsequent
|
||
|
|
* `input` event register as a genuine value change. */
|
||
|
|
function setInputValue(input: HTMLInputElement, value: string) {
|
||
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')!.set!;
|
||
|
|
setter.call(input, value);
|
||
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Reset Entire Site -- guard + wiring (mocked hooks)', () => {
|
||
|
|
test('the confirm button is disabled until the domain is typed exactly', () => {
|
||
|
|
mockSiteDomain = 'example.com';
|
||
|
|
render();
|
||
|
|
act(() => { (container.querySelector('[data-action="open-site-reset"]') as HTMLButtonElement).click(); });
|
||
|
|
|
||
|
|
const confirm = container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement;
|
||
|
|
expect(confirm.disabled).toBe(true);
|
||
|
|
|
||
|
|
const input = container.querySelector('[data-testid="site-reset-domain"]') as HTMLInputElement;
|
||
|
|
act(() => { setInputValue(input, 'example.co'); });
|
||
|
|
expect((container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement).disabled).toBe(true);
|
||
|
|
|
||
|
|
act(() => { setInputValue(input, 'example.com'); });
|
||
|
|
expect((container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement).disabled).toBe(false);
|
||
|
|
|
||
|
|
unmount();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('confirming blanks pages, header, footer and design tokens', () => {
|
||
|
|
mockSiteDomain = 'example.com';
|
||
|
|
replaceAllPages.mockClear();
|
||
|
|
setHeader.mockClear();
|
||
|
|
setFooter.mockClear();
|
||
|
|
resetToDefaults.mockClear();
|
||
|
|
|
||
|
|
render();
|
||
|
|
act(() => { (container.querySelector('[data-action="open-site-reset"]') as HTMLButtonElement).click(); });
|
||
|
|
const input = container.querySelector('[data-testid="site-reset-domain"]') as HTMLInputElement;
|
||
|
|
act(() => { setInputValue(input, 'example.com'); });
|
||
|
|
act(() => { (container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement).click(); });
|
||
|
|
|
||
|
|
expect(replaceAllPages).toHaveBeenCalledTimes(1);
|
||
|
|
const pagesArg = replaceAllPages.mock.calls[0][0];
|
||
|
|
expect(pagesArg).toHaveLength(1);
|
||
|
|
expect(pagesArg[0].name).toBe('Home');
|
||
|
|
// The replacement is a SerializedTreeNode (recursive tree), not a flat
|
||
|
|
// craft-state entry: a real Container root with no children.
|
||
|
|
expect(pagesArg[0].tree.type.resolvedName).toBe('Container');
|
||
|
|
expect(pagesArg[0].tree.nodes).toEqual([]);
|
||
|
|
|
||
|
|
expect(setHeader).toHaveBeenCalledTimes(1);
|
||
|
|
expect(setHeader.mock.calls[0][0].type.resolvedName).toBe('Container');
|
||
|
|
expect(setFooter).toHaveBeenCalledTimes(1);
|
||
|
|
expect(setFooter.mock.calls[0][0].type.resolvedName).toBe('Container');
|
||
|
|
expect(resetToDefaults).toHaveBeenCalledTimes(1);
|
||
|
|
|
||
|
|
// The panel closes its own dialog back up after confirming.
|
||
|
|
expect(container.querySelector('[data-action="confirm-site-reset"]')).toBeNull();
|
||
|
|
expect(container.querySelector('[data-action="open-site-reset"]')).toBeTruthy();
|
||
|
|
|
||
|
|
unmount();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('standalone mode (no WHP_CONFIG, siteDomain "") hides the entry point entirely, ' +
|
||
|
|
'while non-standalone mode shows it -- an empty typed input must never trivially ' +
|
||
|
|
'satisfy the guard. Asserted as a contrast within one test (not "absence" alone) ' +
|
||
|
|
'so this fails if the whole feature -- not just the guard -- were ever removed.', () => {
|
||
|
|
mockSiteDomain = '';
|
||
|
|
render();
|
||
|
|
expect(container.querySelector('[data-action="open-site-reset"]')).toBeNull();
|
||
|
|
expect(container.querySelector('[data-testid="site-reset-domain"]')).toBeNull();
|
||
|
|
expect(container.textContent).not.toContain('Danger zone');
|
||
|
|
expect(container.textContent).not.toContain('Reset Entire Site');
|
||
|
|
unmount();
|
||
|
|
|
||
|
|
mockSiteDomain = 'example.com';
|
||
|
|
render();
|
||
|
|
expect(container.querySelector('[data-action="open-site-reset"]')).toBeTruthy();
|
||
|
|
expect(container.textContent).toContain('Danger zone');
|
||
|
|
unmount();
|
||
|
|
});
|
||
|
|
});
|