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 source = fs.readFileSync(path.join(__dirname, '../../static/js/support.js'), 'utf8'); function createSupportUI(){ let document; class Element { constructor(tagName = 'div'){ this.tagName = tagName.toUpperCase(); this.children = []; this.attributes = {}; this.listeners = {}; this.style = {}; this.value = ''; this.parent = null; this.className = ''; this.text = ''; } set textContent(value){ this.text = String(value); this.children = []; } get textContent(){ return this.text + this.children.map(child => child.textContent).join(''); } appendChild(child){ child.parent = this; this.children.push(child); return child; } remove(){ if(this.parent) this.parent.children = this.parent.children.filter(child => child !== this); this.parent = null; } setAttribute(name, value){ this.attributes[name] = String(value); } getAttribute(name){ return this.attributes[name] ?? null; } addEventListener(name, callback){ (this.listeners[name] ||= []).push(callback); } contains(target){ return this === target || this.children.some(child => child.contains(target)); } querySelectorAll(selector){ const tags = selector.split(',').map(value => value.trim().toUpperCase()); return this.children.flatMap(child => [ ...(tags.includes(child.tagName) ? [child] : []), ...child.querySelectorAll(selector), ]); } focus(){ document.activeElement = this; } dispatch(name, event = {}){ event.target ||= this; event.preventDefault ||= function(){ this.defaultPrevented = true; }; event.stopPropagation ||= function(){}; if(this['on' + name]) this['on' + name](event); (this.listeners[name] || []).forEach(callback => callback(event)); } click(){ this.dispatch('click'); } } document = { body: new Element('body'), activeElement: null, createElement: tagName => new Element(tagName), }; const context = vm.createContext({document, window:{}}); vm.runInContext(source, context, {filename:'support.js'}); return {ui:context.SupportUI, document}; } test('confirm resolves from the in-page dialog and restores focus', async () => { const {ui, document} = createSupportUI(); const trigger = document.createElement('button'); document.body.appendChild(trigger); trigger.focus(); const result = ui.confirm('确定清空记录?', {title:'请确认', confirmText:'清空', cancelText:'取消'}); await Promise.resolve(); const dialog = document.body.children[1].children[0]; assert.equal(dialog.getAttribute('role'), 'dialog'); assert.equal(dialog.getAttribute('aria-modal'), 'true'); assert.match(dialog.textContent, /确定清空记录?/); const [cancel, confirm] = dialog.querySelectorAll('button'); assert.equal(document.activeElement, cancel); confirm.click(); assert.equal(await result, true); assert.equal(document.body.children.length, 1); assert.equal(document.activeElement, trigger); }); test('confirm returns false when cancelled with Escape', async () => { const {ui, document} = createSupportUI(); const result = ui.confirm('是否继续?'); await Promise.resolve(); const dialog = document.body.children[0].children[0]; const event = {key:'Escape'}; dialog.dispatch('keydown', event); assert.equal(event.defaultPrevented, true); assert.equal(await result, false); assert.equal(document.body.children.length, 0); }); test('prompt returns entered text and focuses its labelled input', async () => { const {ui, document} = createSupportUI(); const result = ui.prompt('请输入运单号', {title:'查询物流', inputLabel:'运单号'}); await Promise.resolve(); const dialog = document.body.children[0].children[0]; const input = dialog.querySelectorAll('input')[0]; assert.ok(input); assert.equal(input.getAttribute('aria-label'), '运单号'); assert.equal(document.activeElement, input); input.value = 'AB123'; dialog.querySelectorAll('button')[1].click(); assert.equal(await result, 'AB123'); }); test('prompt returns null when the backdrop is clicked', async () => { const {ui, document} = createSupportUI(); const result = ui.prompt('请输入留言'); await Promise.resolve(); document.body.children[0].click(); assert.equal(await result, null); }); test('dialog keeps keyboard focus inside while tabbing', async () => { const {ui, document} = createSupportUI(); ui.confirm('确认操作?'); await Promise.resolve(); const dialog = document.body.children[0].children[0]; const [cancel, confirm] = dialog.querySelectorAll('button'); cancel.focus(); dialog.dispatch('keydown', {key:'Tab', shiftKey:true}); assert.equal(document.activeElement, confirm); dialog.dispatch('keydown', {key:'Tab'}); assert.equal(document.activeElement, cancel); });