Consolidate MangaCircus app, scraper, and Compose baseline
This commit is contained in:
1 parent
cfb2d79103
commit
eef4ffa07c
103 files changed
+9316
-172
No files matched your search
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
applyPulse, chapterInsight, compactRanges, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary,
|
||||
} = require('../reading-analytics');
|
||||
|
||||
const pulse = (session, seq, extra = {}) => ({
|
||||
session, seq, activeMs: 0, loadingMs: 0,
|
||||
inputMs: { mouse: 0, touch: 0, keyboard: 0 }, seen: [],
|
||||
fastBursts: 0, rejectedPageViews: 0, maxPps: 0,
|
||||
...extra,
|
||||
});
|
||||
|
||||
test('page ranges remain compact and deterministic', () => {
|
||||
assert.deepEqual(compactRanges([8, 2, 3, 3, 4, 9, 12]), [[2, 4], [8, 9], [12, 12]]);
|
||||
});
|
||||
|
||||
test('reading pulses are idempotent and server-clamped', () => {
|
||||
const record = emptyRecord(), options = { version: 'source-a', pageCount: 4, eligible: [0, 1, 2, 3], now: 10000 };
|
||||
applyPulse(record, pulse('session-one', 1, { activeMs: 9800, inputMs: { touch: 9800 } }), options);
|
||||
assert.equal(record.totalActiveMs, 9800);
|
||||
assert.equal(record.sessions, 1);
|
||||
assert.equal(record.inputMs.touch, 9800);
|
||||
|
||||
const duplicate = applyPulse(record, pulse('session-one', 1, { activeMs: 9800 }), { ...options, now: 20000 });
|
||||
assert.equal(duplicate.duplicate, true);
|
||||
assert.equal(record.totalActiveMs, 9800);
|
||||
|
||||
applyPulse(record, pulse('session-one', 2, { activeMs: 50000, loadingMs: 50000, inputMs: { mouse: 50000 } }), { ...options, now: 21000 });
|
||||
assert.equal(record.totalActiveMs + record.loadingMs, 9800 + 12000);
|
||||
assert.ok(record.inputMs.mouse <= 6000);
|
||||
});
|
||||
|
||||
test('eligible coverage completes a pass and a reread starts a fresh pass', () => {
|
||||
const version = { pages: [{ t: ['cover'] }, { t: [] }, { t: ['extra'] }, { t: [] }] };
|
||||
const eligible = eligiblePages(version, { skipCovers: true, skipExtras: true });
|
||||
assert.deepEqual(eligible, [1, 3]);
|
||||
|
||||
const record = emptyRecord();
|
||||
applyPulse(record, pulse('first-pass', 1, { activeMs: 1000, inputMs: { keyboard: 1000 }, seen: [0, 1, 2, 3] }), {
|
||||
version: 'source-a', pageCount: 4, eligible, now: 1000,
|
||||
});
|
||||
assert.equal(record.completedPasses, 1);
|
||||
assert.equal(record.firstCompleteMs, 1000);
|
||||
assert.deepEqual(record.currentPass.seenRanges, [[1, 1], [3, 3]]);
|
||||
|
||||
applyPulse(record, pulse('second-pass', 1), { version: 'source-a', pageCount: 4, eligible, now: 2000 });
|
||||
const summary = publicSummary(record, 'source-a', eligible);
|
||||
assert.equal(summary.currentPass.complete, false);
|
||||
assert.equal(summary.currentPass.seenPages, 0);
|
||||
assert.equal(summary.totalActiveMs, 1000);
|
||||
});
|
||||
|
||||
test('switching source versions starts separate page coverage without losing lifetime time', () => {
|
||||
const record = emptyRecord();
|
||||
applyPulse(record, pulse('version-test', 1, { activeMs: 800, seen: [0] }), {
|
||||
version: 'source-a', pageCount: 2, eligible: [0, 1], now: 1000,
|
||||
});
|
||||
applyPulse(record, pulse('version-test', 2, { activeMs: 500, seen: [2] }), {
|
||||
version: 'source-b', pageCount: 3, eligible: [0, 1, 2], now: 2000,
|
||||
});
|
||||
const summary = publicSummary(record, 'source-b', [0, 1, 2]);
|
||||
assert.equal(summary.totalActiveMs, 1300);
|
||||
assert.deepEqual(summary.currentPass.seenRanges, [[2, 2]]);
|
||||
});
|
||||
|
||||
test('chapter and manga insights distinguish untimed, partial, complete and reread records', () => {
|
||||
assert.equal(chapterInsight(null), null);
|
||||
const records = {
|
||||
first: { ...emptyRecord(), totalActiveMs: 60000, updatedAt: 100, currentPass: { activeMs: 60000 } },
|
||||
second: { ...emptyRecord(), totalActiveMs: 540000, lastCompleteMs: 180000, firstCompleteMs: 210000, completedPasses: 2, updatedAt: 300, currentPass: { activeMs: 180000, completedAt: 300 } },
|
||||
third: { ...emptyRecord(), totalActiveMs: 300000, lastCompleteMs: 300000, firstCompleteMs: 300000, completedPasses: 1, updatedAt: 200, currentPass: { activeMs: 300000, completedAt: 200 } },
|
||||
};
|
||||
assert.deepEqual(chapterInsight(records.first), {
|
||||
totalActiveMs: 60000, firstCompleteMs: null, lastCompleteMs: null, completedPasses: 0,
|
||||
currentPassMs: 60000, currentPassComplete: false, updatedAt: 100,
|
||||
});
|
||||
assert.deepEqual(mangaInsight(records, ['first', 'second', 'third', 'missing']), {
|
||||
activeMs: 900000,
|
||||
timedChapters: 3,
|
||||
completedChapters: 2,
|
||||
completedPasses: 3,
|
||||
rereads: 1,
|
||||
typicalChapterMs: 240000,
|
||||
lastChapterId: 'second',
|
||||
updatedAt: 300,
|
||||
});
|
||||
});
|
||||
|
||||
test('library insights select the most-read and most-recent manga', () => {
|
||||
const summary = libraryInsight([
|
||||
{ id: 'a', reading: { activeMs: 1000, timedChapters: 1, completedPasses: 1, rereads: 0, updatedAt: 100, lastChapterId: '1/1' } },
|
||||
{ id: 'b', reading: { activeMs: 3000, timedChapters: 2, completedPasses: 4, rereads: 2, updatedAt: 90, lastChapterId: '1/2' } },
|
||||
{ id: 'empty', reading: { activeMs: 0, timedChapters: 0, completedPasses: 0, rereads: 0, updatedAt: 0 } },
|
||||
]);
|
||||
assert.deepEqual(summary, {
|
||||
activeMs: 4000, timedChapters: 3, completedPasses: 5, rereads: 2, timedMangas: 2,
|
||||
mostTimeMangaId: 'b', lastMangaId: 'a', lastChapterId: '1/1', updatedAt: 100,
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user