mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-08-29 00:41:25 +08:00
001e1308cf
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.
76 lines
2.5 KiB
TypeScript
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();
|
|
});
|
|
});
|