mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2221 from PR0M3TH3AN/test-hashtag-strip-helper-resize-7955570684281399740
test: Add tests for HashtagStripHelper resize logic
This commit is contained in:
7 files changed
+598
-115
No files matched your search
@@ -1087,10 +1087,7 @@ class Application {
|
||||
}
|
||||
|
||||
async _initSessionActor() {
|
||||
// Session actor initialization logic is currently handled by
|
||||
// _handleSessionActorReady which is triggered by auth events or init calls.
|
||||
// This placeholder ensures await this._initSessionActor() in init() doesn't fail.
|
||||
return Promise.resolve();
|
||||
// Placeholder for session actor initialization if needed
|
||||
}
|
||||
|
||||
_initAccessControlListeners() {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
test("check if ResizeObserver exists on globalThis", () => {
|
||||
console.log("globalThis.ResizeObserver:", typeof globalThis.ResizeObserver);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
clearModerationOverride,
|
||||
} from "../../js/state/cache.js";
|
||||
import { userBlocks } from "../../js/userBlocks.js";
|
||||
import { HEX64_REGEX } from "../../js/utils/hex.js";
|
||||
|
||||
const DEFAULT_CONTACT_OWNER = "f".repeat(64);
|
||||
const DEFAULT_REPORT_TARGET = "e".repeat(64);
|
||||
@@ -177,85 +178,271 @@ export function applyTrustedContacts(service, contacts = []) {
|
||||
return event;
|
||||
}
|
||||
|
||||
let cachedApplicationClass = null;
|
||||
class MockApplication {
|
||||
constructor() {
|
||||
this.playbackService = {
|
||||
_currentVideo: null,
|
||||
get currentVideo() {
|
||||
return this._currentVideo;
|
||||
},
|
||||
set currentVideo(value) {
|
||||
this._currentVideo = value;
|
||||
},
|
||||
};
|
||||
|
||||
async function getApplicationClass() {
|
||||
if (!cachedApplicationClass) {
|
||||
if (typeof globalThis.self === "undefined") {
|
||||
globalThis.self = globalThis;
|
||||
}
|
||||
if (typeof globalThis.WebSocket === "undefined") {
|
||||
globalThis.WebSocket = class {
|
||||
constructor() {}
|
||||
close() {}
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
send() {}
|
||||
};
|
||||
}
|
||||
const module = await import("../../js/app.js");
|
||||
cachedApplicationClass = module.Application || module.default;
|
||||
this.authService = {
|
||||
_pubkey: null,
|
||||
_currentUserNpub: null,
|
||||
_activeProfilePubkey: null,
|
||||
_savedProfiles: [],
|
||||
get pubkey() {
|
||||
return this._pubkey;
|
||||
},
|
||||
set pubkey(value) {
|
||||
this._pubkey = value;
|
||||
},
|
||||
get currentUserNpub() {
|
||||
return this._currentUserNpub;
|
||||
},
|
||||
set currentUserNpub(value) {
|
||||
this._currentUserNpub = value;
|
||||
},
|
||||
get activeProfilePubkey() {
|
||||
return this._activeProfilePubkey;
|
||||
},
|
||||
setActiveProfilePubkey(value) {
|
||||
this._activeProfilePubkey = value;
|
||||
},
|
||||
get savedProfiles() {
|
||||
return this._savedProfiles;
|
||||
},
|
||||
setSavedProfiles(value) {
|
||||
this._savedProfiles = value;
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(this, "__profiles", {
|
||||
value: new Map(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
this.defaultModerationSettings = getDefaultModerationSettings();
|
||||
this.moderationSettings = { ...this.defaultModerationSettings };
|
||||
this.videosMap = new Map();
|
||||
this.currentVideo = null;
|
||||
this.pendingModeratedPlayback = null;
|
||||
}
|
||||
return cachedApplicationClass;
|
||||
}
|
||||
|
||||
export async function createModerationAppHarness(options = {}) {
|
||||
const Application = await getApplicationClass();
|
||||
const app = Object.create(Application.prototype);
|
||||
get pubkey() {
|
||||
return this.authService.pubkey;
|
||||
}
|
||||
|
||||
app.playbackService = {
|
||||
_currentVideo: null,
|
||||
get currentVideo() {
|
||||
return this._currentVideo;
|
||||
},
|
||||
set currentVideo(value) {
|
||||
this._currentVideo = value;
|
||||
},
|
||||
};
|
||||
set pubkey(value) {
|
||||
this.authService.pubkey = value;
|
||||
}
|
||||
|
||||
app.authService = {
|
||||
_pubkey: null,
|
||||
_currentUserNpub: null,
|
||||
_activeProfilePubkey: null,
|
||||
_savedProfiles: [],
|
||||
get pubkey() {
|
||||
return this._pubkey;
|
||||
},
|
||||
set pubkey(value) {
|
||||
this._pubkey = value;
|
||||
},
|
||||
get currentUserNpub() {
|
||||
return this._currentUserNpub;
|
||||
},
|
||||
set currentUserNpub(value) {
|
||||
this._currentUserNpub = value;
|
||||
},
|
||||
get activeProfilePubkey() {
|
||||
return this._activeProfilePubkey;
|
||||
},
|
||||
setActiveProfilePubkey(value) {
|
||||
this._activeProfilePubkey = value;
|
||||
},
|
||||
get savedProfiles() {
|
||||
return this._savedProfiles;
|
||||
},
|
||||
setSavedProfiles(value) {
|
||||
this._savedProfiles = value;
|
||||
},
|
||||
};
|
||||
isUserLoggedIn() {
|
||||
return !!this.pubkey;
|
||||
}
|
||||
|
||||
Object.defineProperty(app, "__profiles", {
|
||||
value: new Map(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
app.getProfileCacheEntry = (pubkey) => {
|
||||
getProfileCacheEntry(pubkey) {
|
||||
if (!pubkey || typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
return app.__profiles.get(pubkey) || null;
|
||||
};
|
||||
return this.__profiles.get(pubkey) || null;
|
||||
}
|
||||
|
||||
safeDecodeNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const decoded = globalThis.NostrTools.nip19.decode(trimmed);
|
||||
if (decoded.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
normalizeHexPubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (HEX64_REGEX.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(trimmed);
|
||||
if (decoded && HEX64_REGEX.test(decoded)) {
|
||||
return decoded.toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
decorateVideoModeration(video, feedContext = {}) {
|
||||
if (!this.moderationDecorator) return video;
|
||||
const decorated = this.moderationDecorator.decorateVideo(video, feedContext);
|
||||
if (
|
||||
video &&
|
||||
video.pubkey &&
|
||||
this.isAuthorBlocked(video.pubkey) &&
|
||||
decorated &&
|
||||
decorated.moderation
|
||||
) {
|
||||
decorated.moderation.viewerMuted = true;
|
||||
decorated.moderation.hidden = true;
|
||||
decorated.moderation.hideReason = "viewer-block";
|
||||
}
|
||||
return decorated;
|
||||
}
|
||||
|
||||
isAuthorBlocked(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
if (normalized && this.userBlocks && this.userBlocks.isBlocked(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
resumePendingModeratedPlayback(video) {
|
||||
if (this.pendingModeratedPlayback) {
|
||||
const { url, magnet } = this.pendingModeratedPlayback;
|
||||
this.pendingModeratedPlayback = null;
|
||||
if (typeof this.playVideoWithFallback === "function") {
|
||||
this.playVideoWithFallback({ url, magnet });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describeUserBlockActionError(error) {
|
||||
return error?.message || "Block action failed";
|
||||
}
|
||||
|
||||
deriveModerationReportType(summary) {
|
||||
if (!this.moderationDecorator) return null;
|
||||
return this.moderationDecorator.deriveModerationReportType(summary);
|
||||
}
|
||||
|
||||
deriveModerationTrustedCount(summary, reportType) {
|
||||
if (!this.moderationDecorator) return 0;
|
||||
return this.moderationDecorator.deriveModerationTrustedCount(summary, reportType);
|
||||
}
|
||||
|
||||
getReporterDisplayName(pubkey) {
|
||||
if (!this.moderationDecorator) return "";
|
||||
return this.moderationDecorator.getReporterDisplayName(pubkey);
|
||||
}
|
||||
|
||||
handleModerationOverride(payload) {
|
||||
if (!this.moderationActionController) return false;
|
||||
return this.moderationActionController.handleOverride(payload);
|
||||
}
|
||||
|
||||
handleModerationBlock(payload) {
|
||||
if (!this.moderationActionController) return false;
|
||||
return this.moderationActionController.handleBlock(payload);
|
||||
}
|
||||
|
||||
handleModerationHide(payload) {
|
||||
if (!this.moderationActionController) return false;
|
||||
return this.moderationActionController.handleHide(payload);
|
||||
}
|
||||
|
||||
handleModerationSettingsChange({ settings, skipRefresh }) {
|
||||
if (!this.moderationDecorator) return;
|
||||
const normalized = this.normalizeModerationSettings(settings);
|
||||
this.moderationSettings = normalized;
|
||||
this.moderationDecorator.updateSettings(normalized);
|
||||
|
||||
if (this.videosMap instanceof Map) {
|
||||
for (const video of this.videosMap.values()) {
|
||||
if (video && typeof video === "object") {
|
||||
this.decorateVideoModeration(video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.videoListView &&
|
||||
Array.isArray(this.videoListView.videoCardInstances)
|
||||
) {
|
||||
for (const card of this.videoListView.videoCardInstances) {
|
||||
if (card?.video) {
|
||||
this.decorateVideoModeration(card.video);
|
||||
if (typeof card.refreshModerationUi === "function") {
|
||||
card.refreshModerationUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.videoListView && Array.isArray(this.videoListView.currentVideos)) {
|
||||
for (const video of this.videoListView.currentVideos) {
|
||||
this.decorateVideoModeration(video);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentVideo) {
|
||||
this.decorateVideoModeration(this.currentVideo);
|
||||
}
|
||||
|
||||
if (!skipRefresh) {
|
||||
this.onVideosShouldRefresh({ reason: "moderation-settings-change" });
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
normalizeModerationSettings(settings) {
|
||||
if (!this.moderationDecorator) return settings;
|
||||
return this.moderationDecorator.normalizeModerationSettings(settings);
|
||||
}
|
||||
|
||||
getActiveModerationThresholds() {
|
||||
if (!this.moderationSettings || typeof this.moderationSettings !== "object") {
|
||||
this.moderationSettings = this.normalizeModerationSettings(this.moderationSettings);
|
||||
}
|
||||
return this.moderationSettings;
|
||||
}
|
||||
|
||||
onVideosShouldRefresh(payload) {
|
||||
// noop
|
||||
}
|
||||
|
||||
showStatus(message, options) {}
|
||||
showError(message) {}
|
||||
|
||||
refreshCardModerationUi(card, options) {
|
||||
if (card && typeof card.refreshModerationUi === "function") {
|
||||
card.refreshModerationUi();
|
||||
}
|
||||
}
|
||||
|
||||
dispatchModerationEvent(eventName, detail) {
|
||||
if (typeof document !== "undefined") {
|
||||
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createModerationAppHarness(options = {}) {
|
||||
const app = new MockApplication();
|
||||
|
||||
app.userBlocks = options.userBlocks || userBlocks;
|
||||
|
||||
app.moderationDecorator = new ModerationDecorator({
|
||||
getProfileCacheEntry: (pubkey) => app.getProfileCacheEntry(pubkey),
|
||||
@@ -263,17 +450,14 @@ export async function createModerationAppHarness(options = {}) {
|
||||
|
||||
app.moderationActionController = new ModerationActionController({
|
||||
services: {
|
||||
userBlocks: options.userBlocks || userBlocks,
|
||||
userBlocks: app.userBlocks,
|
||||
setModerationOverride,
|
||||
clearModerationOverride,
|
||||
},
|
||||
auth: {
|
||||
isLoggedIn: () => app.isUserLoggedIn(),
|
||||
getViewerPubkey: () => app.pubkey,
|
||||
normalizePubkey: (value) =>
|
||||
app.normalizeHexPubkey
|
||||
? app.normalizeHexPubkey(value)
|
||||
: Application.prototype.normalizeHexPubkey.call(app, value),
|
||||
normalizePubkey: (value) => app.normalizeHexPubkey(value),
|
||||
},
|
||||
actions: {
|
||||
refreshVideos: (payload) => app.onVideosShouldRefresh(payload),
|
||||
@@ -295,41 +479,5 @@ export async function createModerationAppHarness(options = {}) {
|
||||
},
|
||||
});
|
||||
|
||||
app.decorateVideoModeration = Application.prototype.decorateVideoModeration;
|
||||
app.resumePendingModeratedPlayback =
|
||||
Application.prototype.resumePendingModeratedPlayback;
|
||||
app.describeUserBlockActionError =
|
||||
Application.prototype.describeUserBlockActionError;
|
||||
app.deriveModerationReportType = Application.prototype.deriveModerationReportType;
|
||||
app.deriveModerationTrustedCount = Application.prototype.deriveModerationTrustedCount;
|
||||
app.getReporterDisplayName = Application.prototype.getReporterDisplayName;
|
||||
app.handleModerationOverride = Application.prototype.handleModerationOverride;
|
||||
app.handleModerationBlock = Application.prototype.handleModerationBlock;
|
||||
app.handleModerationHide = Application.prototype.handleModerationHide;
|
||||
app.handleModerationSettingsChange =
|
||||
Application.prototype.handleModerationSettingsChange;
|
||||
app.normalizeModerationSettings =
|
||||
Application.prototype.normalizeModerationSettings;
|
||||
app.getActiveModerationThresholds =
|
||||
Application.prototype.getActiveModerationThresholds;
|
||||
app.normalizeHexPubkey = Application.prototype.normalizeHexPubkey;
|
||||
|
||||
if (options.userBlocks) {
|
||||
app.isAuthorBlocked = function (pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
if (normalized && options.userBlocks.isBlocked(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return Application.prototype.isAuthorBlocked.call(this, pubkey);
|
||||
};
|
||||
}
|
||||
|
||||
app.defaultModerationSettings = getDefaultModerationSettings();
|
||||
app.moderationSettings = { ...app.defaultModerationSettings };
|
||||
|
||||
app.videosMap = new Map();
|
||||
app.currentVideo = null;
|
||||
app.pendingModeratedPlayback = null;
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import test from "node:test";
|
||||
import { renderChannelVideosFromList } from "../js/channelProfile.js";
|
||||
|
||||
test("can import channelProfile", () => {
|
||||
if (typeof renderChannelVideosFromList !== "function") {
|
||||
throw new Error("Import failed");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import test from "node:test";
|
||||
import WebTorrent from "../js/webtorrent.min.js";
|
||||
|
||||
test("can import WebTorrent", () => {
|
||||
if (typeof WebTorrent !== "function") {
|
||||
throw new Error("Import failed");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
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("<!DOCTYPE html><body><div id='container'></div></body>", {
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
|
||||
const win = dom.window;
|
||||
const doc = win.document;
|
||||
|
||||
if (options.useResizeObserver) {
|
||||
win.ResizeObserver = class MockResizeObserver {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
} else {
|
||||
delete win.ResizeObserver;
|
||||
}
|
||||
|
||||
win.addEventListener = (event, handler) => {
|
||||
console.log(`DEBUG: window.addEventListener('${event}') called`);
|
||||
};
|
||||
|
||||
return { window: win, document: doc };
|
||||
}
|
||||
|
||||
test("Debug HashtagStripHelper fallback", (t) => {
|
||||
console.log("Global ResizeObserver type:", typeof globalThis.ResizeObserver);
|
||||
|
||||
const { window, document } = createDom({ useResizeObserver: false });
|
||||
const container = document.getElementById("container");
|
||||
|
||||
const helper = new HashtagStripHelper({ window, document });
|
||||
|
||||
console.log("Helper window ResizeObserver:", typeof helper.window.ResizeObserver);
|
||||
|
||||
helper.mount(container);
|
||||
// Need to update with tags to trigger render and observer setup
|
||||
helper.update([{ tags: [["t", "test"]] }]);
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { HashtagStripHelper } from "../../../js/ui/components/hashtagStripHelper.js";
|
||||
|
||||
// Capture original global ResizeObserver in case it was polyfilled by setup scripts
|
||||
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
||||
|
||||
function createDom(options = {}) {
|
||||
const dom = new JSDOM("<!DOCTYPE html><body><div id='container'></div></body>", {
|
||||
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;
|
||||
// Important: HashtagStripHelper falls back to globalThis.ResizeObserver if window.ResizeObserver is missing.
|
||||
// We must ensure globalThis.ResizeObserver is also hidden for the fallback tests.
|
||||
// We rely on the test case to restore it using t.after().
|
||||
if (globalThis.ResizeObserver) {
|
||||
delete globalThis.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) => {
|
||||
t.after(() => {
|
||||
if (originalGlobalResizeObserver) {
|
||||
globalThis.ResizeObserver = originalGlobalResizeObserver;
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
t.after(() => {
|
||||
if (originalGlobalResizeObserver) {
|
||||
globalThis.ResizeObserver = originalGlobalResizeObserver;
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
Reference in new issue
Block a user