const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); function app() { const nodes = new Map(); const node = selector => { if (!nodes.has(selector)) nodes.set(selector, { value: '', textContent: '', innerHTML: '', disabled: false, classList: { add() {}, remove() {}, toggle() {} }, setAttribute() {}, addEventListener() {}, setCustomValidity() {}, reportValidity() {}, showModal() { this.open = true; }, close() { this.open = false; }, }); return nodes.get(selector); }; const pending = () => new Promise(() => {}); const context = vm.createContext({ document: { querySelector: node, querySelectorAll: () => [], addEventListener() {}, documentElement: { dataset: {} } }, localStorage: { getItem: () => null }, WeApi: { status: pending, shelf: pending, dashboard: pending, goals: pending }, crypto: { randomUUID: () => 'test-goal' }, setTimeout() {}, clearTimeout() {}, console, }); vm.runInContext(fs.readFileSync('static/js/app.js', 'utf8'), context); return { context, node, run: code => vm.runInContext(code, context) }; } test('目标书籍不在当前书架时仍保留总数,并可打开和编辑', () => { const { run, node } = app(); run(`loadState.goals = 'ready'; books = [{id:'known',title:'已读书',status:'done'}]; goals = [{id:'g',name:'阅读',start:'2026-09-01',end:'2026-09-30',ids:['known','missing'],sources:[]}]; shelfData = {};`); assert.equal(run('goalMetrics(goals[0]).total'), 2); assert.equal(run('goalMetrics(goals[0]).percent'), 50); run("openGoalDetail('g'); openGoalEditor('g');"); assert.match(node('#goal-picker').innerHTML, /状态待同步/); assert.equal(run("selectedGoalBooks().includes('missing')"), true); }); test('数据库保存失败时保留原目标和编辑草稿,允许重试', async () => { const { context, run, node } = app(); run(`loadState.goals = 'ready'; shelfData = {}; books = [{id:'known',title:'一本书',status:'done'}]; goals = [{id:'g',name:'原名称',start:'2026-09-01',end:'2026-09-30',ids:['known'],sources:[]}]; openGoalEditor('g');`); node('#goal-name').value = '新名称'; context.WeApi.saveGoal = async () => { throw new Error('数据库暂不可用'); }; await run('saveGoal({preventDefault(){}})'); assert.equal(run('goals[0].name'), '原名称'); assert.equal(node('#goal-editor').open, true); assert.equal(node('#goal-name').value, '新名称'); assert.match(node('#goal-error').textContent, /数据库暂不可用/); context.WeApi.saveGoal = async goal => ({ goal }); await run('saveGoal({preventDefault(){}})'); assert.equal(run('goals[0].name'), '新名称'); assert.equal(node('#goal-editor').open, false); }); test('目标列表未完成加载时不打开编辑器', () => { const { run, node } = app(); run(`page = 'overview'; loadState.goals = 'loading'; shelfData = {}; books = [{id:'known',title:'一本书',status:'done'}]; openGoalEditor(null, ['known'], []);`); assert.notEqual(node('#goal-editor').open, true); assert.equal(run('page'), 'goals'); assert.match(node('#page-content').innerHTML, /正在读取阅读目标/); });