Files
MangaCircus/test/reading-visits.test.js

84 lines
5.2 KiB
JavaScript

'use strict';
const test = require('node:test'), assert = require('node:assert/strict');
const { applyVisit, emptyRecord, chapterInsight, publicSummary } = require('../reading-analytics');
const { exposureTracker, stateFor, IDLE_MS } = require('../public/reading-clock-core');
const pages = n => Array.from({ length: n }, (_, i) => i);
const config = { version: 'webtoon', pageCount: 20, eligible: pages(20), now: 10000 };
const visit = (extra = {}) => ({ session: 'reading-visit', seq: 1, elapsedMs: 10000, activeMs: 9000, seen: [], ...extra });
test('59% is a draft; 60% credits the entire visit and final duration keeps growing', () => {
const r = emptyRecord(), cfg = { ...config, pageCount: 100, eligible: pages(100) };
applyVisit(r, visit({ seen: pages(59) }), cfg);
assert.equal(r.totalActiveMs, 0); assert.equal(chapterInsight(r), null);
applyVisit(r, visit({ seq: 2, elapsedMs: 12000, activeMs: 11000, seen: pages(60) }), cfg);
assert.equal(r.totalActiveMs, 11000); assert.equal(r.completedPasses, 1);
applyVisit(r, visit({ seq: 3, elapsedMs: 18000, activeMs: 16000, seen: pages(65), final: true }), cfg);
assert.equal(r.totalActiveMs, 16000); assert.equal(r.firstCompleteMs, 16000); assert.equal(r.lastCompleteMs, 16000);
assert.equal(r.currentPass.closed, true);
});
test('an exit snapshot overtaking a pulse counts once and survives a JSON reload', () => {
const r = emptyRecord();
applyVisit(r, visit({ seq: 3, seen: pages(12), final: true }), config);
const restored = JSON.parse(JSON.stringify(r));
assert.equal(applyVisit(restored, visit({ seq: 1, activeMs: 0 }), config).duplicate, true);
assert.equal(applyVisit(restored, visit({ seq: 3, seen: pages(12), final: true }), config).duplicate, true);
applyVisit(restored, visit({ seq: 4, seen: pages(13), activeMs: 10000, elapsedMs: 11000 }), config);
assert.equal(restored.totalActiveMs, 10000); assert.equal(restored.completedPasses, 1);
});
test('delayed delivery does not truncate an honest minute to twelve seconds', () => {
const r = emptyRecord();
applyVisit(r, visit({ elapsedMs: 65000, activeMs: 60000, inputMs: { touch: 60000 }, seen: pages(12) }), config);
assert.equal(r.totalActiveMs, 60000); assert.equal(r.inputMs.touch, 60000);
});
test('one-picture lookup does not borrow prior coverage or change saved statistics', () => {
const r = emptyRecord();
applyVisit(r, visit({ seen: pages(12), final: true }), config);
applyVisit(r, visit({ session: 'lookup-visit', elapsedMs: 60000, activeMs: 59000, seen: [15], final: true }), { ...config, now: 70000 });
assert.equal(r.totalActiveMs, 9000); assert.equal(r.completedPasses, 1);
assert.equal(chapterInsight(r).updatedAt, 10000);
});
test('late finals update their own visit without replacing a newer visit', () => {
const r = emptyRecord();
applyVisit(r, visit({ seen: pages(12) }), config);
applyVisit(r, visit({ session: 'new-visit', seen: pages(12), activeMs: 4000 }), { ...config, now: 20000 });
applyVisit(r, visit({ seq: 2, seen: pages(14), elapsedMs: 15000, activeMs: 13000, final: true }), { ...config, now: 21000 });
assert.equal(r.currentPass.visit, 'new-visit'); assert.equal(r.firstCompleteMs, 13000);
assert.equal(r.lastCompleteMs, 4000); assert.equal(r.totalActiveMs, 17000);
});
test('legacy time and source IDs are retained; covers/extras use the eligible denominator', () => {
const r = { ...emptyRecord(), totalActiveMs: 5000, completedPasses: 1, firstCompleteMs: 5000, lastCompleteMs: 5000, updatedAt: 500 };
const cfg = { ...config, eligible: [1, 3, 5, 7, 9] };
applyVisit(r, visit({ seen: [0, 1, 3, 5, 19], activeMs: 2000 }), cfg);
assert.equal(r.totalActiveMs, 7000); assert.equal(r.firstCompleteMs, 5000);
assert.equal(publicSummary(r, 'webtoon', cfg.eligible).currentPass.requiredPages, 3);
});
test('fast webtoon slices above five pages per second are credited without a ceiling', () => {
const t = exposureTracker();
for (let i = 0; i < 10; i++) t.update([{ index: i, loaded: true }], i * 120, true);
t.update([], 1200, true);
assert.deepEqual([...t.seen], pages(10));
});
test('jumping over pages to dwell on one image never invents skipped coverage', () => {
const t = exposureTracker();
t.update([{ index: 0, loaded: true }], 0, true);
t.update([{ index: 19, loaded: true }], 50, true);
t.update([{ index: 19, loaded: true }], 60000, true);
assert.deepEqual([...t.seen], [19]);
});
test('unloaded and hidden pages cannot qualify; mixed loading does not pause readable pages', () => {
assert.equal(IDLE_MS, 30000, 'The user-selected inactivity allowance is 30 seconds');
const t = exposureTracker();
t.update([{ index: 0, loaded: false }], 0, true);
t.update([{ index: 0, loaded: false }], 1000, true);
assert.equal(t.seen.size, 0);
const view = { pages: [{ index: 0, loaded: true }, { index: 1, loaded: false }] };
const s = { visible: true, focused: true, lastInputAt: 0, now: 5000, owned: true, view };
assert.equal(stateFor(s), 'reading');
assert.equal(stateFor({ ...s, visible: false }), 'hidden');
assert.equal(stateFor({ ...s, focused: false }), 'unfocused');
assert.equal(stateFor({ ...s, now: IDLE_MS }), 'idle');
assert.equal(stateFor({ ...s, now: IDLE_MS - 1 }), 'reading');
t.update(view.pages, 1000, false); t.update(view.pages, 100000, false);
assert.equal(t.seen.size, 0);
});