vibe-proxy/frontend/tests/quotaUiState.test.ts
2026-08-24 00:10:41 +02:00

75 lines
2.5 KiB
TypeScript

/**
* Session-scoped quota page preferences.
*
* The merge-on-write case is the one that matters: the tab strip and the sort
* control each write a single field and know nothing about the other, so a
* whole-object write would make changing a tab silently reset the sort.
*/
import { afterAll, beforeEach, describe, expect, test } from 'vitest';
import { readQuotaUiState, writeQuotaUiState } from '@/features/quota/uiState';
const KEY = 'quotaPage.uiState';
/** Test files share one process — leaving a fake `window` behind would leak. */
const originalWindow = (globalThis as { window?: unknown }).window;
/** The Node test environment has no sessionStorage; a Map-backed stub is enough here. */
function installSessionStorage() {
const store = new Map<string, string>();
const storage = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => void store.set(key, value),
removeItem: (key: string) => void store.delete(key),
clear: () => store.clear(),
key: (index: number) => [...store.keys()][index] ?? null,
get length() {
return store.size;
},
};
(globalThis as unknown as { window: unknown }).window = { sessionStorage: storage };
return storage;
}
let storage: ReturnType<typeof installSessionStorage>;
beforeEach(() => {
storage = installSessionStorage();
});
afterAll(() => {
if (originalWindow === undefined) {
delete (globalThis as { window?: unknown }).window;
} else {
(globalThis as { window?: unknown }).window = originalWindow;
}
});
describe('quota ui state', () => {
test('round-trips both preferences', () => {
writeQuotaUiState({ tab: 'codex', sortMode: 'soonest' });
expect(readQuotaUiState()).toEqual({ tab: 'codex', sortMode: 'soonest' });
});
test('writing one preference preserves the other', () => {
writeQuotaUiState({ sortMode: 'soonest' });
writeQuotaUiState({ tab: 'kimi' });
expect(readQuotaUiState()).toEqual({ tab: 'kimi', sortMode: 'soonest' });
});
test('rejects values that are not part of the current contract', () => {
storage.setItem(KEY, JSON.stringify({ tab: 'not-a-tab', sortMode: 'by-vibes' }));
expect(readQuotaUiState()).toEqual({ tab: undefined, sortMode: undefined });
});
test('survives absent, malformed, and non-object payloads', () => {
expect(readQuotaUiState()).toBeNull();
storage.setItem(KEY, '{not json');
expect(readQuotaUiState()).toBeNull();
storage.setItem(KEY, '"a string"');
expect(readQuotaUiState()).toBeNull();
});
});