From a7d19fc1dbe5f6f60ca213aaad29ddd5d406a979 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:22:16 +0000 Subject: [PATCH] test: improve gridHealth prioritization testing - Extract `prioritizeEntries` logic to `js/gridHealthLogic.js` for testability - Add comprehensive unit tests in `tests/grid-health.test.mjs` - Improve `getIntersectionRect` to robustly handle invalid rects - Ensure 0x0 rects are filtered out explicitly Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/gridHealth.js | 139 +---------------------- js/gridHealthLogic.js | 134 ++++++++++++++++++++++ tests/grid-health.test.mjs | 221 +++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+), 134 deletions(-) create mode 100644 js/gridHealthLogic.js create mode 100644 tests/grid-health.test.mjs diff --git a/js/gridHealth.js b/js/gridHealth.js index c428d293..19669701 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -8,6 +8,11 @@ import { removeTorrentProbeFromStorage, writeTorrentProbeToStorage, } from "./utils/storage.js"; +import { + prioritizeEntries, + getViewportCenter, + PRIORITY_BASELINE, +} from "./gridHealthLogic.js"; const badgeUpdateListeners = new Set(); @@ -226,7 +231,6 @@ const PROBE_CACHE_TTL_MS = 5 * 60 * 1000; const PROBE_TIMEOUT_MS = 20000; const PROBE_CONCURRENCY = 96; const PROBE_POLL_COUNT = 3; -const PRIORITY_BASELINE = 1_000_000; class ProbeQueue { constructor(max = 2) { @@ -308,139 +312,6 @@ const probeQueue = new ProbeQueue(PROBE_CONCURRENCY); const probeCache = new Map(); const probeInflight = new Map(); -function getViewportCenter() { - if (typeof window === "undefined") { - return null; - } - const width = Number(window.innerWidth) || 0; - const height = Number(window.innerHeight) || 0; - if (width <= 0 && height <= 0) { - return null; - } - return { - x: width > 0 ? width / 2 : 0, - y: height > 0 ? height / 2 : 0, - }; -} - -function prioritizeEntries(entries, viewportCenter) { - if (!Array.isArray(entries) || entries.length === 0) { - return []; - } - const filtered = entries - .filter((entry) => entry.isIntersecting && entry.target instanceof HTMLElement) - .map((entry) => { - const rect = getIntersectionRect(entry); - if (!rect) { - return null; - } - const ratio = - typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0; - const centerY = rect.top + rect.height / 2; - const verticalDistance = viewportCenter - ? Math.abs(centerY - viewportCenter.y) - : Number.POSITIVE_INFINITY; - return { entry, ratio, centerY, verticalDistance }; - }) - .filter(Boolean); - - if (filtered.length === 0) { - return []; - } - - if (!viewportCenter) { - return filtered - .sort((a, b) => { - if (b.ratio !== a.ratio) { - return b.ratio - a.ratio; - } - return a.centerY - b.centerY; - }) - .map((item, index) => ({ - entry: item.entry, - priority: PRIORITY_BASELINE - index, - })); - } - - const ordered = filtered - .slice() - .sort((a, b) => a.centerY - b.centerY); - - let centerIndex = 0; - let minDistance = Number.POSITIVE_INFINITY; - for (let i = 0; i < ordered.length; i += 1) { - const candidate = ordered[i]; - if (candidate.verticalDistance < minDistance) { - minDistance = candidate.verticalDistance; - centerIndex = i; - } - } - - const prioritized = []; - const pushCandidate = (candidate) => { - if (!candidate) { - return; - } - prioritized.push(candidate); - }; - - pushCandidate(ordered[centerIndex]); - - let left = centerIndex - 1; - let right = centerIndex + 1; - while (left >= 0 || right < ordered.length) { - const leftCandidate = left >= 0 ? ordered[left] : null; - const rightCandidate = right < ordered.length ? ordered[right] : null; - - if (leftCandidate && rightCandidate) { - const leftDistance = leftCandidate.verticalDistance; - const rightDistance = rightCandidate.verticalDistance; - const distanceDelta = Math.abs(leftDistance - rightDistance); - if (distanceDelta <= 0.5) { - if (rightCandidate.ratio > leftCandidate.ratio) { - pushCandidate(rightCandidate); - right += 1; - } else { - pushCandidate(leftCandidate); - left -= 1; - } - } else if (leftDistance < rightDistance) { - pushCandidate(leftCandidate); - left -= 1; - } else { - pushCandidate(rightCandidate); - right += 1; - } - } else if (rightCandidate) { - pushCandidate(rightCandidate); - right += 1; - } else if (leftCandidate) { - pushCandidate(leftCandidate); - left -= 1; - } - } - - return prioritized.map((candidate, index) => ({ - entry: candidate.entry, - priority: PRIORITY_BASELINE - index, - })); -} - -function getIntersectionRect(entry) { - if (!entry) { - return null; - } - const rect = entry.intersectionRect; - if (rect && rect.width > 0 && rect.height > 0) { - return rect; - } - const fallback = entry.boundingClientRect; - if (fallback && fallback.width > 0 && fallback.height > 0) { - return fallback; - } - return rect || fallback || null; -} - function setBadge(card, state, details) { if (!(card instanceof HTMLElement)) { return; diff --git a/js/gridHealthLogic.js b/js/gridHealthLogic.js new file mode 100644 index 00000000..7b7de952 --- /dev/null +++ b/js/gridHealthLogic.js @@ -0,0 +1,134 @@ +export const PRIORITY_BASELINE = 1_000_000; + +export function getViewportCenter() { + if (typeof window === "undefined") { + return null; + } + const width = Number(window.innerWidth) || 0; + const height = Number(window.innerHeight) || 0; + if (width <= 0 && height <= 0) { + return null; + } + return { + x: width > 0 ? width / 2 : 0, + y: height > 0 ? height / 2 : 0, + }; +} + +export function getIntersectionRect(entry) { + if (!entry) { + return null; + } + const rect = entry.intersectionRect; + if (rect && rect.width > 0 && rect.height > 0) { + return rect; + } + const fallback = entry.boundingClientRect; + if (fallback && fallback.width > 0 && fallback.height > 0) { + return fallback; + } + return null; +} + +export function prioritizeEntries(entries, viewportCenter) { + if (!Array.isArray(entries) || entries.length === 0) { + return []; + } + const filtered = entries + .filter((entry) => entry.isIntersecting && entry.target instanceof HTMLElement) + .map((entry) => { + const rect = getIntersectionRect(entry); + if (!rect) { + return null; + } + const ratio = + typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0; + const centerY = rect.top + rect.height / 2; + const verticalDistance = viewportCenter + ? Math.abs(centerY - viewportCenter.y) + : Number.POSITIVE_INFINITY; + return { entry, ratio, centerY, verticalDistance }; + }) + .filter(Boolean); + + if (filtered.length === 0) { + return []; + } + + if (!viewportCenter) { + return filtered + .sort((a, b) => { + if (b.ratio !== a.ratio) { + return b.ratio - a.ratio; + } + return a.centerY - b.centerY; + }) + .map((item, index) => ({ + entry: item.entry, + priority: PRIORITY_BASELINE - index, + })); + } + + const ordered = filtered + .slice() + .sort((a, b) => a.centerY - b.centerY); + + let centerIndex = 0; + let minDistance = Number.POSITIVE_INFINITY; + for (let i = 0; i < ordered.length; i += 1) { + const candidate = ordered[i]; + if (candidate.verticalDistance < minDistance) { + minDistance = candidate.verticalDistance; + centerIndex = i; + } + } + + const prioritized = []; + const pushCandidate = (candidate) => { + if (!candidate) { + return; + } + prioritized.push(candidate); + }; + + pushCandidate(ordered[centerIndex]); + + let left = centerIndex - 1; + let right = centerIndex + 1; + while (left >= 0 || right < ordered.length) { + const leftCandidate = left >= 0 ? ordered[left] : null; + const rightCandidate = right < ordered.length ? ordered[right] : null; + + if (leftCandidate && rightCandidate) { + const leftDistance = leftCandidate.verticalDistance; + const rightDistance = rightCandidate.verticalDistance; + const distanceDelta = Math.abs(leftDistance - rightDistance); + if (distanceDelta <= 0.5) { + if (rightCandidate.ratio > leftCandidate.ratio) { + pushCandidate(rightCandidate); + right += 1; + } else { + pushCandidate(leftCandidate); + left -= 1; + } + } else if (leftDistance < rightDistance) { + pushCandidate(leftCandidate); + left -= 1; + } else { + pushCandidate(rightCandidate); + right += 1; + } + } else if (rightCandidate) { + pushCandidate(rightCandidate); + right += 1; + } else if (leftCandidate) { + pushCandidate(leftCandidate); + left -= 1; + } + } + + return prioritized.map((candidate, index) => ({ + entry: candidate.entry, + priority: PRIORITY_BASELINE - index, + })); +} diff --git a/tests/grid-health.test.mjs b/tests/grid-health.test.mjs new file mode 100644 index 00000000..570fa1d2 --- /dev/null +++ b/tests/grid-health.test.mjs @@ -0,0 +1,221 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; +import { prioritizeEntries } from "../js/gridHealthLogic.js"; + +function setupDom() { + const dom = new JSDOM(""); + globalThis.window = dom.window; + globalThis.HTMLElement = dom.window.HTMLElement; + return dom; +} + +function teardownDom(dom) { + dom.window.close(); + delete globalThis.window; + delete globalThis.HTMLElement; +} + +test("prioritizeEntries input validation", (t) => { + const dom = setupDom(); + t.after(() => teardownDom(dom)); + + assert.deepEqual(prioritizeEntries(null), [], "should return empty array for null entries"); + assert.deepEqual(prioritizeEntries(undefined), [], "should return empty array for undefined entries"); + assert.deepEqual(prioritizeEntries([]), [], "should return empty array for empty entries"); +}); + +test("prioritizeEntries filtering", (t) => { + const dom = setupDom(); + t.after(() => teardownDom(dom)); + + const entries = [ + { isIntersecting: false, target: dom.window.document.createElement("div") }, + { isIntersecting: true, target: {} }, // Not an HTMLElement + { isIntersecting: true, target: dom.window.document.createElement("div"), intersectionRect: { width: 0, height: 0 } }, // Invalid rect + ]; + + const result = prioritizeEntries(entries, { x: 500, y: 500 }); + assert.deepEqual(result, [], "should filter out invalid entries"); +}); + +test("prioritizeEntries without viewport center", (t) => { + const dom = setupDom(); + t.after(() => teardownDom(dom)); + + // prioritizeEntries sorts by ratio desc, then centerY asc + // Note: the implementation sorts by ratio desc (b.ratio - a.ratio), then a.centerY - b.centerY + + const entries = [ + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 100, height: 100, width: 100 }, // centerY = 150 + intersectionRatio: 0.5, + id: 1 + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 200, height: 100, width: 100 }, // centerY = 250 + intersectionRatio: 0.8, + id: 2 + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 0, height: 100, width: 100 }, // centerY = 50 + intersectionRatio: 0.5, + id: 3 + }, + ]; + + const result = prioritizeEntries(entries, null); + + assert.equal(result.length, 3); + // Highest ratio first (id: 2) + assert.equal(result[0].entry.id, 2); + + // Equal ratio (0.5), sort by centerY asc (id: 3 (50) < id: 1 (150)) + assert.equal(result[1].entry.id, 3); + assert.equal(result[2].entry.id, 1); +}); + +test("prioritizeEntries with viewport center", (t) => { + const dom = setupDom(); + t.after(() => teardownDom(dom)); + + const viewportCenter = { x: 500, y: 500 }; + + const entries = [ + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 450, height: 100, width: 100 }, // centerY = 500 (distance 0) + intersectionRatio: 1.0, + id: "center" + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 350, height: 100, width: 100 }, // centerY = 400 (distance 100) + intersectionRatio: 1.0, + id: "above" + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 550, height: 100, width: 100 }, // centerY = 600 (distance 100) + intersectionRatio: 1.0, + id: "below" + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 0, height: 100, width: 100 }, // centerY = 50 (distance 450) + intersectionRatio: 0.5, + id: "far_above" + } + ]; + + const result = prioritizeEntries(entries, viewportCenter); + + assert.equal(result.length, 4); + + // The closest to center should be first + assert.equal(result[0].entry.id, "center"); + + // The logic for left/right (above/below) depends on implementation details + // but "above" and "below" are equidistant. + // The implementation sorts by centerY first: above (400), center (500), below (600), far_above(50) -> far_above, above, center, below + // centerIndex is found (min distance). + // Then it expands left/right. + + // Let's trace: + // Ordered by Y: far_above (50), above (400), center (500), below (600) + // minDistance is center (0). centerIndex = 2. + // result: [center] + // left = 1 (above), right = 3 (below) + // leftDist = 100, rightDist = 100. + // delta = 0 <= 0.5. + // rightRatio (1.0) > leftRatio (1.0) -> False. + // else -> push left (above), left-- + // result: [center, above] + // left = 0 (far_above), right = 3 (below) + // leftDist = 450, rightDist = 100. + // leftDist > rightDist -> push right (below), right++ + // result: [center, above, below] + // left = 0 (far_above), right = 4 (out) + // push left (far_above) + // result: [center, above, below, far_above] + + assert.equal(result[1].entry.id, "above"); + assert.equal(result[2].entry.id, "below"); + assert.equal(result[3].entry.id, "far_above"); +}); + +test("prioritizeEntries ratio prioritization when distances are similar", (t) => { + const dom = setupDom(); + t.after(() => teardownDom(dom)); + + const viewportCenter = { x: 500, y: 500 }; + + const entries = [ + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 450, height: 100, width: 100 }, // centerY = 500 (distance 0) + intersectionRatio: 1.0, + id: "center" + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 350, height: 100, width: 100 }, // centerY = 400 (distance 100) + intersectionRatio: 0.5, + id: "above_low_ratio" + }, + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + intersectionRect: { top: 550, height: 100, width: 100 }, // centerY = 600 (distance 100) + intersectionRatio: 0.9, + id: "below_high_ratio" + } + ]; + + // Logic check: + // Ordered by Y: above_low_ratio (400), center (500), below_high_ratio (600) + // centerIndex = 1 (center) + // left = 0 (above), right = 2 (below) + // Distances: 100 vs 100. Delta = 0. + // rightRatio (0.9) > leftRatio (0.5) -> True. + // Push right (below) + + const result = prioritizeEntries(entries, viewportCenter); + + assert.equal(result[0].entry.id, "center"); + assert.equal(result[1].entry.id, "below_high_ratio"); + assert.equal(result[2].entry.id, "above_low_ratio"); +}); + +test("prioritizeEntries uses boundingClientRect fallback", (t) => { + const dom = setupDom(); + t.after(() => teardownDom(dom)); + + const viewportCenter = { x: 500, y: 500 }; + const entries = [ + { + isIntersecting: true, + target: dom.window.document.createElement("div"), + // No intersectionRect + boundingClientRect: { top: 450, height: 100, width: 100 }, + intersectionRatio: 1.0, + id: "fallback" + } + ]; + + const result = prioritizeEntries(entries, viewportCenter); + assert.equal(result.length, 1); + assert.equal(result[0].entry.id, "fallback"); +});