From 2469828d0fefe04f1b6ccf3bd1daf4a51f59a662 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 01:14:27 +0000 Subject: [PATCH] test: Add comprehensive tests for HashtagStripHelper resize logic - Added `tests/ui/components/hashtagStripHelper.test.mjs` to test `ResizeObserver` integration, fallback mechanisms (RAF/setTimeout), and teardown. - Implemented robust mocks for `ResizeObserver`, `requestAnimationFrame`, and `setTimeout` within `createDom` helper. - Verified correct behavior for resize events and cleanup. - Ensured test data matches `collectVideoTags` expected structure. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- .../ui/components/hashtagStripHelper.test.mjs | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 tests/ui/components/hashtagStripHelper.test.mjs diff --git a/tests/ui/components/hashtagStripHelper.test.mjs b/tests/ui/components/hashtagStripHelper.test.mjs new file mode 100644 index 00000000..deba8a6d --- /dev/null +++ b/tests/ui/components/hashtagStripHelper.test.mjs @@ -0,0 +1,252 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; +import { HashtagStripHelper } from "../../../js/ui/components/hashtagStripHelper.js"; + +function createDom(options = {}) { + const dom = new JSDOM("
", { + pretendToBeVisual: true, + }); + + const win = dom.window; + const doc = win.document; + + // Mock RequestAnimationFrame if needed + if (options.mockRaf) { + let rafId = 0; + const pendingRafs = new Map(); + win.requestAnimationFrame = (callback) => { + rafId++; + pendingRafs.set(rafId, callback); + return rafId; + }; + win.cancelAnimationFrame = (id) => { + pendingRafs.delete(id); + }; + win.triggerRaf = () => { + const callbacks = Array.from(pendingRafs.values()); + pendingRafs.clear(); + callbacks.forEach((cb) => cb()); + return callbacks.length > 0; + }; + } else if (options.removeRaf) { + delete win.requestAnimationFrame; + delete win.cancelAnimationFrame; + } + + // Mock setTimeout if needed + if (options.mockTimeout) { + let timeoutId = 0; + const pendingTimeouts = new Map(); + win.setTimeout = (callback, delay) => { + timeoutId++; + pendingTimeouts.set(timeoutId, callback); + return timeoutId; + }; + win.clearTimeout = (id) => { + pendingTimeouts.delete(id); + }; + win.triggerTimeout = () => { + const callbacks = Array.from(pendingTimeouts.values()); + pendingTimeouts.clear(); + callbacks.forEach((cb) => cb()); + return callbacks.length > 0; + }; + } else if (options.removeTimeout) { + // JSDOM provides setTimeout by default, remove it if requested + // Note: removing setTimeout might break other things, so use with caution + // But for testing fallback path where RAF is missing, we usually want setTimeout available. + // If we want to test "no RAF, use setTimeout", we remove RAF. + } + + // Mock ResizeObserver + let resizeCallback = null; + let observedElement = null; + let disconnectCalled = false; + + class MockResizeObserver { + constructor(callback) { + resizeCallback = callback; + } + observe(element) { + observedElement = element; + } + disconnect() { + disconnectCalled = true; + observedElement = null; + } + } + + if (options.useResizeObserver) { + win.ResizeObserver = MockResizeObserver; + } else { + delete win.ResizeObserver; + } + + // Helper to trigger resize from observer + win.triggerResizeObserver = () => { + if (resizeCallback && observedElement) { + resizeCallback([{ target: observedElement }]); + return true; + } + return false; + }; + + // Mock window event listeners for fallback path + const eventListeners = new Map(); + const originalAddEventListener = win.addEventListener; + const originalRemoveEventListener = win.removeEventListener; + + win.addEventListener = (event, handler) => { + if (!eventListeners.has(event)) { + eventListeners.set(event, new Set()); + } + eventListeners.get(event).add(handler); + originalAddEventListener.call(win, event, handler); + }; + + win.removeEventListener = (event, handler) => { + if (eventListeners.has(event)) { + eventListeners.get(event).delete(handler); + } + originalRemoveEventListener.call(win, event, handler); + }; + + win.getEventListeners = (event) => { + return eventListeners.get(event) || new Set(); + }; + + win.triggerEvent = (eventName) => { + const event = new win.Event(eventName); + win.dispatchEvent(event); + }; + + return { + window: win, + document: doc, + ResizeObserver: MockResizeObserver, + getResizeObserverState: () => ({ resizeCallback, observedElement, disconnectCalled }), + }; +} + +test("HashtagStripHelper uses ResizeObserver when available", (t) => { + const { window, document, getResizeObserverState } = createDom({ + useResizeObserver: true, + }); + const container = document.getElementById("container"); + + const helper = new HashtagStripHelper({ window, document }); + + // Initial render with data to setup observer + helper.mount(container); + helper.update([{ tags: [["t", "test"]] }]); + + const state = getResizeObserverState(); + assert.ok(state.observedElement === container, "Should observe the container"); + + // Spy on render via side effect: _tagStrip reference change + const initialStrip = helper._tagStrip; + assert.ok(initialStrip, "Initial strip should be rendered"); + + // Trigger resize + window.triggerResizeObserver(); + + assert.notEqual(helper._tagStrip, initialStrip, "Strip should be re-rendered (new element created)"); + assert.ok(helper._tagStrip.isConnected, "New strip should be in the DOM"); + + // Teardown + helper.destroy(); + assert.ok(getResizeObserverState().disconnectCalled, "Should disconnect observer on destroy"); +}); + +test("HashtagStripHelper falls back to window resize with RAF", (t) => { + const { window, document } = createDom({ + useResizeObserver: false, + mockRaf: true, + }); + const container = document.getElementById("container"); + + const helper = new HashtagStripHelper({ window, document }); + + helper.mount(container); + helper.update([{ tags: [["t", "test"]] }]); + + const listeners = window.getEventListeners("resize"); + assert.equal(listeners.size, 1, "Should add window resize listener"); + + const initialStrip = helper._tagStrip; + assert.ok(initialStrip, "Initial strip rendered"); + + // Trigger resize event + window.triggerEvent("resize"); + + // Should not re-render immediately (debounced) + assert.equal(helper._tagStrip, initialStrip, "Should not re-render immediately"); + + // Trigger RAF + const rafTriggered = window.triggerRaf(); + assert.ok(rafTriggered, "RAF callback should have been scheduled"); + + // Now it should have re-rendered + assert.notEqual(helper._tagStrip, initialStrip, "Strip should be re-rendered after RAF"); + + // Teardown + helper.destroy(); + assert.equal(window.getEventListeners("resize").size, 0, "Should remove resize listener on destroy"); +}); + +test("HashtagStripHelper falls back to window resize with setTimeout when RAF is missing", (t) => { + const { window, document } = createDom({ + useResizeObserver: false, + mockRaf: false, // Ensure RAF is missing/removed + removeRaf: true, + mockTimeout: true, + }); + const container = document.getElementById("container"); + + const helper = new HashtagStripHelper({ window, document }); + + helper.mount(container); + helper.update([{ tags: [["t", "test"]] }]); + + const listeners = window.getEventListeners("resize"); + assert.equal(listeners.size, 1, "Should add window resize listener"); + + const initialStrip = helper._tagStrip; + + // Trigger resize event + window.triggerEvent("resize"); + + // Should not re-render immediately + assert.equal(helper._tagStrip, initialStrip, "Should not re-render immediately"); + + // Trigger Timeout + const timeoutTriggered = window.triggerTimeout(); + assert.ok(timeoutTriggered, "Timeout callback should have been scheduled"); + + // Now it should have re-rendered + assert.notEqual(helper._tagStrip, initialStrip, "Strip should be re-rendered after timeout"); + + // Teardown + helper.destroy(); + assert.equal(window.getEventListeners("resize").size, 0, "Should remove resize listener on destroy"); +}); + +test("HashtagStripHelper handles teardown correctly", (t) => { + const { window, document, getResizeObserverState } = createDom({ + useResizeObserver: true, + }); + const container = document.getElementById("container"); + + const helper = new HashtagStripHelper({ window, document }); + helper.mount(container); + helper.update([{ tags: [["t", "test"]] }]); + + assert.ok(getResizeObserverState().observedElement, "Observed before destroy"); + + helper.destroy(); + + assert.ok(getResizeObserverState().disconnectCalled, "Disconnected after destroy"); + assert.equal(helper.container, null, "Container reference cleared"); + assert.equal(helper._tagStrip, null, "Tag strip reference cleared"); +});