// The page's stream-termination rules (./termination.js), in Node: the declaration it reads // (capability-constraints §4.1), the refresh rate it measures, which reason is due // (stream-termination §4.3, §6.2) and how objects gather into segments (§4.5). // // node --test web/room/termination.test.mjs import test from "node:test"; import assert from "node:assert/strict"; import { declarationFrom, refreshFromIntervals, dueReason, freshState, observe, unseen, Gatherer, isActivation, bitDepthOf, ladderOf, SIGNAL, SEEN, UNSEEN } from "./termination.js"; test("the declaration: CSS pixels times the ratio, sides without orientation, depth per component", () => { assert.deepEqual(declarationFrom({ width: 1440, height: 900, dpr: 2, colorDepth: 30, hdr: true, refreshMilli: 120000 }), { long: 2880, short: 1800, refresh_milli: 120000, bit_depth: 10, hdr: true, long_mm: 0, short_mm: 0 }); // Portrait, a fractional ratio rounded to the nearest pixel, and the 24 a user agent gives // when it does not say: 8 bits. assert.deepEqual(declarationFrom({ width: 390, height: 844, dpr: 2.625, colorDepth: 24, hdr: false, refreshMilli: 60000 }), { long: 2216, short: 1024, refresh_milli: 60000, bit_depth: 8, hdr: false, long_mm: 0, short_mm: 0 }); }); test("the refresh rate: the nearest whole rate to the median interval", () => { assert.equal(refreshFromIntervals(Array(60).fill(16667)), 60000); // A 59.94 Hz panel declares 60, under which a 59.94 rendition is within. assert.equal(refreshFromIntervals(Array(60).fill(16683)), 60000); assert.equal(refreshFromIntervals([8333, 8334, 8333, 50000, 8333]), 120000, "one late frame does not move the median"); assert.equal(refreshFromIntervals([]), null); }); const video = { media: "video", start: "user", unseen_grace_ms: 3000, idle_ms: 0, autoplay_grace_ms: 0 }; const S = 1_000_000; test("hidden is due after the grace, and not once the page is visible again", () => { let st = freshState(0); st = observe(st, SIGNAL.visibility, UNSEEN, 5 * S); assert.equal(dueReason(video, st, 8 * S - 1), null); assert.equal(dueReason(video, st, 8 * S), "hidden"); // A repeated observation does not move `since`. assert.equal(dueReason(video, observe(st, SIGNAL.visibility, UNSEEN, 7 * S), 8 * S), "hidden"); assert.equal(dueReason(video, observe(st, SIGNAL.visibility, SEEN, 6 * S), 20 * S), null); assert.equal(unseen(st), true); }); test("occluded likewise; audio is never ended for being unseen", () => { const st = observe(freshState(0), SIGNAL.occlusion, UNSEEN, S); assert.equal(dueReason(video, st, 4 * S), "occluded"); assert.equal(dueReason({ ...video, media: "audio" }, observe(st, SIGNAL.visibility, UNSEEN, S), 60 * S), null); }); test("idle after the timeout from the last interaction; autoplay unattended only with none", () => { const idle = { ...video, idle_ms: 30000 }; let st = observe(freshState(0), SIGNAL.interaction, 1, 4 * S); assert.equal(dueReason(idle, st, 34 * S - 1), null); assert.equal(dueReason(idle, st, 34 * S), "idle"); const auto = { ...video, start: "autoplay", autoplay_grace_ms: 20000 }; assert.equal(dueReason(auto, freshState(0), 20 * S), "autoplay_unattended"); assert.equal(dueReason(auto, st, 25 * S), null, "an interaction makes it attended"); assert.equal(dueReason({ ...auto, start: "user" }, freshState(0), 25 * S), null); }); test("objects gather into segments of at least the span, closed by the next object, never overlapping", () => { const g = new Gatherer(2 * S); const o = (k, r = 0) => ({ hash: k.toString(16).padStart(64, "0"), bytes: 100, mediaUs: k * 100_000, durUs: 100_000, rendition: r }); let closed = []; for (let k = 0; k < 25; k++) closed = closed.concat(g.add(o(k))); assert.equal(closed.length, 1); assert.deepEqual([closed[0].media_start, closed[0].media_end, closed[0].hashes.length], [0, 2 * S, 20]); assert.deepEqual(g.add(o(24)), [], "a repeated object is not gathered again"); assert.deepEqual(g.add(o(10)), [], "nor one before the last segment's end"); const r = g.add(o(25, 1)); assert.equal(r.length, 1, "a change of rendition closes the segment"); assert.deepEqual([r[0].media_start, r[0].media_end, r[0].rendition], [2 * S, 2_500_000, 0]); const last = g.take(); assert.deepEqual([last.media_start, last.media_end, last.rendition], [2_500_000, 2_600_000, 1]); assert.equal(g.take(), null); // A gap longer than the span closes the segment where its last object ends. const h = new Gatherer(2 * S); h.add(o(0)); h.add(o(1)); const gapped = h.add(o(60)); assert.deepEqual([gapped[0].media_start, gapped[0].media_end, gapped[0].hashes.length], [0, 200_000, 2]); // With nothing gathered, an object before the last segment's end starts no segment. assert.deepEqual(g.add(o(20, 1)), []); assert.equal(g.take(), null, "a segment would overlap the one before it"); }); test("activation-triggering input events (HTML §6.4)", () => { const e = (type, extra = {}) => ({ type, isTrusted: true, ...extra }); assert.equal(isActivation(e("keydown", { key: "a" })), true); assert.equal(isActivation(e("keydown", { key: "Escape" })), false); assert.equal(isActivation(e("pointerdown", { pointerType: "mouse" })), true); assert.equal(isActivation(e("pointerdown", { pointerType: "touch" })), false); assert.equal(isActivation(e("pointerup", { pointerType: "touch" })), true); assert.equal(isActivation({ ...e("mousedown"), isTrusted: false }), false, "a script's event is not the viewer's"); }); test("the ladder from a catalog's tracks", () => { assert.equal(bitDepthOf("vp09.00.10.08"), 8); assert.equal(bitDepthOf("av01.0.08M.10"), 10); assert.equal(bitDepthOf("vp8"), 8); assert.deepEqual(ladderOf([{ width: 640, height: 480, framerate: 30, codec: "vp09.00.10.08" }]), [{ width: 640, height: 480, frame_rate_milli: 30000, bit_depth: 8, transfer: 1 }]); });