Files
openchamber/packages/ui/src/lib/persistence.test.ts
T

174 lines
4.7 KiB
TypeScript
Raw Normal View History

import { afterAll, beforeEach, describe, expect, test } from 'bun:test';
import type { RuntimeAPIs, SettingsPayload } from '@/lib/api/types';
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { applyPersistedHomeDirectoryToWindow, updateDesktopSettings } from './persistence';
type TestWindow = {
__OPENCHAMBER_HOME__?: string;
dispatchEvent: (event: Event) => boolean;
};
let createdWindow = false;
let createdLocalStorage = false;
const ensureLocalStorage = (): void => {
if (typeof localStorage !== 'undefined') {
return;
}
const values = new Map<string, string>();
Object.defineProperty(globalThis, 'localStorage', {
value: {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => {
values.set(key, value);
},
removeItem: (key: string) => {
values.delete(key);
},
clear: () => {
values.clear();
},
},
configurable: true,
writable: true,
});
createdLocalStorage = true;
};
const getWindow = (): TestWindow => {
if (typeof window === 'undefined') {
Object.defineProperty(globalThis, 'window', {
value: {},
configurable: true,
writable: true,
});
createdWindow = true;
}
const testWindow = window as unknown as Partial<TestWindow>;
testWindow.dispatchEvent ??= () => true;
ensureLocalStorage();
return testWindow as TestWindow;
};
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
const registerSettingsSave = (save: (changes: Partial<SettingsPayload>) => Promise<SettingsPayload>): void => {
registerRuntimeAPIs({
runtime: { platform: 'web', isDesktop: false, isVSCode: false },
settings: {
load: async () => ({ settings: {}, source: 'web' }),
save,
},
} as unknown as RuntimeAPIs);
};
afterAll(() => {
registerRuntimeAPIs(null);
if (createdWindow) {
delete (globalThis as { window?: unknown }).window;
} else if (typeof window !== 'undefined') {
delete getWindow().__OPENCHAMBER_HOME__;
}
if (createdLocalStorage) {
delete (globalThis as { localStorage?: unknown }).localStorage;
}
});
describe('applyPersistedHomeDirectoryToWindow', () => {
beforeEach(() => {
delete getWindow().__OPENCHAMBER_HOME__;
});
test('does not overwrite an injected desktop home directory', () => {
getWindow().__OPENCHAMBER_HOME__ = '/Users/example';
applyPersistedHomeDirectoryToWindow('/Users/example/projects/app');
expect(getWindow().__OPENCHAMBER_HOME__).toBe('/Users/example');
});
test('uses persisted home when no runtime home was injected', () => {
applyPersistedHomeDirectoryToWindow('/Users/example/projects/app');
expect(getWindow().__OPENCHAMBER_HOME__).toBe('/Users/example/projects/app');
});
});
describe('updateDesktopSettings', () => {
beforeEach(() => {
getWindow();
registerRuntimeAPIs(null);
});
test('waits for the debounced settings save to finish before resolving', async () => {
let saveStarted = false;
let saveFinished = false;
let updateResolved = false;
registerSettingsSave(async () => {
saveStarted = true;
await delay(100);
saveFinished = true;
return {};
});
const update = updateDesktopSettings({
skillCatalogs: [{ id: 'custom:test', label: 'Test', source: 'owner/repo' }],
});
update.then(() => {
updateResolved = true;
}).catch(() => {
updateResolved = true;
});
await delay(50);
expect(saveStarted).toBe(false);
expect(updateResolved).toBe(false);
await delay(200);
expect(saveStarted).toBe(true);
expect(saveFinished).toBe(false);
expect(updateResolved).toBe(false);
await update;
expect(saveFinished).toBe(true);
expect(updateResolved).toBe(true);
});
test('coalesces rapid settings updates and resolves every caller after one merged save', async () => {
const saveCalls: Array<Partial<SettingsPayload>> = [];
let firstResolved = false;
let secondResolved = false;
registerSettingsSave(async (changes) => {
saveCalls.push(changes);
await delay(50);
return {};
});
const first = updateDesktopSettings({ themeVariant: 'dark' });
first.then(() => {
firstResolved = true;
}).catch(() => {
firstResolved = true;
});
await delay(50);
const second = updateDesktopSettings({ fontSize: 14 });
second.then(() => {
secondResolved = true;
}).catch(() => {
secondResolved = true;
});
await Promise.all([first, second]);
expect(saveCalls).toEqual([{ themeVariant: 'dark', fontSize: 14 }]);
expect(firstResolved).toBe(true);
expect(secondResolved).toBe(true);
});
});