import { describe, test, expect, afterEach, beforeEach } from 'vitest'; import React from 'react'; import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness'; import { ProjectExtrasProvider, useProjectExtras } from '../../state/ProjectExtrasContext'; import { SaveGuardProvider } from '../../state/SaveGuardContext'; import { SiteDesignProvider } from '../../state/SiteDesignContext'; import { __resetCommandsForTests, getCommand } from '../../editor/commands'; import { SectionLibraryModal, openSectionLibrary, closeSectionLibrary } from './SectionLibraryModal'; import { BlocksPanel } from '../left/BlocksPanel'; let h: EditorHarness; let extrasApi: ReturnType; const Grab: React.FC = () => { extrasApi = useProjectExtras(); return null; }; function mount() { h = renderEditorHarness({ wrap: (tree) => ( {tree} ), }); h.mountChild(<>); } const $ = (sel: string) => h.container.querySelector(sel) as HTMLElement | null; const $$ = (sel: string) => [...h.container.querySelectorAll(sel)] as HTMLElement[]; const rootKids = () => h.query.node('ROOT').get().data.nodes as string[]; const type = (el: HTMLInputElement, v: string) => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!; h.act(() => { setter.call(el, v); el.dispatchEvent(new Event('input', { bubbles: true })); }); }; beforeEach(() => __resetCommandsForTests()); afterEach(() => { h?.unmount(); closeSectionLibrary(); }); describe('SectionLibraryModal', () => { test('closed by default; Browse sections button in the Blocks panel opens it', () => { mount(); expect($('.section-lib')).toBeNull(); h.act(() => { ($$('button').find((b) => b.textContent?.includes('Browse sections')))!.click(); }); expect($('.section-lib')).not.toBeNull(); expect($$('.section-lib-card').length).toBeGreaterThan(0); }); test('thumbnails are sandboxed, script-less iframes in the site palette', () => { mount(); h.act(() => openSectionLibrary()); const frame = $('iframe.section-lib-frame') as HTMLIFrameElement; expect(frame.getAttribute('sandbox')).toBe(''); expect(frame.getAttribute('srcdoc')).toContain('--wsb-primary:'); }); test('family chip filters and search narrows', () => { mount(); h.act(() => openSectionLibrary()); const chip = (l: string) => $$('.section-lib-chip').find((c) => c.textContent === l)!; h.act(() => chip('Hero').click()); expect($$('.section-lib-card').map((c) => c.dataset.sectionId)).toEqual(['hero-centered']); h.act(() => chip('All').click()); type($('.section-lib-search') as HTMLInputElement, 'form'); expect($$('.section-lib-card').map((c) => c.dataset.sectionId)).toContain('contact-form'); type($('.section-lib-search') as HTMLInputElement, 'zzzz'); expect($$('.section-lib-card')).toHaveLength(0); }); test('clicking a card inserts the section, selects it and closes', () => { mount(); h.act(() => openSectionLibrary()); h.act(() => { ($('[data-section-id="hero-centered"] .section-lib-pick') as HTMLElement).click(); }); expect(rootKids()).toHaveLength(1); expect(h.query.getEvent('selected').all()).toEqual([rootKids()[0]]); expect($('.section-lib')).toBeNull(); }); test('registers insert.section-library and site.save-as-section (only for a top-level section)', () => { mount(); expect(getCommand('insert.section-library')).toBeTruthy(); const save = getCommand('site.save-as-section')!; const ctx = (ids: string[]) => ({ query: h.query, selectedIds: ids } as any); expect(save.when!(ctx([]))).toBe(false); h.act(() => openSectionLibrary()); h.act(() => { ($('[data-section-id="hero-centered"] .section-lib-pick') as HTMLElement).click(); }); const id = rootKids()[0]; expect(save.when!(ctx([id]))).toBe(true); const inner = h.query.node(id).decendants(true)[0]; expect(save.when!(ctx([inner]))).toBe(false); }); test('save current section -> extras.customSections, shown under My sections, deletable', () => { mount(); h.act(() => openSectionLibrary()); h.act(() => { ($('[data-section-id="hero-centered"] .section-lib-pick') as HTMLElement).click(); }); h.act(() => openSectionLibrary()); h.act(() => { $$('button').find((b) => b.textContent?.includes('Save current section'))!.click(); }); type($('input[aria-label="Section name"]') as HTMLInputElement, 'My hero'); h.act(() => { $$('button').find((b) => b.textContent === 'Save')!.click(); }); const saved = extrasApi.get('customSections', []); expect(saved).toHaveLength(1); expect(saved[0]).toMatchObject({ name: 'My hero', family: 'custom' }); expect(saved[0].tree.type.resolvedName).toBe('Section'); expect($$('.section-lib-card').map((c) => c.dataset.sectionId)).toEqual([saved[0].id]); h.act(() => { ($('.section-lib-delete') as HTMLElement).click(); }); expect(extrasApi.get('customSections', [])).toHaveLength(0); }); test('sections loaded silently after mount (replaceAll) appear in My sections on open', () => { mount(); // Mirrors useWhpApi: the project load calls replaceAll, which does not re-render. h.act(() => { extrasApi.replaceAll({ customSections: [{ id: 'custom-loaded', name: 'Loaded hero', family: 'custom', createdAt: 1, tree: { type: { resolvedName: 'Section' }, props: {} }, }], }); }); h.act(() => openSectionLibrary()); expect($$('.section-lib-card').map((c) => c.dataset.sectionId)).toContain('custom-loaded'); }); });