Each sign-in keeps its own reader mode, zoom, page tone and skipping once it changes one; sessions that never changed follow the latest set, and a new sign-in starts from the session used last. Settings -> Reading shows every session's state, lets a session follow the latest again or become the latest, and turns the feature off for one shared set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
6.1 KiB
JavaScript
107 lines
6.1 KiB
JavaScript
'use strict';
|
|
// Reader settings per login session: a session follows the latest set until it changes one, then keeps its own.
|
|
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 readerSessions = require('../reader-sessions');
|
|
|
|
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;
|
|
}
|
|
|
|
test('device labels name browser and system', () => {
|
|
assert.equal(readerSessions.deviceLabel('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Version/17.0 Mobile/15E148 Safari/604.1'), 'Safari on iPhone');
|
|
assert.equal(readerSessions.deviceLabel('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130.0 Safari/537.36 Edg/130.0'), 'Edge on Windows');
|
|
assert.equal(readerSessions.deviceLabel(''), 'Unknown device');
|
|
});
|
|
|
|
test('sessions lock their reader settings on the first change, new ones copy the last active one', { timeout: 20000 }, async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-reader-sessions-'));
|
|
let child;
|
|
try {
|
|
const library = path.join(root, 'library'), data = path.join(root, 'data');
|
|
fs.mkdirSync(library); fs.mkdirSync(data);
|
|
const port = await freePort(), output = [];
|
|
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(''));
|
|
|
|
const json = async r => { const body = await r.json(); assert.equal(r.status, 200, JSON.stringify(body)); return body; };
|
|
const post = (cookie, url, body) => fetch(base + url, { method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) Firefox/130.0', ...(cookie ? { cookie } : {}) },
|
|
body: JSON.stringify(body) }).then(async r => (url.endsWith('login') || url.endsWith('signup') ? r : json(r)));
|
|
const me = async cookie => (await json(await fetch(base + '/api/me', { headers: { cookie } }))).settings;
|
|
const overview = cookie => fetch(base + '/api/reader-sessions', { headers: { cookie } }).then(json);
|
|
const login = async () => (await post(null, '/api/login', { name: 'reader', password: 'reader-password' })).headers.get('set-cookie').split(';')[0];
|
|
|
|
const a = (await post(null, '/api/signup', { name: 'reader', password: 'reader-password' })).headers.get('set-cookie').split(';')[0];
|
|
// First session: defaults, following the latest set.
|
|
assert.equal((await me(a)).lastMode, 'page');
|
|
assert.equal((await overview(a)).sessions[0].locked, false);
|
|
|
|
// Session B exists before A changes anything: it follows A's change.
|
|
const b = await login();
|
|
await post(a, '/api/settings', { lastMode: 'flow', zoom: { flow: 1.5 } });
|
|
assert.equal((await me(b)).lastMode, 'flow');
|
|
assert.equal((await me(b)).zoom.flow, 1.5);
|
|
|
|
// B changes its own: B locks, A keeps its own set.
|
|
await post(b, '/api/settings', { pageTone: 'sepia' });
|
|
assert.equal((await me(b)).pageTone, 'sepia');
|
|
assert.equal((await me(b)).lastMode, 'flow');
|
|
assert.equal((await me(a)).pageTone, 'original');
|
|
|
|
// A non-reader setting is shared and does not lock anything.
|
|
const c = await login();
|
|
await post(c, '/api/settings', { continueRow: false });
|
|
assert.equal((await me(a)).continueRow, false);
|
|
assert.equal((await overview(c)).sessions.find(s => s.current).locked, false);
|
|
// C follows the last active session, which is A (it loaded /api/me last).
|
|
assert.equal((await me(c)).pageTone, 'original');
|
|
await me(b);
|
|
assert.equal((await me(c)).pageTone, 'sepia');
|
|
|
|
// Overview: three sessions, labels, intent.
|
|
const o = await overview(a);
|
|
assert.equal(o.enabled, true);
|
|
assert.equal(o.sessions.length, 3);
|
|
assert.equal(o.sessions[0].current, true);
|
|
assert.equal(o.sessions[0].device, 'Firefox on Linux');
|
|
assert.deepEqual(o.sessions.map(s => s.locked).sort(), [false, true, true]);
|
|
|
|
// "Follow latest again" drops A's lock; "Use for new sessions" makes a set the latest.
|
|
const idA = o.sessions[0].id, idB = o.sessions.find(s => !s.current && s.locked).id;
|
|
await post(a, '/api/reader-sessions', { id: idA, action: 'follow' });
|
|
assert.equal((await me(a)).pageTone, 'sepia');
|
|
assert.equal((await fetch(base + '/api/reader-sessions', { method: 'POST', headers: { 'content-type': 'application/json', cookie: a },
|
|
body: JSON.stringify({ id: 'nope', action: 'follow' }) })).status, 404);
|
|
assert.equal((await post(a, '/api/reader-sessions', { id: idB, action: 'latest' })).latestFrom, idB);
|
|
|
|
// Off: one shared set; a change applies everywhere and locks nothing. On again: kept locks return.
|
|
await post(b, '/api/settings', { readerPerSession: false });
|
|
await post(a, '/api/settings', { pageTone: 'dim' });
|
|
assert.equal((await me(b)).pageTone, 'dim');
|
|
assert.equal((await me(c)).pageTone, 'dim');
|
|
await post(a, '/api/settings', { readerPerSession: true });
|
|
assert.equal((await me(b)).pageTone, 'sepia');
|
|
assert.equal((await me(a)).pageTone, 'sepia');
|
|
} finally {
|
|
if (child && child.exitCode === null) { child.kill(); await new Promise(resolve => child.once('exit', resolve)); }
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|