import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import vm from 'node:vm' import test from 'node:test' const testDirectory = dirname(fileURLToPath(import.meta.url)) const script = readFileSync(resolve(testDirectory, '../background/service-worker.js'), 'utf8') function createRuntime(initialStorage = {}) { const storage = { ...initialStorage } const createdTabs = [] let nextTabId = 900 const event = () => ({ addListener() {} }) const chrome = { runtime: { getManifest: () => ({ version: '1.1.17' }), onInstalled: event(), onStartup: event(), onMessage: event(), sendMessage: async () => undefined }, alarms: { onAlarm: event(), create: async () => undefined, clear: async () => true }, sidePanel: { setPanelBehavior: async () => undefined }, storage: { local: { get: async key => { if (key == null) return { ...storage } if (Array.isArray(key)) return Object.fromEntries(key.filter(item => item in storage).map(item => [item, storage[item]])) return key in storage ? { [key]: storage[key] } : {} }, set: async values => Object.assign(storage, values), remove: async keys => { for (const key of Array.isArray(keys) ? keys : [keys]) delete storage[key] } } }, tabs: { onUpdated: event(), onRemoved: event(), create: async options => { const tab = { id: nextTabId++, windowId: 12, ...options } createdTabs.push(tab) return tab }, get: async () => { throw new Error('tab not found') }, update: async (id, options) => ({ id, windowId: 12, ...options }), query: async () => [], sendMessage: async () => undefined }, windows: { update: async () => undefined }, scripting: { executeScript: async () => [] } } const context = { chrome, console, URL, setTimeout, clearTimeout, fetch: async () => ({ ok: true, json: async () => ({ code: 200 }) }) } vm.createContext(context) vm.runInContext(script, context) return { context, storage, createdTabs } } function task(taskId, platform, createdAt, status = 'FAILED') { return { taskId, token: 'a'.repeat(32), apiBaseUrl: 'https://feast.yidaima.cn/prod-api', platform, task: { title: platform + ' title' }, status, statusMessage: '测试状态', createdAt, platformTabId: null } } test('side panel lists only the latest task for each platform', async () => { const now = Date.now() const firstId = 'publish_' + '1'.repeat(32) const latestId = 'publish_' + '2'.repeat(32) const douyinId = 'publish_' + '3'.repeat(32) const runtime = createRuntime({ ['publisher_task_' + firstId]: task(firstId, 'xiaohongshu', now - 2000), ['publisher_task_' + latestId]: task(latestId, 'xiaohongshu', now - 1000), ['publisher_task_' + douyinId]: task(douyinId, 'douyin', now) }) const tasks = await runtime.context.listSidePanelTasks() assert.equal(tasks.length, 2) assert.equal(tasks.find(item => item.platform === 'xiaohongshu').taskId, latestId) assert.equal(tasks.find(item => item.platform === 'douyin').taskId, douyinId) }) test('retry reopens a closed platform tab and stores the new mapping', async () => { const taskId = 'publish_' + '4'.repeat(32) const runtime = createRuntime({ ['publisher_task_' + taskId]: task(taskId, 'xianyu', Date.now()) }) const result = await runtime.context.retryTask(taskId) assert.equal(result.tabId, 900) assert.equal(runtime.createdTabs[0].url, 'https://www.goofish.pro/sale/product/add?from=%2Fon-sale') assert.equal(runtime.storage['publisher_task_' + taskId].status, 'OPENING') assert.equal(runtime.storage.publisher_tab_900, taskId) }) test('retry rejects a successfully published task', async () => { const taskId = 'publish_' + '5'.repeat(32) const runtime = createRuntime({ ['publisher_task_' + taskId]: task(taskId, 'bilibili', Date.now(), 'SUCCEEDED') }) await assert.rejects(() => runtime.context.retryTask(taskId), /不能重试/) })