mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Optimize feed lifecycle and navigation rendering
This commit is contained in:
1 parent
2c4aefe872
commit
687f8c438d
12 files changed
+287
-21
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { createCardObserver } from "../../js/dom/cardObserver.js";
|
||||
|
||||
test("card observer disconnect unregisters every card and releases its observer", () => {
|
||||
const dom = new JSDOM("<div id=grid><article class='card' data-video-id=a></article><article class='card' data-video-id=b></article></div>");
|
||||
const originalHTMLElement = globalThis.HTMLElement;
|
||||
const originalIntersectionObserver = globalThis.IntersectionObserver;
|
||||
globalThis.HTMLElement = dom.window.HTMLElement;
|
||||
let disconnects = 0;
|
||||
globalThis.IntersectionObserver = class {
|
||||
observe() {}
|
||||
takeRecords() { return []; }
|
||||
disconnect() { disconnects += 1; }
|
||||
};
|
||||
|
||||
const unregistered = [];
|
||||
try {
|
||||
const observer = createCardObserver({
|
||||
onCardUnregister: ({ card }) => unregistered.push(card.dataset.videoId),
|
||||
});
|
||||
const grid = dom.window.document.getElementById("grid");
|
||||
observer.observe(grid);
|
||||
observer.disconnect(grid);
|
||||
|
||||
assert.deepEqual(unregistered.sort(), ["a", "b"]);
|
||||
assert.equal(disconnects, 1);
|
||||
assert.equal(observer.getState(grid), null);
|
||||
} finally {
|
||||
dom.window.close();
|
||||
if (originalHTMLElement === undefined) delete globalThis.HTMLElement;
|
||||
else globalThis.HTMLElement = originalHTMLElement;
|
||||
if (originalIntersectionObserver === undefined) delete globalThis.IntersectionObserver;
|
||||
else globalThis.IntersectionObserver = originalIntersectionObserver;
|
||||
}
|
||||
});
|
||||
@@ -33,3 +33,33 @@ test("ExploreDataService stays idle until a recommendation feed activates it", a
|
||||
assert.equal(idfRefreshes, 1, "inactive feeds do not rebuild recommendation indexes");
|
||||
service.destroy();
|
||||
});
|
||||
|
||||
test("ExploreDataService coalesces a refresh burst into one running pass and one follow-up", async () => {
|
||||
const service = new ExploreDataService({
|
||||
historyRefreshIntervalMs: 0,
|
||||
idfRefreshIntervalMs: 0,
|
||||
});
|
||||
service.active = true;
|
||||
let runs = 0;
|
||||
let releaseFirst;
|
||||
service._refreshTagIdf = async () => {
|
||||
runs += 1;
|
||||
if (runs === 1) {
|
||||
await new Promise((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
}
|
||||
return new Map();
|
||||
};
|
||||
|
||||
const first = service.refreshTagIdf({ force: true, reason: "initial" });
|
||||
const duplicate = service.refreshTagIdf({ reason: "relay-event" });
|
||||
service.refreshTagIdf({ reason: "timer" });
|
||||
assert.equal(runs, 1, "only one rebuild starts during the burst");
|
||||
|
||||
releaseFirst();
|
||||
await Promise.all([first, duplicate]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(runs, 2, "the burst produces exactly one follow-up rebuild");
|
||||
service.destroy();
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { VideoListView } from "../../js/ui/views/VideoListView.js";
|
||||
|
||||
test("VideoListView disposes health observers before replacing its card grid", () => {
|
||||
const dom = new JSDOM("<div id=grid></div>");
|
||||
const grid = dom.window.document.getElementById("grid");
|
||||
let healthDetaches = 0;
|
||||
let urlDetaches = 0;
|
||||
const view = new VideoListView({
|
||||
document: dom.window.document,
|
||||
badgeHelpers: {
|
||||
attachHealthBadges: () => () => { healthDetaches += 1; },
|
||||
attachUrlHealthBadges: () => () => { urlDetaches += 1; },
|
||||
},
|
||||
});
|
||||
view.mount(grid);
|
||||
const videos = [{ id: "one", title: "One", pubkey: "a", created_at: 1 }];
|
||||
|
||||
view.render(videos);
|
||||
view.render([{ ...videos[0], title: "Updated", created_at: 2 }]);
|
||||
view.unmount();
|
||||
|
||||
assert.equal(healthDetaches, 2, "rerender and unmount each detach health observers");
|
||||
assert.equal(urlDetaches, 2, "rerender and unmount each detach URL observers");
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
test("VideoListView restores cards when a navigation remount clears unchanged feed markup", () => {
|
||||
const dom = new JSDOM("<div id=grid></div>");
|
||||
const grid = dom.window.document.getElementById("grid");
|
||||
const view = new VideoListView({ document: dom.window.document });
|
||||
const videos = [{ id: "for-you-one", title: "For You", pubkey: "a", created_at: 1 }];
|
||||
|
||||
view.mount(grid);
|
||||
view.render(videos);
|
||||
assert.equal(grid.querySelectorAll('[data-component="video-card"]').length, 1);
|
||||
|
||||
// Loading a view replaces #videoList. The data can still be identical when
|
||||
// returning to For You, so a signature-only cache must not skip the repaint.
|
||||
grid.innerHTML = '<div data-for-you-empty-state="true"></div>';
|
||||
view.render(videos);
|
||||
|
||||
assert.equal(grid.querySelectorAll('[data-component="video-card"]').length, 1);
|
||||
assert.equal(grid.querySelector('[data-for-you-empty-state]'), null);
|
||||
dom.window.close();
|
||||
});
|
||||
@@ -373,3 +373,25 @@ test("most-zapped sorter: sats desc, recency tie-break, muted sinks", () => {
|
||||
"sats desc, newer wins ties, trusted-muted last regardless of sats",
|
||||
);
|
||||
});
|
||||
|
||||
test("most-zapped sorter keeps strict zap order when one creator has several top videos", () => {
|
||||
const item = (id, author, sats, createdAt) => ({
|
||||
video: { id, pubkey: author, created_at: createdAt },
|
||||
metadata: {},
|
||||
sats,
|
||||
});
|
||||
const items = [
|
||||
item("creator-a-top", "creator-a", 900, 100),
|
||||
item("creator-a-second", "creator-a", 800, 200),
|
||||
item("creator-b", "creator-b", 700, 300),
|
||||
];
|
||||
const sorted = createMostZappedSorter()(items, {
|
||||
runtime: { getZapTotal: (video) => items.find((entry) => entry.video === video)?.sats || 0 },
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
sorted.map((entry) => entry.video.id),
|
||||
["creator-a-top", "creator-a-second", "creator-b"],
|
||||
"a diversity rule must not move a lower-zapped video above a higher-zapped one",
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user