Files
site-builder/craft/src/App.dirty.test.tsx
T
shadowdaoandClaude Sonnet 5.5 9418444916 fix(site-builder): honest save state (dirty tracking, status pill, beforeunload, error notice)
Track dirty from Craft node changes and page/design/header/footer state, cleared
only by a successful save. Persistent Unsaved/Saving/Saved/Published/Save failed
pill, beforeunload prompt while dirty or saving, one persistent error notice,
resp.ok/non-JSON handling for save and publish, and publish refuses to run
after a failed save. Draft badge no longer flips on a no-op autosave.

Refs #30

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:07:03 -07:00

76 lines
2.7 KiB
TypeScript

import { describe, test, expect, afterEach } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import { Frame, Element, useEditor } from '@craftjs/core';
import { SaveAwareEditor } from './App';
import { Container } from './components/layout/Container';
import { ensureJsdomShims } from './test-utils/editorHarness';
import { EditorConfigProvider } from './state/EditorConfigContext';
import { SaveGuardProvider, useSaveGuard } from './state/SaveGuardContext';
import { WhpConfig } from './types';
/**
* Against a REAL Craft editor: onNodesChange (what App wires to markDirty)
* must fire for content edits and must NOT fire for selection or for the
* load's own deserialize -- otherwise a freshly opened site would warn
* "unsaved changes" on close.
*/
const whpConfig: WhpConfig = {
user: 'u', apiUrl: '/api', csrfToken: 't', siteId: 1,
siteDomain: 'example.com', siteName: 'Example', backUrl: '/', isRoot: false,
};
let api: { actions: any; query: any; guard: ReturnType<typeof useSaveGuard> };
const Capture: React.FC = () => {
const { actions, query } = useEditor();
const guard = useSaveGuard();
api = { actions, query, guard };
return null;
};
let root: Root;
let container: HTMLDivElement;
afterEach(() => {
act(() => { root.unmount(); });
container.remove();
});
const STATE = JSON.stringify({
ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: {}, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {} },
});
describe('canvas dirty tracking on a real Craft editor', () => {
test('load deserialize and selection stay clean; a prop edit makes it dirty', async () => {
ensureJsdomShims();
container = document.createElement('div');
document.body.appendChild(container);
act(() => {
root = createRoot(container);
root.render(
<EditorConfigProvider config={whpConfig}>
<SaveGuardProvider>
<SaveAwareEditor>
<Frame><Element is={Container} canvas style={{}} /></Frame>
<Capture />
</SaveAwareEditor>
</SaveGuardProvider>
</EditorConfigProvider>,
);
});
// What LoadGate does: deserialize while loading, then flip to loaded.
act(() => { api.actions.deserialize(STATE); });
act(() => { api.guard.setLoadState('loaded'); });
await act(async () => { await Promise.resolve(); });
expect(api.guard.dirty).toBe(false);
act(() => { api.actions.selectNode('ROOT'); });
expect(api.guard.dirty).toBe(false);
act(() => { api.actions.setProp('ROOT', (p: any) => { p.tag = 'section'; }); });
expect(api.guard.dirty).toBe(true);
});
});