diff --git a/tests/services/exploreDataService.test.mjs b/tests/services/exploreDataService.test.mjs new file mode 100644 index 00000000..ac9be04f --- /dev/null +++ b/tests/services/exploreDataService.test.mjs @@ -0,0 +1,167 @@ + +import { describe, it, before, beforeEach, afterEach, after, mock } from 'node:test'; +import assert from 'node:assert'; + +// Set Dev Mode Override to ensure logs are emitted +globalThis.__BITVID_DEV_MODE_OVERRIDE__ = true; + +// Mock Worker globally before importing the service +class MockWorker { + constructor(scriptURL) { + this.scriptURL = scriptURL; + this.listeners = {}; + MockWorker.instances.push(this); + } + + addEventListener(event, handler) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(handler); + } + + removeEventListener(event, handler) { + if (this.listeners[event]) { + this.listeners[event] = this.listeners[event].filter((h) => h !== handler); + } + } + + postMessage(data) { + // Simulate async processing + setImmediate(() => { + if (MockWorker.onPostMessage) { + MockWorker.onPostMessage(this, data); + } + }); + } + + // Helper to simulate incoming message from worker + emitMessage(data) { + if (this.listeners['message']) { + const event = { data }; + this.listeners['message'].forEach((handler) => handler(event)); + } + } +} + +MockWorker.instances = []; +MockWorker.onPostMessage = null; + +globalThis.Worker = MockWorker; + +describe('exploreDataService - buildWatchHistoryTagCounts', () => { + let buildWatchHistoryTagCounts; + let consoleWarnMock; + let initialCallCount = 0; + + before(async () => { + // Mock console.warn BEFORE import so logger.js captures the mock + // if console.warn is already mocked (e.g. by other tests? unlikely in isolated process), restore it first + if (console.warn.mock) console.warn.mock.restore(); + + consoleWarnMock = mock.method(console, 'warn', () => {}); + + // Dynamic import + const mod = await import('../../js/services/exploreDataService.js'); + buildWatchHistoryTagCounts = mod.buildWatchHistoryTagCounts; + }); + + after(() => { + if (consoleWarnMock) consoleWarnMock.mock.restore(); + }); + + beforeEach(() => { + MockWorker.instances = []; + MockWorker.onPostMessage = null; + initialCallCount = consoleWarnMock.mock.calls.length; + }); + + it('should return empty Map if watchHistoryService is missing', async () => { + const result = await buildWatchHistoryTagCounts({}); + assert.ok(result instanceof Map); + assert.strictEqual(result.size, 0); + }); + + it('should handle missing loadLatest method gracefully', async () => { + const watchHistoryService = {}; + const result = await buildWatchHistoryTagCounts({ watchHistoryService }); + assert.strictEqual(result.size, 0); + }); + + it('should handle loadLatest failure gracefully', async () => { + const watchHistoryService = { + loadLatest: mock.fn(async () => { + throw new Error('Load failed'); + }), + }; + + // Even if load fails, it should continue with empty items + MockWorker.onPostMessage = (worker, data) => { + assert.deepStrictEqual(data.payload.items, []); + worker.emitMessage({ id: data.id, result: new Map() }); + }; + + const result = await buildWatchHistoryTagCounts({ watchHistoryService }); + + assert.ok(result instanceof Map); + assert.strictEqual(result.size, 0); + assert.strictEqual(watchHistoryService.loadLatest.mock.callCount(), 1); + + // Verify warning was logged + const newCalls = consoleWarnMock.mock.calls.slice(initialCallCount); + const warning = newCalls.find(c => c.arguments[0] && String(c.arguments[0]).includes('Failed to load watch history entries')); + assert.ok(warning, 'Should log warning about load failure'); + }); + + it('should return counts from worker on success', async () => { + const watchHistoryService = { + loadLatest: mock.fn(async () => ['item1']), + }; + const nostrService = { + getVideosMap: mock.fn(() => new Map([['v1', { tags: ['t1'] }]])), + }; + + MockWorker.onPostMessage = (worker, data) => { + assert.strictEqual(data.type, 'CALC_HISTORY_COUNTS'); + assert.deepStrictEqual(data.payload.items, ['item1']); + + // Verify worker receives correct payload structure + assert.ok(data.payload.videosMap instanceof Map); + + // Simulate successful result + worker.emitMessage({ + id: data.id, + result: new Map([['t1', 5]]) + }); + }; + + const result = await buildWatchHistoryTagCounts({ + watchHistoryService, + nostrService, + actor: 'user1' + }); + + assert.strictEqual(result.get('t1'), 5); + }); + + it('should handle worker error gracefully', async () => { + const watchHistoryService = { + loadLatest: mock.fn(async () => ['item1']), + }; + + MockWorker.onPostMessage = (worker, data) => { + worker.emitMessage({ + id: data.id, + error: 'Worker exploded' + }); + }; + + const result = await buildWatchHistoryTagCounts({ watchHistoryService }); + assert.ok(result instanceof Map); + assert.strictEqual(result.size, 0); + + const newCalls = consoleWarnMock.mock.calls.slice(initialCallCount); + const warning = newCalls.find(c => c.arguments[0] && String(c.arguments[0]).includes('Worker failed to calculate history counts')); + assert.ok(warning, 'Should log warning about worker failure'); + }); +});