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>
76 lines
2.7 KiB
TypeScript
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);
|
|
});
|
|
});
|