'use strict'; const test = require('node:test'), assert = require('node:assert/strict'); const act = require('../reading-activity'); const core = require('../public/activity-core'); const MIN = 60000, HOUR = 60 * MIN, DAY = 24 * HOUR; const sum = a => Object.values(a.fine).concat(Object.values(a.hourly)).reduce((s, b) => s + b.a, 0); test('active time ending at a moment is spread over the 6-minute buckets it covers', () => { const a = act.emptyActivity(), end = 1000 * act.FINE_MS + 2 * MIN; act.addActive(a, 'alpha', 5 * MIN, end); assert.deepEqual(Object.keys(a.fine).map(Number), [999, 1000]); assert.equal(a.fine[1000].a, 2 * MIN); assert.equal(a.fine[999].a, 3 * MIN); act.addActive(a, 'beta', MIN, end); assert.deepEqual(a.fine[1000].m, { alpha: 2 * MIN, beta: MIN }); }); test('old buckets fold into hours, very old ones are dropped, totals stay', () => { const now = 2000 * DAY, a = act.emptyActivity(); act.addActive(a, 'alpha', 10 * MIN, now - 200 * DAY); act.addActive(a, 'alpha', 10 * MIN, now - 2 * DAY); act.addActive(a, 'alpha', 10 * MIN, now - 1200 * DAY); act.fold(a, now); assert.equal(Object.keys(a.hourly).length, 1); assert.equal(sum(a), 20 * MIN); }); test('backfill places remembered visits before their last save, once', () => { const now = 100 * DAY, at = now - DAY; const reading = { alpha: { c1: { recentSessions: [ { id: 'v1', at, pass: { activeMs: 20 * MIN, startedAt: at - 30 * MIN } }, { id: 'legacy', at, counted: true }, ] } } }; const a = act.backfill(act.emptyActivity(), reading, now); assert.equal(sum(a), 20 * MIN); assert.equal(a.backfilled, true); act.backfill(a, reading, now); assert.equal(sum(a), 20 * MIN); }); test('forgetting a manga removes only its share', () => { const a = act.emptyActivity(); act.addActive(a, 'alpha', MIN, 10 * act.FINE_MS); act.addActive(a, 'beta', 2 * MIN, 10 * act.FINE_MS); act.forgetManga(a, 'beta'); assert.equal(sum(a), MIN); assert.deepEqual(a.fine[9].m, { alpha: MIN }); }); test('series lists buckets in time order and chapter completions with labels', () => { const a = act.emptyActivity(); act.addActive(a, 'alpha', MIN, 20 * act.FINE_MS); a.hourly[1] = { a: MIN, m: { beta: MIN } }; const s = act.activitySeries(a, { alpha: { c1: { done: true, doneAt: 5 }, c2: { done: false } } }, { titleOf: id => id.toUpperCase(), labelOf: (m, c) => 'Ch. ' + c }); assert.deepEqual(s.buckets.map(b => b[0]), [act.HOUR_MS, 19 * act.FINE_MS]); assert.deepEqual(s.done, [[5, s.mangas.findIndex(m => m.id === 'alpha'), 'Ch. c1']]); assert.equal(s.mangas.find(m => m.id === 'beta').title, 'BETA'); }); test('periods are local and Monday based; cells carry their natural segments', () => { const tue = new Date(2026, 10, 3, 15, 30).getTime(); assert.equal(core.periodStart('week', tue), new Date(2026, 10, 2).getTime()); assert.equal(core.periodStart('month', tue), new Date(2026, 10, 1).getTime()); assert.equal(core.isoWeek(tue), 45); assert.equal(core.segmentBounds('month', new Date(2026, 1, 1).getTime()).length, 29); assert.equal(core.segmentBounds('week', new Date(2026, 10, 2).getTime()).length, 29); assert.equal(core.segmentBounds('hour', new Date(2026, 10, 3, 15).getTime()).length, 11); }); test('layouts: two years of months, six months of weeks without repeats, a month calendar, a day of hours', () => { const t = new Date(2026, 10, 3, 15).getTime(); const months = core.activityLayout('month', t); assert.deepEqual(months.rows.map(r => r.cells.length), [12, 12]); const weeks = core.activityLayout('week', t).rows.flatMap(r => r.cells); assert.equal(new Set(weeks.map(c => c.start)).size, weeks.length); for (let i = 1; i < weeks.length; i++) assert.equal(weeks[i].start, core.periodShift('week', weeks[i - 1].start, 1)); const days = core.activityLayout('day', t); assert.equal(days.rows.flatMap(r => r.cells).filter(c => !c.outside).length, 30); assert.equal(core.activityLayout('hour', t).rows.flatMap(r => r.cells).length, 24); }); test('sums split buckets by overlap and count completions in their segment', () => { const day = new Date(2026, 10, 3).getTime(); const cells = core.activityLayout('hour', day).rows.flatMap(r => r.cells); const nine = day + 9 * HOUR; core.activitySums({ buckets: [[nine - 30 * MIN, HOUR, 60 * MIN, [[0, 60 * MIN]]]], done: [[nine + 7 * MIN, 0, 'Ch. 1']] }, cells); assert.equal(Math.round(cells[8].totalTime), 30 * MIN); assert.equal(Math.round(cells[9].totalTime), 30 * MIN); assert.deepEqual(cells[9].time.slice(0, 6).map(Math.round), [6 * MIN, 6 * MIN, 6 * MIN, 6 * MIN, 6 * MIN, 0]); assert.equal(cells[9].chapters[1], 1); assert.equal(cells[9].done.length, 1); }); test('drilling: month and week open days (week marked), day opens hours, hour is the end', () => { const week = new Date(2026, 10, 30).getTime(); const next = core.activityDrill({ level: 'week', start: week }); assert.equal(next.view, 'day'); assert.equal(next.mark, week); assert.equal(new Date(next.anchor).getMonth(), 11); assert.equal(core.activityDrill({ level: 'day', start: week }).view, 'hour'); assert.equal(core.activityDrill({ level: 'hour', start: week }), null); assert.equal(core.activityGlow(0, 10), 0); assert.equal(core.activityGlow(10, 10), 1); });