A done chapter after the head now counts as left again when it was finished more than rereadDays before the head (default 7) or before a run of more than rereadChapters chapters read in order up to the head (default 5). Both are user settings under Library page; 0 turns a rule off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
5.9 KiB
JavaScript
111 lines
5.9 KiB
JavaScript
'use strict';
|
|
// "Continue where you left off" only lists manga with something left after the last chapter read:
|
|
// unread chapters, or chapters last read in an earlier pass (a reread).
|
|
const test = require('node:test'), assert = require('node:assert/strict');
|
|
const fs = require('fs'), os = require('os'), path = require('path'), net = require('net');
|
|
const { spawn } = require('child_process');
|
|
const { Canvas } = require('../tools/png');
|
|
|
|
async function freePort() {
|
|
const listener = net.createServer();
|
|
await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve));
|
|
const port = listener.address().port;
|
|
await new Promise(resolve => listener.close(resolve));
|
|
return port;
|
|
}
|
|
|
|
async function start(library, data) {
|
|
const port = await freePort(), output = [];
|
|
const child = spawn(process.execPath, ['server.js'], { cwd: path.resolve(__dirname, '..'), windowsHide: true,
|
|
env: { ...process.env, LIBRARY: library, DATA: data, PORT: String(port), SCAN_INTERVAL: '0', FULL_SCAN_INTERVAL: '0',
|
|
SCRAPER_URL: 'http://127.0.0.1:9', SCRAPER_TOKEN: 'fixture-token', SCRAPER_TOKEN_FILE: '', BACKUP: '', GREATEST_COVER_WARM: '0' } });
|
|
child.stdout.on('data', d => output.push(String(d)));
|
|
child.stderr.on('data', d => output.push(String(d)));
|
|
const base = `http://127.0.0.1:${port}`;
|
|
let ready = false;
|
|
for (let n = 0; n < 100 && !ready; n++) {
|
|
ready = await fetch(base + '/health').then(r => r.ok).catch(() => false);
|
|
if (!ready) await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
assert.ok(ready, output.join(''));
|
|
return { child, base };
|
|
}
|
|
async function stop(child) {
|
|
if (child && child.exitCode === null) { child.kill(); await new Promise(resolve => child.once('exit', resolve)); }
|
|
}
|
|
|
|
test('the continue row counts chapters left after the head, rereads included', { timeout: 30000 }, async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-left-'));
|
|
let server;
|
|
try {
|
|
const library = path.join(root, 'library'), data = path.join(root, 'data');
|
|
for (const [title, count] of [['Moon Garden', 4], ['Sun Field', 4], ['Star Path', 8]])
|
|
for (let n = 1; n <= count; n++) {
|
|
const dir = path.join(library, title, 'v01', `c00${n}`);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, '001.png'), new Canvas(60, 90, [40, 60, 120]).png());
|
|
}
|
|
fs.mkdirSync(data);
|
|
server = await start(library, data);
|
|
let { base } = server;
|
|
const post = (cookie, url, body) => fetch(base + url, { method: 'POST',
|
|
headers: { 'content-type': 'application/json', cookie }, body: JSON.stringify(body) }).then(r => r.json());
|
|
const get = (cookie, url) => fetch(base + url, { headers: { cookie } }).then(r => r.json());
|
|
const signup = await fetch(base + '/api/signup', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ name: 'alice', password: 'alice-password' }) });
|
|
const alice = signup.headers.get('set-cookie').split(';')[0];
|
|
await post(alice, '/api/admin/rescan', { full: true });
|
|
const ids = async m => (await get(alice, '/api/manga?m=' + encodeURIComponent(m))).chapters.map(c => ({ c: c.id, v: c.versions[0].id }));
|
|
const moon = await ids('Moon Garden'), sun = await ids('Sun Field');
|
|
const done = (m, ch) => post(alice, '/api/progress', { m, ...ch, p: 0, n: 1, done: true });
|
|
const left = async () => Object.fromEntries((await get(alice, '/api/library')).mangas.map(m => [m.id, m.left]));
|
|
|
|
// First read stopped after chapter 2: two chapters left. Finished: nothing left.
|
|
for (const ch of moon.slice(0, 2)) await done('Moon Garden', ch);
|
|
for (const ch of sun) await done('Sun Field', ch);
|
|
assert.deepEqual(await left(), { 'Moon Garden': 2, 'Sun Field': 0, 'Star Path': 0 });
|
|
|
|
// Read all 8 chapters of Star Path, then reread 1-6 right away: more than 5 in a row is a reread
|
|
// even within the time limit; with a longer run setting, or the run rule off, it is not.
|
|
const star = await ids('Star Path');
|
|
for (const ch of star) await done('Star Path', ch);
|
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
for (const ch of star.slice(0, 5)) await done('Star Path', ch);
|
|
assert.equal((await left())['Star Path'], 0, 'five in a row is not yet a reread');
|
|
await done('Star Path', star[5]);
|
|
assert.equal((await left())['Star Path'], 2);
|
|
await post(alice, '/api/settings', { rereadChapters: 6 });
|
|
assert.equal((await left())['Star Path'], 0);
|
|
await post(alice, '/api/settings', { rereadChapters: 0 });
|
|
assert.equal((await left())['Star Path'], 0);
|
|
await post(alice, '/api/settings', { rereadChapters: 5, rereadDays: -1 });
|
|
const settings = (await get(alice, '/api/me')).settings;
|
|
assert.equal(settings.rereadDays, 7, 'invalid values are ignored');
|
|
assert.equal((await left())['Star Path'], 2);
|
|
|
|
// Unfinished head: the rest of that chapter is still left.
|
|
await post(alice, '/api/progress', { m: 'Moon Garden', ...moon[2], p: 0, n: 1 });
|
|
assert.equal((await left())['Moon Garden'], 2);
|
|
|
|
// Glancing back at chapter 1 right after finishing is not a reread.
|
|
await done('Sun Field', sun[0]);
|
|
assert.equal((await left())['Sun Field'], 0);
|
|
|
|
// The finish was a month ago: rereading chapters 1-2 now leaves 3-4 from the earlier pass.
|
|
await new Promise(resolve => setTimeout(resolve, 700));
|
|
await stop(server.child);
|
|
const dbFile = path.join(data, 'db.json'), db = JSON.parse(fs.readFileSync(dbFile, 'utf8'));
|
|
const month = 30 * 24 * 3600 * 1000, sunProgress = db.progress.alice['Sun Field'];
|
|
for (const p of Object.values(sunProgress)) { p.t -= month; p.doneAt -= month; }
|
|
sunProgress[sun[0].c].t += month;
|
|
fs.writeFileSync(dbFile, JSON.stringify(db));
|
|
server = await start(library, data);
|
|
base = server.base;
|
|
await done('Sun Field', sun[1]);
|
|
assert.equal((await left())['Sun Field'], 2);
|
|
} finally {
|
|
await stop(server && server.child);
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|