Files
feuerwerk/tests/editor/session-teardown.test.ts
T

148 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* What has to let go when a session ends (#177): signing out takes the editor down while the page
* stays up, so everything it registered outside itself needs a way back – the autosave's timer and
* its `beforeunload` listener, and the handlers panels push into the focus manager. A handler left
* behind would go on typing into an editor nobody can see.
*
* The editor as a whole is torn down in the browser (e2e step 45); the pieces are here.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import { createEmptyProject, type Project } from '../../src/core/model/Project';
import type { ProjectStore } from '../../src/core/store/ProjectStore';
import { attachAutosave } from '../../src/editor/autosave';
import { EditorState } from '../../src/editor/state';
/** Just enough `window` for the two modules under test; vitest runs these in node. */
function fakeWindow() {
let next = 1;
const timers = new Map<number, () => void>();
const listeners: [string, unknown][] = [];
return {
listeners,
/** Runs every timer that is still scheduled, newest first, as the browser eventually would. */
fire: () => {
for (const run of [...timers.values()]) run();
timers.clear();
},
setTimeout: (fn: () => void) => {
const id = next++;
timers.set(id, fn);
return id;
},
clearTimeout: (id: number) => void timers.delete(id),
addEventListener: (type: string, fn: unknown) => void listeners.push([type, fn]),
removeEventListener: (type: string, fn: unknown) => {
const index = listeners.findIndex(([t, f]) => t === type && f === fn);
if (index >= 0) listeners.splice(index, 1);
},
};
}
function savingStore(saves: Project[]): ProjectStore {
return {
list: () => Promise.resolve([]),
load: () => Promise.reject(new Error('not used')),
save: (project, revision) => {
saves.push(project);
return Promise.resolve(revision + 1);
},
create: () => Promise.reject(new Error('not used')),
delete: () => Promise.resolve(),
};
}
describe('autosave', () => {
let win: ReturnType<typeof fakeWindow>;
beforeEach(() => {
win = fakeWindow();
vi.stubGlobal('window', win);
});
afterEach(() => vi.unstubAllGlobals());
const dirtyState = () => {
const state = new EditorState();
state.loadProject(createEmptyProject('p1', 'Teardown'), 1);
state.execute(new CreateAssetCommand('sprite', 'Crate'));
return state;
};
it('writes what is pending when it is flushed', async () => {
const saves: Project[] = [];
const state = dirtyState();
const autosave = attachAutosave(state, savingStore(saves));
expect(state.saveStatus).toBe('dirty');
await autosave.flush();
expect(saves).toHaveLength(1);
expect(state.saveStatus).toBe('saved');
expect(state.revision).toBe(2);
});
it('stops for good: no pending save fires, and the page listener is gone', async () => {
const saves: Project[] = [];
const state = dirtyState();
const autosave = attachAutosave(state, savingStore(saves));
expect(win.listeners.map(([type]) => type)).toEqual(['beforeunload']);
autosave.stop();
win.fire();
await Promise.resolve();
expect(saves).toEqual([]);
expect(win.listeners).toEqual([]);
});
it('a change after the stop schedules nothing any more', async () => {
const saves: Project[] = [];
const state = dirtyState();
const autosave = attachAutosave(state, savingStore(saves));
await autosave.flush();
autosave.stop();
state.execute(new CreateAssetCommand('sprite', 'Box'));
win.fire();
await Promise.resolve();
expect(saves).toHaveLength(1);
});
});
describe('focus manager handlers', () => {
beforeEach(() => vi.stubGlobal('window', fakeWindow()));
afterEach(() => vi.unstubAllGlobals());
it('hands back the call that removes a global key handler', async () => {
const { FocusManager } = await import('../../src/ui/FocusManager');
const { Container } = await import('pixi.js');
const focus = new FocusManager(new Container());
const seen: string[] = [];
const remove = focus.onGlobalKey((event) => {
seen.push(event.key);
return false;
});
expect(focus.globalHandlers).toHaveLength(1);
remove();
expect(focus.globalHandlers).toEqual([]);
// A second call is harmless – a widget may be destroyed after the editor already let go.
remove();
expect(seen).toEqual([]);
});
it('hands back the call that removes a focus-change handler', async () => {
const { FocusManager } = await import('../../src/ui/FocusManager');
const { Container } = await import('pixi.js');
const focus = new FocusManager(new Container());
let runs = 0;
const remove = focus.onFocusChange(() => runs++);
const other = focus.onFocusChange(() => runs++);
expect(focus.changeHandlers).toHaveLength(2);
remove();
expect(focus.changeHandlers).toHaveLength(1);
other();
expect(focus.changeHandlers).toEqual([]);
expect(runs).toBe(0);
});
});