| const test = require('node:test'); |
| const assert = require('node:assert/strict'); |
| const fs = require('node:fs'); |
| const path = require('node:path'); |
| const vm = require('node:vm'); |
|
|
| const uiSource = fs.readFileSync(path.join(__dirname, 'ui.js'), 'utf8'); |
|
|
| function extractCommonUiHelpers(source) { |
| const startMarker = '// 公共工具函数(DRY原则:消除重复代码)'; |
| const endMarker = '// 通用可搜索下拉选择框组件 (SearchableCombobox)'; |
| const start = source.indexOf(startMarker); |
| const end = source.indexOf(endMarker); |
| assert.notEqual(start, -1, '找不到 ui.js 公共工具函数区块起点'); |
| assert.notEqual(end, -1, '找不到 ui.js 公共工具函数区块终点'); |
| return source.slice(start, end); |
| } |
|
|
| function makeMemoryStorage() { |
| const map = new Map(); |
| return { |
| getItem(k) { return map.has(k) ? map.get(k) : null; }, |
| setItem(k, v) { map.set(k, String(v)); }, |
| removeItem(k) { map.delete(k); }, |
| clear() { map.clear(); } |
| }; |
| } |
|
|
| function loadAutoRefresh(opts = {}) { |
| const settingsValue = opts.settingsValue ?? '0'; |
| const settingsError = opts.settingsError === true; |
|
|
| const listeners = new Map(); |
| const intervals = []; |
| let nextIntervalId = 1; |
| let nextTimeoutId = 1; |
|
|
| const state = { |
| hidden: Boolean(opts.initialHidden), |
| modal: null |
| }; |
|
|
| const document = { |
| body: { dataset: {} }, |
| readyState: 'complete', |
| addEventListener(type, handler) { |
| listeners.set(type, handler); |
| }, |
| removeEventListener(type, handler) { |
| if (listeners.get(type) === handler) listeners.delete(type); |
| }, |
| querySelector(selector) { |
| if (selector === '.modal.show') return state.modal; |
| return null; |
| } |
| }; |
| Object.defineProperty(document, 'hidden', { |
| get: () => state.hidden, |
| configurable: true |
| }); |
|
|
| const fetchCalls = []; |
| const fetchDataWithAuth = async (url) => { |
| fetchCalls.push(url); |
| if (settingsError) throw new Error('boom'); |
| if (url === '/admin/settings') { |
| return [{ key: 'auto_refresh_interval_seconds', value: settingsValue }]; |
| } |
| return null; |
| }; |
|
|
| const sandbox = { |
| console, |
| document, |
| window: { |
| fetchDataWithAuth, |
| sessionStorage: opts.sessionStorage ?? makeMemoryStorage() |
| }, |
| setInterval(fn, ms) { |
| const id = nextIntervalId++; |
| intervals.push({ id, fn, ms }); |
| return id; |
| }, |
| clearInterval(id) { |
| const idx = intervals.findIndex(i => i.id === id); |
| if (idx >= 0) intervals.splice(idx, 1); |
| }, |
| setTimeout(fn) { |
| |
| return nextTimeoutId++; |
| }, |
| clearTimeout() { }, |
| Date, |
| Number, |
| JSON, |
| Math, |
| Promise, |
| Array, |
| Object, |
| String, |
| Error |
| }; |
|
|
| vm.createContext(sandbox); |
| vm.runInContext(extractCommonUiHelpers(uiSource), sandbox); |
|
|
| return { |
| sandbox, |
| listeners, |
| intervals, |
| fetchCalls, |
| state, |
| createAutoRefresh: sandbox.window.createAutoRefresh |
| }; |
| } |
|
|
| test('createAutoRefresh: 配置 = 0 时不启动 interval, 不注册 visibilitychange', async () => { |
| const env = loadAutoRefresh({ settingsValue: '0' }); |
| let loadCalls = 0; |
| const ar = env.createAutoRefresh({ load: () => { loadCalls++; } }); |
|
|
| await ar.init(); |
|
|
| assert.equal(env.intervals.length, 0, '不应注册 setInterval'); |
| assert.equal(env.listeners.has('visibilitychange'), false, '不应注册 visibilitychange'); |
| assert.equal(loadCalls, 0); |
| }); |
|
|
| test('createAutoRefresh: 配置 > 0 启动 interval, 触发时调用 load', async () => { |
| const env = loadAutoRefresh({ settingsValue: '30' }); |
| let loadCalls = 0; |
| const ar = env.createAutoRefresh({ load: () => { loadCalls++; } }); |
|
|
| await ar.init(); |
|
|
| assert.equal(env.intervals.length, 1, '应注册 1 个 setInterval'); |
| assert.equal(env.intervals[0].ms, 30000, '间隔应为 30000ms'); |
| assert.equal(env.listeners.has('visibilitychange'), true); |
|
|
| |
| env.intervals[0].fn(); |
| assert.equal(loadCalls, 1); |
|
|
| env.intervals[0].fn(); |
| assert.equal(loadCalls, 2); |
| }); |
|
|
| test('createAutoRefresh: 配置 > 0 但 document.hidden=true 时跳过 load', async () => { |
| const env = loadAutoRefresh({ settingsValue: '60' }); |
| let loadCalls = 0; |
| const ar = env.createAutoRefresh({ load: () => { loadCalls++; } }); |
|
|
| await ar.init(); |
| assert.equal(env.intervals.length, 1); |
|
|
| env.state.hidden = true; |
| env.intervals[0].fn(); |
| assert.equal(loadCalls, 0, 'hidden 状态下不应调用 load'); |
|
|
| env.state.hidden = false; |
| env.intervals[0].fn(); |
| assert.equal(loadCalls, 1); |
| }); |
|
|
| test('createAutoRefresh: .modal.show 存在时跳过 load (保护未保存内容)', async () => { |
| const env = loadAutoRefresh({ settingsValue: '30' }); |
| let loadCalls = 0; |
| const ar = env.createAutoRefresh({ load: () => { loadCalls++; } }); |
|
|
| await ar.init(); |
|
|
| env.state.modal = { id: 'editModal' }; |
| env.intervals[0].fn(); |
| assert.equal(loadCalls, 0, '存在打开的对话框时应跳过'); |
|
|
| env.state.modal = null; |
| env.intervals[0].fn(); |
| assert.equal(loadCalls, 1); |
| }); |
|
|
| test('createAutoRefresh: visibilitychange 隐藏时停止 interval, 恢复时立即 load 并重启', async () => { |
| const env = loadAutoRefresh({ settingsValue: '45' }); |
| let loadCalls = 0; |
| const ar = env.createAutoRefresh({ load: () => { loadCalls++; } }); |
|
|
| await ar.init(); |
| assert.equal(env.intervals.length, 1); |
| const handler = env.listeners.get('visibilitychange'); |
| assert.equal(typeof handler, 'function'); |
|
|
| |
| env.state.hidden = true; |
| handler(); |
| assert.equal(env.intervals.length, 0, '隐藏时应清除 interval'); |
| assert.equal(loadCalls, 0); |
|
|
| |
| env.state.hidden = false; |
| handler(); |
| assert.equal(loadCalls, 1, '恢复时应立即触发 load'); |
| assert.equal(env.intervals.length, 1, '恢复时应重启 interval'); |
| assert.equal(env.intervals[0].ms, 45000); |
| }); |
|
|
| test('createAutoRefresh: 拉取设置失败时不启动', async () => { |
| const env = loadAutoRefresh({ settingsError: true }); |
| let loadCalls = 0; |
| const ar = env.createAutoRefresh({ load: () => { loadCalls++; } }); |
|
|
| await ar.init(); |
| assert.equal(env.intervals.length, 0); |
| assert.equal(env.listeners.has('visibilitychange'), false); |
| assert.equal(loadCalls, 0); |
| }); |
|
|
| test('createAutoRefresh: 缺少 load 回调时返回空 noop', async () => { |
| const env = loadAutoRefresh({ settingsValue: '30' }); |
| const ar = env.createAutoRefresh({}); |
| await ar.init(); |
| assert.equal(env.intervals.length, 0); |
| assert.equal(env.fetchCalls.length, 0, '无 load 时不应调用接口'); |
| }); |
|
|
| test('createAutoRefresh: stop() 清理 interval 与 visibilitychange 监听', async () => { |
| const env = loadAutoRefresh({ settingsValue: '30' }); |
| const ar = env.createAutoRefresh({ load: () => {} }); |
|
|
| await ar.init(); |
| assert.equal(env.intervals.length, 1); |
| assert.equal(env.listeners.has('visibilitychange'), true); |
|
|
| ar.stop(); |
| assert.equal(env.intervals.length, 0); |
| assert.equal(env.listeners.has('visibilitychange'), false); |
| }); |
|
|
| test('createAutoRefresh: sessionStorage 60s 缓存命中时不重复请求', async () => { |
| const sharedStorage = makeMemoryStorage(); |
| |
| { |
| const env = loadAutoRefresh({ settingsValue: '30', sessionStorage: sharedStorage }); |
| const ar = env.createAutoRefresh({ load: () => {} }); |
| await ar.init(); |
| assert.equal(env.fetchCalls.length, 1); |
| } |
| |
| { |
| const env = loadAutoRefresh({ settingsValue: '999', sessionStorage: sharedStorage }); |
| const ar = env.createAutoRefresh({ load: () => {} }); |
| await ar.init(); |
| assert.equal(env.fetchCalls.length, 0, '缓存应阻止再次请求'); |
| assert.equal(env.intervals.length, 1); |
| assert.equal(env.intervals[0].ms, 30000, '应使用缓存的 30s 而非新值 999s'); |
| } |
| }); |
|
|