fix(site-builder): final whole-branch review fixes
C1: HtmlBlock's PURIFY_CONFIG omitted 'style' from ALLOWED_ATTR, so the toolbar colour picker added in this branch was silently deleted by DOMPurify -- issue #2 was regressed, not fixed. Adds style/id plus table tags, with tests pinning the markup path in both render and toHtml. I3: PagesPanel's three confirmation states were not mutually exclusive; cancelling delete revealed an unbidden reset prompt on a destructive action. I5: orphan repair logged at console.warn, which the new console buffer cannot see -- the reporter would never capture the most diagnostic signal for the still-unreproduced drop bug. Also aligns useWhpApi's initial-load failure handling with loadState's fallback. I7: corrects comments (and the design spec) that asserted an orphan "renders somewhere on the canvas", which a mid-plan audit disproved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EditorConfigProvider } from '../state/EditorConfigContext';
|
||||
import { PageProvider, usePages } from '../state/PageContext';
|
||||
import { EMPTY_CANVAS, PageProvider, usePages } from '../state/PageContext';
|
||||
import { SiteDesignProvider } from '../state/SiteDesignContext';
|
||||
import { useWhpApi } from './useWhpApi';
|
||||
import { WhpConfig } from '../types';
|
||||
@@ -22,8 +22,10 @@ import { WhpConfig } from '../types';
|
||||
* This mocks `@craftjs/core` the same way `useWhpApi.load.test.tsx` and
|
||||
* `PageContext.orphan-repair-wiring.test.tsx` do, and asserts on the exact
|
||||
* string handed to the mocked `actions.deserialize` -- the orphan must be
|
||||
* reattached to ROOT and a `console.warn` must fire, exactly like the
|
||||
* page-switch path.
|
||||
* reattached to ROOT and a `console.error` must fire, exactly like the
|
||||
* page-switch path (I5 review: this was `console.warn`, which
|
||||
* console-buffer.ts -- feeding the in-builder issue reporter -- does not
|
||||
* capture).
|
||||
*/
|
||||
const deserializeMock = vi.fn();
|
||||
vi.mock('@craftjs/core', () => ({
|
||||
@@ -133,7 +135,7 @@ describe('useWhpApi load() repairs an orphaned node on the FIRST page before des
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const harness = render();
|
||||
|
||||
await act(async () => {
|
||||
@@ -148,11 +150,69 @@ describe('useWhpApi load() repairs an orphaned node on the FIRST page before des
|
||||
expect(parsed.stray.parent).toBe('ROOT');
|
||||
|
||||
// The observable signal that repair actually ran, not just that the
|
||||
// orphan happened to be absent for some unrelated reason.
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('reattached');
|
||||
// orphan happened to be absent for some unrelated reason. (React's own
|
||||
// act()-environment warnings also go through console.error in this
|
||||
// harness, so search all calls rather than assuming index 0.)
|
||||
expect(errorSpy.mock.calls.some((call) => String(call[0]).includes('reattached'))).toBe(true);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* I5 (review): `PageContext.loadState`'s deserialize failure path logs via
|
||||
* `console.error` AND falls back to `EMPTY_CANVAS` so the user always ends
|
||||
* up with a working (if blank) editor. `useWhpApi.load()`'s equivalent path
|
||||
* used to be `console.warn` with NO fallback deserialize -- the initial
|
||||
* load, which decides whether the user sees a working editor at all, both
|
||||
* failed harder (silently leaving the Frame undeserialized) and reported
|
||||
* quieter than every subsequent page switch. This pins the aligned
|
||||
* behaviour.
|
||||
*/
|
||||
describe('useWhpApi load() falls back to EMPTY_CANVAS when the first page state cannot be deserialized', () => {
|
||||
beforeEach(() => {
|
||||
deserializeMock.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('a deserialize failure on the first page logs console.error and retries with EMPTY_CANVAS', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
json: async () => ({
|
||||
success: true,
|
||||
project: {
|
||||
design: null,
|
||||
header_craft_state: null,
|
||||
footer_craft_state: null,
|
||||
pages_craft_state: [
|
||||
{ id: 'home', name: 'Home', slug: 'index', craftState: '{"ROOT":{"broken":true}}' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
deserializeMock.mockImplementationOnce(() => {
|
||||
throw new Error('malformed state');
|
||||
});
|
||||
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const harness = render();
|
||||
|
||||
await act(async () => {
|
||||
await harness.get().load();
|
||||
});
|
||||
|
||||
// First call (the broken state) threw; the second call is the fallback.
|
||||
expect(deserializeMock).toHaveBeenCalledTimes(2);
|
||||
expect(deserializeMock.mock.calls[1][0]).toBe(EMPTY_CANVAS);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('Failed to load page state:', expect.any(Error));
|
||||
|
||||
errorSpy.mockRestore();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import { useEditorConfig } from '../state/EditorConfigContext';
|
||||
import { usePages } from '../state/PageContext';
|
||||
import { usePages, EMPTY_CANVAS } from '../state/PageContext';
|
||||
import { useSiteDesign, SiteDesign } from '../state/SiteDesignContext';
|
||||
import { exportBodyHtml } from '../utils/html-export';
|
||||
import { repairOrphanNodes } from '../utils/orphan-repair';
|
||||
@@ -321,14 +321,27 @@ export function useWhpApi() {
|
||||
? firstPage.craftState : JSON.stringify(firstPage.craftState);
|
||||
const { state, repaired } = repairOrphanNodes(rawState);
|
||||
if (repaired.length > 0) {
|
||||
console.warn(
|
||||
// I5: console-buffer.ts only patches console.error, and this
|
||||
// reattach signal is the single most diagnostic clue for the
|
||||
// still-unreproduced "elements drop off the canvas" report --
|
||||
// it must reach the in-builder issue reporter's console buffer.
|
||||
console.error(
|
||||
`[site-builder] reattached ${repaired.length} unreachable node(s) to the page root:`,
|
||||
repaired.join(', '),
|
||||
);
|
||||
}
|
||||
actions.deserialize(state);
|
||||
} catch (e) {
|
||||
console.warn('Failed to load page state:', e);
|
||||
// I5: this is the initial load that decides whether the user
|
||||
// sees a working editor at all -- align with `loadState`'s
|
||||
// behaviour (console.error + a known-safe fallback) instead of
|
||||
// warning quietly and leaving the Frame on whatever it last had.
|
||||
console.error('Failed to load page state:', e);
|
||||
try {
|
||||
actions.deserialize(EMPTY_CANVAS);
|
||||
} catch (_e2) {
|
||||
// give up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user