mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-21 00:11:27 -07:00
Add navigation state machine for tab/detail routing, anime overview stats with Yomitan lookup rates, session word count accuracy fixes, vocabulary tab hook order fix, simplified trends data fetching from backend-aggregated endpoints, and improved session detail charts.
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { apiClient, BASE_URL, resolveStatsBaseUrl } from './api-client';
|
|
|
|
test('resolveStatsBaseUrl prefers apiBase query parameter for file-based overlay mode', () => {
|
|
const baseUrl = resolveStatsBaseUrl({
|
|
protocol: 'file:',
|
|
origin: 'null',
|
|
search: '?overlay=1&apiBase=http%3A%2F%2F127.0.0.1%3A6123',
|
|
});
|
|
|
|
assert.equal(baseUrl, 'http://127.0.0.1:6123');
|
|
});
|
|
|
|
test('resolveStatsBaseUrl falls back to configured window origin for browser mode', () => {
|
|
const baseUrl = resolveStatsBaseUrl({
|
|
protocol: 'http:',
|
|
origin: 'http://127.0.0.1:6123',
|
|
search: '',
|
|
});
|
|
|
|
assert.equal(baseUrl, 'http://127.0.0.1:6123');
|
|
});
|
|
|
|
test('resolveStatsBaseUrl keeps legacy localhost fallback for file mode without apiBase', () => {
|
|
const baseUrl = resolveStatsBaseUrl({
|
|
protocol: 'file:',
|
|
origin: 'null',
|
|
search: '?overlay=1',
|
|
});
|
|
|
|
assert.equal(baseUrl, 'http://127.0.0.1:6969');
|
|
});
|
|
|
|
test('deleteSession sends a DELETE request to the session endpoint', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let seenUrl = '';
|
|
let seenMethod = '';
|
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
seenUrl = String(input);
|
|
seenMethod = init?.method ?? 'GET';
|
|
return new Response(null, { status: 200 });
|
|
}) as typeof globalThis.fetch;
|
|
|
|
try {
|
|
await apiClient.deleteSession(42);
|
|
assert.equal(seenUrl, `${BASE_URL}/api/stats/sessions/42`);
|
|
assert.equal(seenMethod, 'DELETE');
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('deleteSession throws when the stats API delete request fails', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async () =>
|
|
new Response('boom', {
|
|
status: 500,
|
|
statusText: 'Internal Server Error',
|
|
})) as typeof globalThis.fetch;
|
|
|
|
try {
|
|
await assert.rejects(() => apiClient.deleteSession(7), /Stats API error: 500 boom/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('getTrendsDashboard requests the chart-ready trends endpoint with range and grouping', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let seenUrl = '';
|
|
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
seenUrl = String(input);
|
|
return new Response(
|
|
JSON.stringify({
|
|
activity: { watchTime: [], cards: [], words: [], sessions: [] },
|
|
progress: {
|
|
watchTime: [],
|
|
sessions: [],
|
|
words: [],
|
|
newWords: [],
|
|
cards: [],
|
|
episodes: [],
|
|
lookups: [],
|
|
},
|
|
ratios: { lookupsPerHundred: [] },
|
|
animePerDay: {
|
|
episodes: [],
|
|
watchTime: [],
|
|
cards: [],
|
|
words: [],
|
|
lookups: [],
|
|
lookupsPerHundred: [],
|
|
},
|
|
animeCumulative: {
|
|
watchTime: [],
|
|
episodes: [],
|
|
cards: [],
|
|
words: [],
|
|
},
|
|
patterns: {
|
|
watchTimeByDayOfWeek: [],
|
|
watchTimeByHour: [],
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
);
|
|
}) as typeof globalThis.fetch;
|
|
|
|
try {
|
|
await apiClient.getTrendsDashboard('90d', 'month');
|
|
assert.equal(
|
|
seenUrl,
|
|
`${BASE_URL}/api/stats/trends/dashboard?range=90d&groupBy=month`,
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|