66 lines
3.5 KiB
JavaScript
66 lines
3.5 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
|
|
// Exercise the actual app functions without DOM/player/network dependencies.
|
|
const source = fs.readFileSync(require('node:path').join(__dirname, 'app.js'), 'utf8');
|
|
function harness({ autoPreload = true, cached = [], result = { ok: true, cached: true }, error } = {}) {
|
|
const calls = [], jobs = new Map();
|
|
const video = { id: 'benchmedia01', title: 'Local clip' };
|
|
const context = vm.createContext({
|
|
data: { settings: { autoPreload }, playlists: [{ id: 'pl', videos: [video] }], pinnedPlaylists: [] },
|
|
cachedIds: new Set(cached), downloading: new Set(), downloadMeta: new Map(), saveControllers: new Map(),
|
|
view: { type: 'home' }, current: null, window: {}, AbortController,
|
|
slim: v => v, markCardCacheState() {}, renderSidebar() {}, updateDownloadBadge() {}, toast() {},
|
|
SaveSlots: { acquire: async () => {}, release() {} },
|
|
SaveQueue: { add: v => jobs.set(v.id, { status: 'queued' }), remove: id => jobs.delete(id), progress() {}, state: (id, status, message) => jobs.set(id, { status, message }) },
|
|
API: { cacheDownload: async id => { calls.push(id); if (error) throw error; return result; } },
|
|
recordDeviceFile() {}, warmThumb() {}, thumbUrlFor: () => '', cacheMutations: 0,
|
|
});
|
|
const preload = source.slice(source.indexOf('async function preload(video,'), source.indexOf('// A device copy that failed to play.'));
|
|
const playlists = source.slice(source.indexOf('function preloadPlaylist(pl)'), source.indexOf('// Update a single card\'s saved badge'));
|
|
vm.runInContext(preload + playlists, context);
|
|
return { context, calls, jobs, video };
|
|
}
|
|
|
|
test('auto preload tops up playlists, while pinned playlists save with the setting off', async () => {
|
|
const h = harness({ autoPreload: false });
|
|
h.context.preloadPlaylist(h.context.data.playlists[0]);
|
|
await new Promise(r => setImmediate(r));
|
|
assert.deepEqual(h.calls, []);
|
|
h.context.data.pinnedPlaylists = ['pl'];
|
|
h.context.preloadPinnedPlaylists();
|
|
await new Promise(r => setImmediate(r));
|
|
assert.deepEqual(h.calls, [h.video.id]);
|
|
assert.ok(h.context.cachedIds.has(h.video.id));
|
|
assert.equal(h.jobs.size, 0);
|
|
assert.equal(h.context.downloading.size, 0);
|
|
const enabled = harness();
|
|
enabled.context.preloadPlaylist(enabled.context.data.playlists[0]);
|
|
await new Promise(r => setImmediate(r));
|
|
assert.deepEqual(enabled.calls, [enabled.video.id]);
|
|
});
|
|
|
|
test('a repeated save shares the in-flight operation and does not download an existing copy', async () => {
|
|
const h = harness();
|
|
await Promise.all([h.context.preload(h.video), h.context.preload(h.video)]);
|
|
await h.context.preload(h.video);
|
|
assert.deepEqual(h.calls, [h.video.id]);
|
|
assert.ok(h.context.cachedIds.has(h.video.id));
|
|
assert.equal(h.context.saveControllers.size, 0);
|
|
});
|
|
|
|
test('failed and interrupted saves do not acquire a Saved badge and can be attempted again', async () => {
|
|
for (const options of [{ result: { ok: false, error: 'server failed' } }, { error: new Error('storage quota') }, { result: { ok: false, paused: true } }]) {
|
|
const h = harness(options);
|
|
await h.context.preload(h.video, { quiet: true });
|
|
assert.equal(h.context.cachedIds.has(h.video.id), false);
|
|
assert.equal(h.context.downloading.size, 0);
|
|
assert.equal(h.context.saveControllers.size, 0);
|
|
assert.ok(h.jobs.has(h.video.id));
|
|
await h.context.preload(h.video, { quiet: true });
|
|
assert.equal(h.calls.length, 2);
|
|
}
|
|
});
|