Files
bitvid/tests/ui/tagPreferenceMenuController.test.mjs
T
google-labs-jules[bot]andPR0M3TH3AN f77c89baf6 fix: ensure active feed refreshes when hashtag preferences change
* Update `refreshAllVideoGrids` in `js/app.js` to detect the currently active feed (For You, Kids, Explore, or Recent) and invoke the corresponding loader, preventing staleness when interacting with the UI.
* Refactor `HashtagPreferencesService` mutation methods (`addInterest`, etc.) to be `async` and await the `publish()` call, ensuring state is persisted before returning.
* Remove redundant `persistPreferencesFromMenu` call in `TagPreferenceMenuController`, relying on the service's internal persistence logic.
* Update unit tests for `HashtagPreferencesService` and `TagPreferenceMenuController` to align with the async behavior.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-03 01:55:21 +00:00

186 lines
6.3 KiB
JavaScript

import assert from "assert";
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import TagPreferenceMenuController from "../../js/ui/tagPreferenceMenuController.js";
import { TAG_PREFERENCE_ACTIONS } from "../../js/ui/components/tagPreferenceMenu.js";
describe("TagPreferenceMenuController", () => {
let controller;
let mockHashtagPreferences;
let mockServices;
let mockCallbacks;
let mockHelpers;
let mockPopover;
let mockPopoverInstance;
beforeEach(() => {
mockHashtagPreferences = {
addInterest: mock.fn(() => true),
removeInterest: mock.fn(() => true),
addDisinterest: mock.fn(() => true),
removeDisinterest: mock.fn(() => true),
publish: mock.fn(async () => true),
};
mockServices = {
hashtagPreferences: mockHashtagPreferences,
};
mockCallbacks = {
isLoggedIn: mock.fn(() => true),
getMembership: mock.fn(() => ({ interest: false, disinterest: false })),
showError: mock.fn(),
onPreferenceUpdate: mock.fn(),
onMenuOpen: mock.fn(),
getPubkey: mock.fn(() => "test-pubkey"),
};
mockPopoverInstance = {
open: mock.fn(async () => {}),
close: mock.fn(() => true),
destroy: mock.fn(),
isOpen: mock.fn(() => false),
};
mockPopover = mock.fn(() => mockPopoverInstance);
mockHelpers = {
createPopover: mockPopover,
designSystem: { mode: "default" },
};
controller = new TagPreferenceMenuController({
services: mockServices,
callbacks: mockCallbacks,
helpers: mockHelpers,
});
});
afterEach(() => {
mock.reset();
});
describe("ensurePopover", () => {
it("should create a new popover entry if one does not exist", () => {
const trigger = { nodeType: 1, ownerDocument: global.document }; // Mock DOM element
const detail = { trigger, tag: "bitcoin" };
const entry = controller.ensurePopover(detail);
assert.ok(entry);
assert.strictEqual(entry.tag, "bitcoin");
assert.strictEqual(entry.trigger, trigger);
assert.strictEqual(entry.popover, mockPopoverInstance);
assert.strictEqual(mockPopover.mock.calls.length, 1);
});
it("should return existing entry if one exists", () => {
const trigger = { nodeType: 1, ownerDocument: global.document };
const detail = { trigger, tag: "bitcoin" };
const entry1 = controller.ensurePopover(detail);
const entry2 = controller.ensurePopover(detail);
assert.strictEqual(entry1, entry2);
assert.strictEqual(mockPopover.mock.calls.length, 1);
});
it("should return null if trigger or tag is missing", () => {
assert.strictEqual(controller.ensurePopover({}), null);
assert.strictEqual(controller.ensurePopover({ trigger: {} }), null); // trigger not element
assert.strictEqual(controller.ensurePopover({ trigger: { nodeType: 1 } }), null); // missing tag
});
});
describe("requestMenu", () => {
it("should ensure popover, close others, and open the requested one", async () => {
const trigger = { nodeType: 1, ownerDocument: global.document };
const detail = { trigger, tag: "bitcoin" };
// Spy on closeMenus
const closeMenusSpy = mock.method(controller, "closeMenus");
controller.requestMenu(detail);
assert.strictEqual(closeMenusSpy.mock.calls.length, 1);
assert.strictEqual(mockCallbacks.onMenuOpen.mock.calls.length, 1);
assert.strictEqual(mockPopoverInstance.open.mock.calls.length, 1);
});
it("should close the popover if it is already open", () => {
const trigger = { nodeType: 1, ownerDocument: global.document };
const detail = { trigger, tag: "bitcoin" };
mockPopoverInstance.isOpen = mock.fn(() => true);
controller.requestMenu(detail);
assert.strictEqual(mockPopoverInstance.close.mock.calls.length, 1);
assert.strictEqual(mockPopoverInstance.open.mock.calls.length, 0);
});
});
describe("handleMenuAction", () => {
it("should call service method and notify update", async () => {
const tag = "bitcoin";
const action = TAG_PREFERENCE_ACTIONS.ADD_INTEREST;
await controller.handleMenuAction(action, { tag });
assert.strictEqual(mockHashtagPreferences.addInterest.mock.calls.length, 1);
assert.strictEqual(mockHashtagPreferences.addInterest.mock.calls[0].arguments[0], tag);
// onPreferenceUpdate should be called once
assert.strictEqual(mockCallbacks.onPreferenceUpdate.mock.calls.length, 1);
// publish should not be called by controller directly
assert.strictEqual(mockHashtagPreferences.publish.mock.calls.length, 0);
});
it("should handle error and call showError", async () => {
const tag = "bitcoin";
const action = TAG_PREFERENCE_ACTIONS.ADD_INTEREST;
mockHashtagPreferences.addInterest = mock.fn(() => {
throw new Error("Service error");
});
await controller.handleMenuAction(action, { tag });
assert.strictEqual(mockCallbacks.showError.mock.calls.length, 1);
assert.match(mockCallbacks.showError.mock.calls[0].arguments[0], /Failed to update hashtag preferences/);
});
});
describe("persistPreferencesFromMenu", () => {
it("should call service.publish", async () => {
await controller.persistPreferencesFromMenu();
assert.strictEqual(mockHashtagPreferences.publish.mock.calls.length, 1);
});
it("should reuse in-flight promise", async () => {
let resolvePublish;
const slowPublish = new Promise((resolve) => { resolvePublish = resolve; });
mockHashtagPreferences.publish = mock.fn(() => slowPublish);
const p1 = controller.persistPreferencesFromMenu();
// Ensure p1 is set before calling again
assert.strictEqual(controller.publishInFlight, true);
const p2 = controller.persistPreferencesFromMenu();
// Wait for p1 to finish, but we want to assert equality of promises returned.
// Note: p1 is the result of async IIFE.
// Inside controller:
// this.publishPromise = publishPromise;
// return publishPromise;
assert.strictEqual(p1, p2);
assert.strictEqual(mockHashtagPreferences.publish.mock.calls.length, 1);
resolvePublish(true);
await p1;
});
});
});