mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 05:01:26 -07:00
test: cut low-value tests, rewrite the rest around behavior, parallelize lanes (#285)
This commit is contained in:
1 parent
4b6842d034
commit
b48083ddd8
368 files changed
+18045
-60331
No files matched your search
@@ -2,6 +2,41 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { apiClient, BASE_URL } from './api-client';
|
||||
|
||||
interface SeenRequest {
|
||||
url: string;
|
||||
method: string;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
/** Stubs `fetch` for one test body; every call is recorded in `requests`. */
|
||||
async function withFetchStub(
|
||||
respond: () => Response,
|
||||
run: (requests: SeenRequest[]) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: SeenRequest[] = [];
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
requests.push({
|
||||
url: String(input),
|
||||
method: init?.method ?? 'GET',
|
||||
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
||||
});
|
||||
return respond();
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await run(requests);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
|
||||
const jsonResponse = (payload: unknown) =>
|
||||
new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
test('getAnimeCoverUrl appends retry tokens for late cover refreshes', () => {
|
||||
const getAnimeCoverUrl = apiClient.getAnimeCoverUrl as (
|
||||
animeId: number,
|
||||
@@ -11,184 +46,53 @@ test('getAnimeCoverUrl appends retry tokens for late cover refreshes', () => {
|
||||
assert.equal(getAnimeCoverUrl(42, 3), `${BASE_URL}/api/stats/anime/42/cover?coverRetry=3`);
|
||||
});
|
||||
|
||||
test('getAnimeMergeRecommendations loads pending duplicate pairs', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(
|
||||
JSON.stringify({ recommendations: [{ recommendationId: 4, animeIds: [7, 8] }] }),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const result = await apiClient.getAnimeMergeRecommendations();
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/anime/merge-recommendations`);
|
||||
assert.deepEqual(result.recommendations, [{ recommendationId: 4, animeIds: [7, 8] }]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('dismissAnimeMergeRecommendation sends DELETE for the selected suggestion', 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(JSON.stringify({ ok: true }), { status: 200 });
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.dismissAnimeMergeRecommendation(4);
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/anime/merge-recommendations/4`);
|
||||
assert.equal(seenMethod, 'DELETE');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('getCoverImages batches anime and media cover requests', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
let seenMethod = '';
|
||||
let seenBody = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenUrl = String(input);
|
||||
seenMethod = init?.method ?? 'GET';
|
||||
seenBody = String(init?.body ?? '');
|
||||
return new Response(JSON.stringify({ anime: {}, media: {} }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.getCoverImages({ animeIds: [1, 1, 2], videoIds: [7, 7, 8] });
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/covers`);
|
||||
assert.equal(seenMethod, 'POST');
|
||||
assert.deepEqual(JSON.parse(seenBody), { animeIds: [1, 2], videoIds: [7, 8] });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
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('getCoverImages dedupes anime and media ids before batching the request', async () => {
|
||||
await withFetchStub(
|
||||
() => jsonResponse({ anime: {}, media: {} }),
|
||||
async (requests) => {
|
||||
await apiClient.getCoverImages({ animeIds: [1, 1, 2], videoIds: [7, 7, 8] });
|
||||
assert.deepEqual(requests, [
|
||||
{
|
||||
url: `${BASE_URL}/api/stats/covers`,
|
||||
method: 'POST',
|
||||
body: { animeIds: [1, 2], videoIds: [7, 8] },
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('searchSentences encodes realtime sentence search requests', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.searchSentences('猫 食べる', 25);
|
||||
assert.equal(
|
||||
seenUrl,
|
||||
`${BASE_URL}/api/stats/sentences/search?q=%E7%8C%AB+%E9%A3%9F%E3%81%B9%E3%82%8B&limit=25&headword=true`,
|
||||
);
|
||||
|
||||
await apiClient.searchSentences('猫 食べる', 25, false);
|
||||
assert.equal(
|
||||
seenUrl,
|
||||
`${BASE_URL}/api/stats/sentences/search?q=%E7%8C%AB+%E9%A3%9F%E3%81%B9%E3%82%8B&limit=25&headword=false`,
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
await withFetchStub(
|
||||
() => jsonResponse([]),
|
||||
async (requests) => {
|
||||
await apiClient.searchSentences('猫 食べる', 25);
|
||||
await apiClient.searchSentences('猫 食べる', 25, false);
|
||||
const query = 'q=%E7%8C%AB+%E9%A3%9F%E3%81%B9%E3%82%8B&limit=25';
|
||||
assert.deepEqual(
|
||||
requests.map((request) => request.url),
|
||||
[
|
||||
`${BASE_URL}/api/stats/sentences/search?${query}&headword=true`,
|
||||
`${BASE_URL}/api/stats/sentences/search?${query}&headword=false`,
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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: [] },
|
||||
librarySummary: [],
|
||||
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&fillEmpty=true`,
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
await withFetchStub(
|
||||
() => new Response('boom', { status: 500, statusText: 'Internal Server Error' }),
|
||||
async () => {
|
||||
await assert.rejects(() => apiClient.deleteSession(7), /Stats API error: 500 boom/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('getTrendsDashboard accepts 365d range and builds correct URL', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
await withFetchStub(
|
||||
() =>
|
||||
jsonResponse({
|
||||
activity: { watchTime: [], cards: [], words: [], sessions: [] },
|
||||
progress: {
|
||||
watchTime: [],
|
||||
@@ -201,113 +105,28 @@ test('getTrendsDashboard accepts 365d range and builds correct URL', async () =>
|
||||
},
|
||||
ratios: { lookupsPerHundred: [] },
|
||||
librarySummary: [],
|
||||
animeCumulative: {
|
||||
watchTime: [],
|
||||
episodes: [],
|
||||
cards: [],
|
||||
words: [],
|
||||
},
|
||||
patterns: {
|
||||
watchTimeByDayOfWeek: [],
|
||||
watchTimeByHour: [],
|
||||
},
|
||||
animeCumulative: { watchTime: [], episodes: [], cards: [], words: [] },
|
||||
patterns: { watchTimeByDayOfWeek: [], watchTimeByHour: [] },
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.getTrendsDashboard('365d', 'day', false);
|
||||
assert.equal(
|
||||
seenUrl,
|
||||
`${BASE_URL}/api/stats/trends/dashboard?range=365d&groupBy=day&fillEmpty=false`,
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
async (requests) => {
|
||||
await apiClient.getTrendsDashboard('365d', 'day', false);
|
||||
assert.equal(
|
||||
requests[0]?.url,
|
||||
`${BASE_URL}/api/stats/trends/dashboard?range=365d&groupBy=day&fillEmpty=false`,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('getSessionEvents can request only specific event types', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.getSessionEvents(42, 120, [4, 5, 6, 7, 8, 9]);
|
||||
assert.equal(
|
||||
seenUrl,
|
||||
`${BASE_URL}/api/stats/sessions/42/events?limit=120&types=4%2C5%2C6%2C7%2C8%2C9`,
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('getExcludedWords requests database-backed exclusions', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(JSON.stringify([{ headword: '猫', word: '猫', reading: 'ねこ' }]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const words = await apiClient.getExcludedWords();
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/excluded-words`);
|
||||
assert.deepEqual(words, [{ headword: '猫', word: '猫', reading: 'ねこ' }]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('setExcludedWords replaces database-backed exclusions', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
let seenMethod = '';
|
||||
let seenBody = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenUrl = String(input);
|
||||
seenMethod = init?.method ?? 'GET';
|
||||
seenBody = String(init?.body ?? '');
|
||||
return new Response(null, { status: 200 });
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.setExcludedWords([{ headword: '猫', word: '猫', reading: 'ねこ' }]);
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/excluded-words`);
|
||||
assert.equal(seenMethod, 'PUT');
|
||||
assert.deepEqual(JSON.parse(seenBody), {
|
||||
words: [{ headword: '猫', word: '猫', reading: 'ねこ' }],
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('getSessionTimeline requests full session history when limit is omitted', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.getSessionTimeline(42);
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/sessions/42/timeline`);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
await withFetchStub(
|
||||
() => jsonResponse([]),
|
||||
async (requests) => {
|
||||
await apiClient.getSessionEvents(42, 120, [4, 5, 6, 7, 8, 9]);
|
||||
assert.equal(
|
||||
requests[0]?.url,
|
||||
`${BASE_URL}/api/stats/sessions/42/events?limit=120&types=4%2C5%2C6%2C7%2C8%2C9`,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user