Files
bitvid/tests/ui/reactionController.test.mjs

183 lines
5.2 KiB
JavaScript

import { describe, it, beforeEach } from "node:test";
import assert from "node:assert";
import ReactionController from "../../js/ui/reactionController.js";
describe("ReactionController", () => {
let controller;
let mockReactionCounter;
let mockVideoModal;
let mockState;
let mockCallbacks;
let mockUi;
beforeEach(() => {
mockReactionCounter = {
subscribe: (pointer, cb) => {
return () => {};
},
publish: async () => ({ ok: true })
};
mockVideoModal = {
updateReactionSummary: () => {},
setUserReaction: () => {}
};
mockUi = {
getVideoModal: () => mockVideoModal,
showError: () => {}
};
mockState = {
getCurrentVideo: () => ({ id: "v1", pubkey: "p1" }),
getCurrentVideoPointer: () => ["e", "v1"],
getCurrentVideoPointerKey: () => "e:v1"
};
mockCallbacks = {
isUserLoggedIn: () => true,
normalizeHexPubkey: (val) => val,
getPubkey: () => "user1"
};
controller = new ReactionController({
services: { reactionCounter: mockReactionCounter },
ui: mockUi,
state: mockState,
callbacks: mockCallbacks
});
});
it("should subscribe to reactions", (t) => {
const subscribeMock = t.mock.method(mockReactionCounter, "subscribe");
controller.subscribe(["e", "v1"], "e:v1");
assert.strictEqual(subscribeMock.mock.callCount(), 1);
assert.deepStrictEqual(subscribeMock.mock.calls[0].arguments[0], [
"e",
"v1"
]);
});
it("includes the immutable event ID when subscribing to addressable videos", (t) => {
const subscribeMock = t.mock.method(mockReactionCounter, "subscribe");
controller.subscribe(
["a", "34235:p1:episode-42", "wss://relay.example"],
"a:34235:p1:episode-42"
);
assert.deepStrictEqual(subscribeMock.mock.calls[0].arguments[0], {
type: "a",
value: "34235:p1:episode-42",
relay: "wss://relay.example",
eventId: "v1"
});
});
it("should handle reaction update from subscription", (t) => {
let subCallback;
mockReactionCounter.subscribe = (pointer, cb) => {
subCallback = cb;
return () => {};
};
const updateSummaryMock = t.mock.method(
mockVideoModal,
"updateReactionSummary"
);
controller.subscribe(["e", "v1"], "e:v1");
// subscribe calls unsubscribe which calls resetState which calls updateReactionSummary (1st call)
subCallback({
total: 10,
counts: { "+": 10 },
reactions: { user1: { content: "+" } }
});
// subCallback calls updateReactionSummary (2nd call)
assert.strictEqual(updateSummaryMock.mock.callCount(), 2);
assert.deepStrictEqual(updateSummaryMock.mock.calls[1].arguments[0], {
total: 10,
counts: { "+": 10, "-": 0 },
userReaction: "+"
});
});
it("should apply optimistic update on handleReaction", async (t) => {
const updateSummaryMock = t.mock.method(
mockVideoModal,
"updateReactionSummary"
);
const publishMock = t.mock.method(mockReactionCounter, "publish");
// Pre-seed state
controller.reactionState = {
total: 5,
counts: { "+": 5, "-": 0 },
userReaction: ""
};
await controller.handleReaction({ reaction: "+" });
// Verify optimistic update call
// Should increment + count and total
assert.strictEqual(updateSummaryMock.mock.callCount(), 1);
const updateArgs = updateSummaryMock.mock.calls[0].arguments[0];
assert.strictEqual(updateArgs.total, 6);
assert.strictEqual(updateArgs.counts["+"], 6);
assert.strictEqual(updateArgs.userReaction, "+");
// Verify publish call
assert.strictEqual(publishMock.mock.callCount(), 1);
});
it("should rollback optimistic update on publish failure", async (t) => {
mockReactionCounter.publish = async () => ({ ok: false });
const updateSummaryMock = t.mock.method(
mockVideoModal,
"updateReactionSummary"
);
// We need to update controller.ui.showError because constructor copied the reference
const showErrorMock = t.mock.method(mockUi, "showError");
controller.ui.showError = showErrorMock;
controller.reactionState = {
total: 5,
counts: { "+": 5, "-": 0 },
userReaction: ""
};
await controller.handleReaction({ reaction: "+" });
// 1st call: optimistic update
// 2nd call: restore snapshot
assert.strictEqual(updateSummaryMock.mock.callCount(), 2);
// Restore check
const restoreArgs = updateSummaryMock.mock.calls[1].arguments[0];
assert.strictEqual(restoreArgs.total, 5);
assert.strictEqual(restoreArgs.counts["+"], 5);
assert.strictEqual(restoreArgs.userReaction, "");
assert.strictEqual(showErrorMock.mock.callCount(), 1);
});
it("should not react if user not logged in", async (t) => {
// Update callback on controller instance
controller.callbacks.isUserLoggedIn = () => false;
const showErrorMock = t.mock.method(mockUi, "showError");
controller.ui.showError = showErrorMock;
const publishMock = t.mock.method(mockReactionCounter, "publish");
await controller.handleReaction({ reaction: "+" });
assert.strictEqual(showErrorMock.mock.callCount(), 1);
assert.strictEqual(publishMock.mock.callCount(), 0);
});
});