Files
Supra4E8C 001e1308cf feat(quota): add a soonest-recovery-first sort to the quota page
Card order was fixed: provider grouping, then whatever order the backend
listed files in. With more than a handful of credentials, "which one is
usable next" meant reading every card.

The new sort ranks credentials by their nearest upcoming reset, across
windows and reset credits alike, and it runs before pagination so the
answer is global rather than per-page.

Quota is click-to-fetch, so most of the list has no instant to sort by.
Those credentials sink to the bottom keeping their provider-grouped
order — the unloaded tail still reads like the default view — and slot
into place as their data arrives. The comparator takes the instant as an
injected resolver, which keeps it store-free and directly testable.

writeQuotaUiState now merges. It previously wrote the whole object, so
adding a second preference would have made changing a tab silently reset
the sort.

The Select sits inside the existing tabs reveal node rather than beside
it: useRevealGroup staggers every [data-reveal] descendant, and a
sibling would have added a cascade step. Reordering relies on stable
card keys, so React moves nodes instead of remounting them — no FLIP and
no replayed entrance, because a re-sort triggered by a minute tick
should not move things with fanfare.
2026-08-02 01:54:20 +08:00

76 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 'bun:test';
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;
/** bun's global 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();
});
});