123 lines
5.2 KiB
JavaScript
123 lines
5.2 KiB
JavaScript
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);
|
|
});
|