maestro/ui/src/lib/urlState.test.ts
oss-sync 5502478636
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (5b6df2f)
2026-06-10 08:40:41 +00:00

204 lines
6.2 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import {
readUiUrlState,
buildUiUrlStateSearch,
paramsEqualExcept,
COLUMN_LABELS,
COLUMN_LIST,
type UiUrlState,
} from './urlState';
const DEFAULT_STATE: UiUrlState = {
page: 'tasks',
repo: '',
status: 'all',
search: '',
sort: 'updated',
scope: 'mine',
detailTab: 'overview',
mobileTab: 'chat',
taskId: null,
};
function stubLocation(search: string) {
vi.stubGlobal('window', { location: { search } });
}
afterEach(() => vi.unstubAllGlobals());
describe('readUiUrlState', () => {
it('returns defaults when window is undefined (SSR/node)', () => {
// node environment: no window global by default
expect(readUiUrlState()).toEqual(DEFAULT_STATE);
});
it('returns defaults for an empty query string', () => {
stubLocation('');
const state = readUiUrlState();
expect(state).toEqual({
...DEFAULT_STATE,
section: undefined,
piece: undefined,
});
});
it('parses every valid parameter', () => {
stubLocation(
'?page=settings&repo=local%2Ftask-1&status=running&q=hello&sort=title' +
'&tab=files&mobileTab=activity&task=42&section=auth&piece=chat' +
'&pieceSource=user-custom&dashboardWidget=gpu&help=intro',
);
const state = readUiUrlState();
expect(state.page).toBe('settings');
expect(state.repo).toBe('local/task-1');
expect(state.status).toBe('running');
expect(state.search).toBe('hello');
expect(state.sort).toBe('title');
expect(state.detailTab).toBe('files');
expect(state.mobileTab).toBe('activity');
expect(state.taskId).toBe(42);
expect(state.section).toBe('auth');
expect(state.piece).toBe('chat');
expect(state.pieceSource).toBe('user-custom');
expect(state.dashboardWidget).toBe('gpu');
expect(state.help).toBe('intro');
});
it('falls back to defaults on invalid enum values', () => {
stubLocation('?page=bogus&status=bogus&sort=bogus&tab=bogus&mobileTab=bogus&section=bogus&pieceSource=bogus');
const state = readUiUrlState();
expect(state.page).toBe('tasks');
expect(state.status).toBe('all');
expect(state.sort).toBe('updated');
expect(state.detailTab).toBe('overview');
expect(state.mobileTab).toBe('chat');
expect(state.section).toBeUndefined();
expect(state.pieceSource).toBeUndefined();
});
it('rejects non-positive and non-numeric task ids', () => {
for (const task of ['0', '-5', 'abc', '']) {
stubLocation(`?task=${task}`);
expect(readUiUrlState().taskId).toBeNull();
}
stubLocation('?task=7');
expect(readUiUrlState().taskId).toBe(7);
});
it('still parses legacy settings section ids (soft bookmark landing)', () => {
for (const legacy of ['gateway-keys', 'provider', 'workspace', 'browser-sessions']) {
stubLocation(`?section=${legacy}`);
expect(readUiUrlState().section).toBe(legacy);
}
});
it('treats an empty piece param as undefined', () => {
stubLocation('?piece=');
expect(readUiUrlState().piece).toBeUndefined();
});
it('omits optional spread keys entirely when their params are absent', () => {
stubLocation('?page=tasks');
const state = readUiUrlState();
expect('pieceSource' in state).toBe(false);
expect('dashboardWidget' in state).toBe(false);
expect('help' in state).toBe(false);
});
});
describe('buildUiUrlStateSearch', () => {
it('produces an empty string for the default state', () => {
expect(buildUiUrlStateSearch(DEFAULT_STATE)).toBe('');
});
it('only serializes non-default values', () => {
const search = buildUiUrlStateSearch({
...DEFAULT_STATE,
status: 'failed',
taskId: 12,
});
const params = new URLSearchParams(search);
expect(params.get('status')).toBe('failed');
expect(params.get('task')).toBe('12');
expect(Array.from(params.keys()).sort()).toEqual(['status', 'task']);
});
it('omits the default dashboardWidget (worker-status) but keeps others', () => {
expect(
buildUiUrlStateSearch({ ...DEFAULT_STATE, dashboardWidget: 'worker-status' }),
).toBe('');
expect(
buildUiUrlStateSearch({ ...DEFAULT_STATE, dashboardWidget: 'gpu' }),
).toBe('dashboardWidget=gpu');
});
it('round-trips through readUiUrlState', () => {
const state: UiUrlState = {
page: 'help',
repo: 'local/task-9',
status: 'waiting_human',
search: 'foo bar',
sort: 'status',
scope: 'all',
detailTab: 'trace',
mobileTab: 'files',
taskId: 99,
section: 'reflection',
piece: 'research',
pieceSource: 'builtin',
dashboardWidget: 'queue',
help: 'pieces',
};
stubLocation(`?${buildUiUrlStateSearch(state)}`);
expect(readUiUrlState()).toEqual(state);
});
});
describe('paramsEqualExcept', () => {
it('considers identical params equal', () => {
const a = new URLSearchParams('a=1&b=2');
const b = new URLSearchParams('a=1&b=2');
expect(paramsEqualExcept(a, b, [])).toBe(true);
});
it('ignores key order', () => {
const a = new URLSearchParams('b=2&a=1');
const b = new URLSearchParams('a=1&b=2');
expect(paramsEqualExcept(a, b, [])).toBe(true);
});
it('detects value differences', () => {
const a = new URLSearchParams('a=1');
const b = new URLSearchParams('a=2');
expect(paramsEqualExcept(a, b, [])).toBe(false);
});
it('skips ignored keys on either side', () => {
const a = new URLSearchParams('a=1&tab=files');
const b = new URLSearchParams('a=1&tab=trace');
expect(paramsEqualExcept(a, b, ['tab'])).toBe(true);
const c = new URLSearchParams('a=1');
expect(paramsEqualExcept(a, c, ['tab'])).toBe(true);
});
it('detects extra non-ignored keys', () => {
const a = new URLSearchParams('a=1&extra=x');
const b = new URLSearchParams('a=1');
expect(paramsEqualExcept(a, b, [])).toBe(false);
});
it('handles repeated keys deterministically', () => {
const a = new URLSearchParams('k=2&k=1');
const b = new URLSearchParams('k=1&k=2');
expect(paramsEqualExcept(a, b, [])).toBe(true);
});
});
describe('column constants', () => {
it('every status column has a label', () => {
for (const col of COLUMN_LIST) {
expect(COLUMN_LABELS[col], `label for ${col}`).toBeTruthy();
}
});
});