mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-12 13:55:51 -07:00
fix(anime): scope preferences, paginate sources, harden installs
- Preference store keys entries by extension package + bridge source id; legacy unscoped entries are discarded once instead of being handed to whichever extension asks first - Source picker's "Load more" appends the next page without duplicating streamed results - Repository index fetches and subtitle/APK downloads now time out and are size-bounded instead of hanging or growing unbounded - APK installs are staged to a temp file and renamed into place - Stream metadata lookup matches the requested path, not only the currently playing one - Reworked animeui into browse-state/detail-panel/panels.css modules - Reverted premature CHANGELOG unreleased entries; refreshed anime-browser docs
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
beginBrowse,
|
||||
beginNextPage,
|
||||
capture,
|
||||
createBrowseState,
|
||||
failBrowse,
|
||||
finishBrowse,
|
||||
LatestRequest,
|
||||
safeUploadDate,
|
||||
soleBrowseRequest,
|
||||
takeUnseenEntries,
|
||||
} from './browse-state';
|
||||
import type { AnimeBrowserEntry } from '../types/anime-browser';
|
||||
|
||||
test('a new browse replaces results and starts from page one', () => {
|
||||
const started = beginBrowse(createBrowseState(), 'frieren');
|
||||
|
||||
assert.deepEqual(started.request, {
|
||||
id: 1,
|
||||
query: 'frieren',
|
||||
page: 1,
|
||||
append: false,
|
||||
});
|
||||
assert.equal(started.state.loading, true);
|
||||
});
|
||||
|
||||
test('the next page appends only when the current result has another page', () => {
|
||||
const first = beginBrowse(createBrowseState(), '');
|
||||
const ready = finishBrowse(first.state, first.request.id, true);
|
||||
const next = beginNextPage(ready);
|
||||
|
||||
assert.ok(next);
|
||||
assert.deepEqual(next.request, {
|
||||
id: 2,
|
||||
query: '',
|
||||
page: 2,
|
||||
append: true,
|
||||
});
|
||||
assert.equal(beginNextPage(next.state), null, 'cannot overlap page requests');
|
||||
assert.equal(beginNextPage(finishBrowse(next.state, next.request.id, false)), null);
|
||||
});
|
||||
|
||||
test('a stale page completion cannot change the active browse state', () => {
|
||||
const first = beginBrowse(createBrowseState(), 'old');
|
||||
const current = beginBrowse(first.state, 'new');
|
||||
|
||||
assert.equal(finishBrowse(current.state, first.request.id, true), current.state);
|
||||
});
|
||||
|
||||
test('stream updates are correlated only when one browse request is in flight', () => {
|
||||
const first = beginBrowse(createBrowseState(), 'old');
|
||||
const second = beginBrowse(first.state, 'new');
|
||||
const inFlight = new Map([
|
||||
[second.request.id, second.request],
|
||||
[first.request.id, first.request],
|
||||
]);
|
||||
|
||||
assert.equal(soleBrowseRequest(inFlight), null, 'start order is ambiguous while calls overlap');
|
||||
inFlight.delete(first.request.id);
|
||||
assert.equal(soleBrowseRequest(inFlight), second.request);
|
||||
});
|
||||
|
||||
test('a failed next page remains retryable', () => {
|
||||
const first = beginBrowse(createBrowseState(), '');
|
||||
const ready = finishBrowse(first.state, first.request.id, true);
|
||||
const next = beginNextPage(ready)!;
|
||||
const failed = failBrowse(next.state, next.request);
|
||||
const retry = beginNextPage(failed);
|
||||
|
||||
assert.ok(retry);
|
||||
assert.equal(retry.request.page, 2);
|
||||
});
|
||||
|
||||
test('latest request tokens invalidate closed and superseded details', () => {
|
||||
const requests = new LatestRequest();
|
||||
const first = requests.begin();
|
||||
const second = requests.begin();
|
||||
|
||||
assert.equal(requests.isCurrent(first), false);
|
||||
assert.equal(requests.isCurrent(second), true);
|
||||
requests.cancel();
|
||||
assert.equal(requests.isCurrent(second), false);
|
||||
});
|
||||
|
||||
test('safeUploadDate ignores malformed timestamps', () => {
|
||||
assert.equal(safeUploadDate(Date.UTC(2025, 3, 2)), '2025-04-02');
|
||||
assert.equal(safeUploadDate(Number.NaN), null);
|
||||
assert.equal(safeUploadDate(Number.POSITIVE_INFINITY), null);
|
||||
});
|
||||
|
||||
test('capture turns a rejected IPC operation into a displayable failure', async () => {
|
||||
const failure = new Error('bridge disconnected');
|
||||
const result = await capture(async () => Promise.reject(failure));
|
||||
|
||||
assert.deepEqual(result, { ok: false, error: failure });
|
||||
});
|
||||
|
||||
test('takeUnseenEntries deduplicates streamed and final-page entries', () => {
|
||||
const entry = (sourceId: string, url: string): AnimeBrowserEntry => ({
|
||||
sourceId,
|
||||
sourceName: sourceId,
|
||||
url,
|
||||
title: url,
|
||||
thumbnailUrl: null,
|
||||
});
|
||||
const seen = new Set<string>();
|
||||
|
||||
assert.deepEqual(takeUnseenEntries([entry('one', '/a')], seen), [entry('one', '/a')]);
|
||||
assert.deepEqual(takeUnseenEntries([entry('one', '/a'), entry('two', '/a')], seen), [
|
||||
entry('two', '/a'),
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user