import { describe, test, expect } from 'vitest'; import { buildSavePayload } from './useWhpApi'; import { PageData } from '../types'; import { DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext'; const pageA: PageData = { id: 'home', name: 'Home', slug: 'index', craftState: 'STORED_HOME' }; const pageB: PageData = { id: 'page_2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT' }; const headerPage: PageData = { id: '__header__', name: 'Header', slug: '__header__', craftState: 'STALE_HEADER' }; const footerPage: PageData = { id: '__footer__', name: 'Footer', slug: '__footer__', craftState: 'STALE_FOOTER' }; describe('buildSavePayload', () => { test('editing header: live serialize lands in header_craft_state, not in any page slot', () => { const payload = buildSavePayload({ siteId: 1, siteName: 'Test Site', liveCraftState: 'LIVE_HEADER', pages: [pageA, pageB], headerPage, footerPage, activePageId: '__header__', isEditingHeader: true, isEditingFooter: false, headCode: DEFAULT_SITE_DESIGN.headCode, design: DEFAULT_SITE_DESIGN, }); // The fresh live canvas must be reflected in header_craft_state, not the stale stored one. expect(payload.header_craft_state).toBe('LIVE_HEADER'); expect(payload.header_craft_state).not.toBe('STALE_HEADER'); // Footer must remain untouched (stored state, since we're not editing it). expect(payload.footer_craft_state).toBe('STALE_FOOTER'); // Live header content must never leak into a page slot or the top-level page fields. expect(payload.craft_state).not.toBe('LIVE_HEADER'); for (const p of payload.pages_craft_state) { expect(p.craftState).not.toBe('LIVE_HEADER'); } // Top-level page fields should fall back to the landing page's own stored state. expect(payload.craft_state).toBe('STORED_HOME'); // Per-page slots must reflect each page's own stored state, untouched. expect(payload.pages_craft_state.find((p) => p.id === 'home')?.craftState).toBe('STORED_HOME'); expect(payload.pages_craft_state.find((p) => p.id === 'page_2')?.craftState).toBe('STORED_ABOUT'); }); test('editing footer: live serialize lands in footer_craft_state, not in any page slot', () => { const payload = buildSavePayload({ siteId: 1, siteName: 'Test Site', liveCraftState: 'LIVE_FOOTER', pages: [pageA, pageB], headerPage, footerPage, activePageId: '__footer__', isEditingHeader: false, isEditingFooter: true, headCode: DEFAULT_SITE_DESIGN.headCode, design: DEFAULT_SITE_DESIGN, }); expect(payload.footer_craft_state).toBe('LIVE_FOOTER'); expect(payload.footer_craft_state).not.toBe('STALE_FOOTER'); expect(payload.header_craft_state).toBe('STALE_HEADER'); expect(payload.craft_state).not.toBe('LIVE_FOOTER'); for (const p of payload.pages_craft_state) { expect(p.craftState).not.toBe('LIVE_FOOTER'); } }); test('editing a real page: behavior unchanged — live serialize goes to that page + top-level, header/footer come from stored state', () => { const payload = buildSavePayload({ siteId: 1, siteName: 'Test Site', liveCraftState: 'LIVE_PAGE_HOME', pages: [pageA, pageB], headerPage, footerPage, activePageId: 'home', isEditingHeader: false, isEditingFooter: false, headCode: DEFAULT_SITE_DESIGN.headCode, design: DEFAULT_SITE_DESIGN, }); // Live canvas goes to the active page and top-level slots. expect(payload.craft_state).toBe('LIVE_PAGE_HOME'); expect(payload.pages_craft_state.find((p) => p.id === 'home')?.craftState).toBe('LIVE_PAGE_HOME'); // Other pages keep their stored state. expect(payload.pages_craft_state.find((p) => p.id === 'page_2')?.craftState).toBe('STORED_ABOUT'); // Header/footer come from stored (stale-but-correct, since we're not editing them) state. expect(payload.header_craft_state).toBe('STALE_HEADER'); expect(payload.footer_craft_state).toBe('STALE_FOOTER'); }); test('I-1: activePageId matches no page (dangling, e.g. original Home deleted then reload reset it to "home") -- live serialize still lands in pages_craft_state[0] / index.html, not lost', () => { // pages[0] has id 'page_x' (the replacement landing page after the // original 'home' was deleted); activePageId is stuck at the stale // default 'home', which matches no entry in `pages`. const pageX: PageData = { id: 'page_x', name: 'Home', slug: 'index', craftState: 'STORED_X' }; const p2: PageData = { id: 'p2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT_2' }; const payload = buildSavePayload({ siteId: 1, siteName: 'Test Site', liveCraftState: 'LIVE_EDIT', pages: [pageX, p2], headerPage, footerPage, activePageId: 'home', isEditingHeader: false, isEditingFooter: false, headCode: DEFAULT_SITE_DESIGN.headCode, design: DEFAULT_SITE_DESIGN, }); // The live edit must land in pages_craft_state[0] (index.html slot), // not be silently dropped to only the legacy top-level fields. expect(payload.pages_craft_state[0].craftState).toBe('LIVE_EDIT'); expect(payload.pages[0].filename).toBe('index.html'); // Top-level fields (legacy) should also reflect the live edit. expect(payload.craft_state).toBe('LIVE_EDIT'); // The other page is untouched. expect(payload.pages_craft_state.find((p) => p.id === 'p2')?.craftState).toBe('STORED_ABOUT_2'); }); test('head code + design tokens are included in the save payload', () => { const design = { ...DEFAULT_SITE_DESIGN, headCode: '' }; const payload = buildSavePayload({ siteId: 1, siteName: 'Test Site', liveCraftState: 'LIVE_PAGE_HOME', pages: [pageA, pageB], headerPage, footerPage, activePageId: 'home', isEditingHeader: false, isEditingFooter: false, headCode: '', design, }); expect(payload.head_code).toBe(''); expect(payload.design).toEqual(design); expect(payload.design.headCode).toBe(''); }); });