fix(builder): route live header/footer edits correctly on save
Editing the Header/Footer sets activePageId to '__header__'/'__footer__', which matches no entry in `pages`. save() was serializing the live canvas into the top-level page slots (mislabeled as page content, matching no page) while exporting header/footer from stale stored state — auto-save every 30s silently dropped header/footer edits. Extract buildSavePayload() as a pure, unit-tested helper: header/footer craft state now comes from the live canvas when that zone is being edited (else stored state), and the top-level page fields fall back to the landing page's stored state when a header/footer zone is active, so page content is never clobbered or mislabeled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { buildSavePayload } from './useWhpApi';
|
||||
import { PageData } from '../types';
|
||||
|
||||
const pageA: PageData = { id: 'home', name: 'Home', slug: 'index', craftState: 'STORED_HOME', headCode: '' };
|
||||
const pageB: PageData = { id: 'page_2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT', headCode: '' };
|
||||
const headerPage: PageData = { id: '__header__', name: 'Header', slug: '__header__', craftState: 'STALE_HEADER', headCode: '' };
|
||||
const footerPage: PageData = { id: '__footer__', name: 'Footer', slug: '__footer__', craftState: 'STALE_FOOTER', headCode: '' };
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user