mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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 <[email protected]>
This commit is contained in:
1 parent
a20d01a412
commit
a7d19fc1db
3 files changed
+360
-134
No files matched your search
+5
-134
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -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("<!DOCTYPE html><html><body></body></html>");
|
||||
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");
|
||||
});
|
||||
Reference in new issue
Block a user