import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { createRoot, Root } from 'react-dom/client'; import { act } from 'react-dom/test-utils'; /* NavStylePanel (via useNodeProp/LinkPicker in this file) needs useEditor from @craftjs/core and usePages from PageContext. Mock both following the DOM-harness pattern used across this repo's other *StylePanel tests (no @testing-library/react here) -- PageContext itself is Wave-2's territory, this package only READS pages via usePages(), so mocking it is the correct boundary for these tests. */ const setPropSpy = vi.fn((_id: string, updater: (p: any) => void) => { updater(lastProps); }); let lastProps: any; vi.mock('@craftjs/core', () => ({ useEditor: () => ({ actions: { setProp: setPropSpy } }), })); let mockPages: { id: string; name: string; slug: string; craftState: string | null }[] = []; vi.mock('../../../state/PageContext', () => ({ usePages: () => ({ pages: mockPages }), })); vi.mock('../../../ui/AssetPicker', () => ({ AssetPicker: () => null, })); import { NavStylePanel, LinkPicker, pageHref } from './NavStylePanel'; 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 rerender(ui: React.ReactElement) { act(() => { root.render(ui); }); } function unmount() { act(() => { root.unmount(); }); container.remove(); } function setValue(el: HTMLInputElement | HTMLSelectElement, value: string) { const proto = el instanceof HTMLSelectElement ? window.HTMLSelectElement.prototype : window.HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set!; act(() => { setter.call(el, value); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }); } function click(el: Element | null) { act(() => { (el as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); }); } beforeEach(() => { setPropSpy.mockClear(); mockPages = [ { id: 'home', name: 'Home', slug: 'index', craftState: null }, { id: 'about', name: 'About', slug: 'about', craftState: null }, ]; }); afterEach(() => { if (container) unmount(); }); describe('pageHref (landing page is always "/")', () => { test('index 0 (landing page) -> "/"', () => { expect(pageHref(mockPages[0], 0)).toBe('/'); }); test('any other page -> "/{slug}"', () => { expect(pageHref(mockPages[1], 1)).toBe('/about'); }); }); describe('LinkPicker (F1: link-to-page picker + manual URL/anchor/tel/mailto)', () => { test('a value matching a page href starts in "Page" mode and lists every page', () => { render(); const [modeSelect] = Array.from(container.querySelectorAll('select')) as HTMLSelectElement[]; expect(modeSelect.value).toBe('page'); const optionText = Array.from(container.querySelectorAll('option')).map((o) => o.textContent); expect(optionText).toContain('Home'); expect(optionText).toContain('About'); }); test('picking a different page from the page dropdown emits that page\'s href', () => { const onChange = vi.fn(); render(); const [, pageSelect] = Array.from(container.querySelectorAll('select')) as HTMLSelectElement[]; setValue(pageSelect, '/'); expect(onChange).toHaveBeenCalledWith('/'); }); test('a "#section" value starts in Anchor mode', () => { render(); const [modeSelect] = Array.from(container.querySelectorAll('select')) as HTMLSelectElement[]; expect(modeSelect.value).toBe('anchor'); const input = container.querySelector('input[type="text"]') as HTMLInputElement; expect(input.value).toBe('#pricing'); }); test('switching mode to Anchor seeds a bare "#"', () => { const onChange = vi.fn(); render(); const [modeSelect] = Array.from(container.querySelectorAll('select')) as HTMLSelectElement[]; setValue(modeSelect, 'anchor'); expect(onChange).toHaveBeenCalledWith('#'); }); test('mailto: helper strips the scheme for editing and re-adds it on change', () => { const onChange = vi.fn(); render(); const input = container.querySelector('input[type="text"]') as HTMLInputElement; expect(input.value).toBe('foo@example.com'); setValue(input, 'bar@example.com'); expect(onChange).toHaveBeenCalledWith('mailto:bar@example.com'); }); test('tel: helper strips the scheme for editing and re-adds it on change', () => { const onChange = vi.fn(); render(); const input = container.querySelector('input[type="text"]') as HTMLInputElement; expect(input.value).toBe('5551234567'); setValue(input, '5559876543'); expect(onChange).toHaveBeenCalledWith('tel:5559876543'); }); test('a plain https:// URL falls back to Custom URL mode', () => { render(); const [modeSelect] = Array.from(container.querySelectorAll('select')) as HTMLSelectElement[]; expect(modeSelect.value).toBe('url'); }); }); describe('NavStylePanel Links section: href set via LinkPicker (F1 wired into the panel)', () => { test('choosing a page for a Navbar link writes that page\'s href onto the link', () => { lastProps = { links: [{ text: 'Home', href: '/old-home' }], }; render(); const selects = Array.from(container.querySelectorAll('select')) as HTMLSelectElement[]; // First select for the one link item is its LinkPicker mode select (the // href starts as a Custom URL, so it opens on "url" mode); switch it to // "page" then pick the About page from the resulting page dropdown. const modeSelect = selects[0]; setValue(modeSelect, 'page'); expect(setPropSpy).toHaveBeenCalled(); expect(lastProps.links[0].href).toBe('/'); // defaults to the first page // The mock setProp mutates `lastProps` in place rather than triggering a // real Craft.js state update, so force a re-render (passing the same, // now-mutated, object) to get the LinkPicker to reflect its new "page" // mode and render the page