Files
wordpress-theme-aksrt/tests/widget-system/user-auth-admin-runtime.test.cjs
T
kate522andClaude Fable 5 bc26d62602 feat(admin): redesign identity review with dialogs
Replace inline identity review forms with focused detail and review dialogs, improve list filters and caching, and align evidence permissions and deletion feedback. Add contract and runtime regression coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-30 20:05:21 +08:00

675 lines
32 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const runtimeSource = fs.readFileSync(path.join(root, 'assets', 'js', 'admin-user-auth.js'), 'utf8');
class FakeEvent {
constructor(type, options = {}) {
this.type = type;
this.bubbles = options.bubbles !== false;
this.key = options.key || '';
this.clientX = options.clientX || 0;
this.clientY = options.clientY || 0;
this.defaultPrevented = false;
this.cancelBubble = false;
this.target = options.target || null;
this.currentTarget = null;
}
preventDefault() { this.defaultPrevented = true; }
stopPropagation() { this.cancelBubble = true; }
}
class FakeClassList {
constructor(element) { this.element = element; }
values() { return new Set((this.element.className || '').split(/\s+/).filter(Boolean)); }
add(...names) {
const values = this.values();
names.forEach((name) => values.add(name));
this.element.className = [...values].join(' ');
}
remove(...names) {
const values = this.values();
names.forEach((name) => values.delete(name));
this.element.className = [...values].join(' ');
}
contains(name) { return this.values().has(name); }
toggle(name, force) {
const present = this.contains(name);
const next = force === undefined ? !present : Boolean(force);
if (next) this.add(name); else this.remove(name);
return next;
}
}
function matchesCompound(element, selector) {
if (!element || element.nodeType !== 1) return false;
const tag = selector.match(/^[a-zA-Z][\w-]*/);
if (tag && element.tagName.toLowerCase() !== tag[0].toLowerCase()) return false;
const classes = selector.match(/\.[\w-]+/g) || [];
if (classes.some((name) => !element.classList.contains(name.slice(1)))) return false;
const attributes = selector.match(/\[[^\]]+\]/g) || [];
for (const token of attributes) {
const match = token.slice(1, -1).match(/^([\w-]+)(?:\s*=\s*["']?([^"']*)["']?)?$/);
if (!match || !element.hasAttribute(match[1])) return false;
if (match[2] !== undefined && element.getAttribute(match[1]) !== match[2]) return false;
}
return Boolean(tag || classes.length || attributes.length);
}
function matchesSelector(element, selector) {
return selector.split(',').some((part) => {
const chain = part.trim().split(/\s+/).filter(Boolean);
if (!chain.length || !matchesCompound(element, chain[chain.length - 1])) return false;
let ancestor = element.parentElement;
for (let index = chain.length - 2; index >= 0; index -= 1) {
while (ancestor && !matchesCompound(ancestor, chain[index])) ancestor = ancestor.parentElement;
if (!ancestor) return false;
ancestor = ancestor.parentElement;
}
return true;
});
}
class FakeElement {
constructor(tagName = 'div', ownerDocument = null) {
this.nodeType = tagName === '#document-fragment' ? 11 : 1;
this.tagName = tagName.toUpperCase();
this.ownerDocument = ownerDocument;
this.parentNode = null;
this._children = [];
this._text = '';
this.attributes = Object.create(null);
this.dataset = Object.create(null);
this.listeners = Object.create(null);
this.className = '';
this.classList = new FakeClassList(this);
this.value = '';
this.checked = false;
this.disabled = false;
this.hidden = false;
this.required = false;
this.type = '';
this.name = '';
this.open = false;
this.reportValidityCalls = 0;
this.customValidity = '';
this.content = null;
}
get children() { return this._children; }
get parentElement() { return this.parentNode && this.parentNode.nodeType === 1 ? this.parentNode : null; }
get isConnected() {
let node = this;
while (node) {
if (this.ownerDocument && node === this.ownerDocument.documentElement) return true;
node = node.parentNode;
}
return false;
}
get textContent() { return this._text + this._children.map((child) => child.textContent).join(''); }
set textContent(value) {
this.replaceChildren();
this._text = String(value);
}
appendChild(child) {
if (child.nodeType === 11) {
child._children.slice().forEach((item) => this.appendChild(item));
child.replaceChildren();
return child;
}
if (child.parentNode) child.parentNode.removeChild(child);
this._children.push(child);
child.parentNode = this;
return child;
}
removeChild(child) {
const index = this._children.indexOf(child);
if (index !== -1) {
this._children.splice(index, 1);
child.parentNode = null;
}
return child;
}
replaceChildren(...children) {
this._children.slice().forEach((child) => { child.parentNode = null; });
this._children = [];
this._text = '';
children.forEach((child) => { if (child) this.appendChild(child); });
}
setAttribute(name, value) {
const stringValue = String(value);
this.attributes[name] = stringValue;
if (name === 'class') this.className = stringValue;
if (name === 'value') this.value = stringValue;
if (name === 'name') this.name = stringValue;
if (name === 'type') this.type = stringValue;
if (name === 'disabled') this.disabled = true;
if (name === 'hidden') this.hidden = true;
if (name === 'required') this.required = true;
if (name.indexOf('data-') === 0) {
const key = name.slice(5).replace(/-([a-z])/g, (_all, letter) => letter.toUpperCase());
this.dataset[key] = stringValue;
}
}
getAttribute(name) { return Object.prototype.hasOwnProperty.call(this.attributes, name) ? this.attributes[name] : null; }
hasAttribute(name) { return Object.prototype.hasOwnProperty.call(this.attributes, name); }
removeAttribute(name) {
delete this.attributes[name];
if (name === 'disabled') this.disabled = false;
if (name === 'hidden') this.hidden = false;
if (name === 'required') this.required = false;
}
addEventListener(type, callback) {
if (!this.listeners[type]) this.listeners[type] = [];
this.listeners[type].push(callback);
}
dispatchEvent(event) {
if (!event.target) event.target = this;
let current = this;
while (current) {
event.currentTarget = current;
(current.listeners[event.type] || []).slice().forEach((callback) => callback.call(current, event));
if (!event.bubbles || event.cancelBubble) break;
current = current.parentElement;
}
return !event.defaultPrevented;
}
querySelectorAll(selector) {
const found = [];
const visit = (node) => {
node._children.forEach((child) => {
if (matchesSelector(child, selector)) found.push(child);
visit(child);
});
if (node.tagName === 'TEMPLATE' && node.content) visit(node.content);
};
visit(this);
return found;
}
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
focus() {
if (this.ownerDocument) this.ownerDocument.activeElement = this;
this.focused = true;
}
click() { return this.dispatchEvent(new FakeEvent('click')); }
setCustomValidity(message) { this.customValidity = String(message); }
reportValidity() {
this.reportValidityCalls += 1;
const controls = this.querySelectorAll('input, textarea, select');
const enabled = controls.filter((field) => {
let parent = field.parentElement;
while (parent) {
if (parent.tagName === 'FIELDSET' && parent.disabled) return false;
parent = parent.parentElement;
}
return !field.disabled;
});
const requiredRadioNames = new Set(enabled.filter((field) => field.type === 'radio' && field.required).map((field) => field.name));
for (const name of requiredRadioNames) {
if (!enabled.some((field) => field.type === 'radio' && field.name === name && field.checked)) return false;
}
for (const field of enabled) {
if (field.customValidity) return false;
if (field.required && field.type !== 'radio' && field.value === '') return false;
}
return true;
}
cloneNode(deep = false) {
const clone = new FakeElement(this.tagName.toLowerCase(), this.ownerDocument);
Object.entries(this.attributes).forEach(([name, value]) => clone.setAttribute(name, value));
clone.value = this.value;
clone.checked = this.checked;
clone.disabled = this.disabled;
clone.hidden = this.hidden;
clone.required = this.required;
clone.type = this.type;
clone.name = this.name;
clone.customValidity = this.customValidity;
clone._text = this._text;
clone.dataset = Object.assign(Object.create(null), this.dataset);
if (this.content) clone.content = this.content.cloneNode(true);
if (deep) this._children.forEach((child) => clone.appendChild(child.cloneNode(true)));
return clone;
}
getBoundingClientRect() { return { left: 100, top: 100, right: 600, bottom: 700 }; }
showModal() { this.open = true; }
close() {
if (!this.open) return;
this.open = false;
this.dispatchEvent(new FakeEvent('close', { bubbles: false }));
}
}
class FakeDocument {
constructor() {
this.documentElement = new FakeElement('html', this);
this.body = new FakeElement('body', this);
this.documentElement.appendChild(this.body);
this.activeElement = null;
}
querySelector(selector) { return this.documentElement.querySelector(selector); }
getElementById(id) {
const visit = (node) => {
if (node.getAttribute && node.getAttribute('id') === id) return node;
for (const child of node.children || []) {
const match = visit(child);
if (match) return match;
}
if (node.tagName === 'TEMPLATE' && node.content) return visit(node.content);
return null;
};
return visit(this.documentElement);
}
}
function makeElement(document, tag, attributes = {}, text = '') {
const element = new FakeElement(tag, document);
Object.entries(attributes).forEach(([name, value]) => element.setAttribute(name, value));
if (text !== '') element.textContent = text;
return element;
}
function addTemplate(rootElement, document, id, child) {
const template = makeElement(document, 'template', { id });
template.content = new FakeElement('#document-fragment', document);
template.content.appendChild(child);
rootElement.appendChild(template);
return template;
}
function makeCloseButton(document) {
return makeElement(document, 'button', { type: 'button', 'data-aksrt-dialog-close': '' }, '关闭');
}
function makeDetailFrame(document, text = '<img src=x onerror=alert(1)>') {
const form = makeElement(document, 'form', { method: 'post', class: 'aksrt-verify-dialog__frame', 'data-aksrt-verify-review': '' });
const head = makeElement(document, 'header', { class: 'aksrt-verify-dialog__head' });
head.appendChild(makeElement(document, 'h2', { id: 'aksrt-verify-dialog-title' }, '认证资料审核'));
head.appendChild(makeElement(document, 'p', { id: 'aksrt-verify-dialog-description' }, '核对申请人与所提交的材料。'));
head.appendChild(makeCloseButton(document));
form.appendChild(head);
const body = makeElement(document, 'div', { class: 'aksrt-verify-dialog__body' });
body.appendChild(makeElement(document, 'div', { class: 'aksrt-verify-applicant' }, '申请人 17'));
const meta = makeElement(document, 'dl', { class: 'aksrt-verify-meta' });
meta.appendChild(makeElement(document, 'div', {}, '用户资料'));
body.appendChild(meta);
const section = makeElement(document, 'section', { class: 'aksrt-verify-section' });
section.appendChild(makeElement(document, 'h3', {}, '申请说明'));
section.appendChild(makeElement(document, 'div', { class: 'aksrt-verify-copy' }, text));
body.appendChild(section);
const evidence = makeElement(document, 'ul', { class: 'aksrt-verify-evidence' });
const evidenceItem = makeElement(document, 'li');
const evidenceLink = makeElement(document, 'a', { href: 'https://example.test/evidence.jpg' });
evidenceLink.appendChild(makeElement(document, 'img', { src: 'https://example.test/thumb.jpg', alt: '证件材料' }));
evidenceLink.appendChild(makeElement(document, 'span', {}, '证件材料'));
evidenceItem.appendChild(evidenceLink);
evidence.appendChild(evidenceItem);
body.appendChild(evidence);
const review = makeElement(document, 'section', { class: 'aksrt-verify-review' });
const decisions = makeElement(document, 'fieldset', { class: 'aksrt-verify-decisions' });
decisions.appendChild(makeElement(document, 'legend', {}, '审核结果'));
for (const [value, label] of [['verified', '通过'], ['rejected', '拒绝']]) {
const choice = makeElement(document, 'label');
const radio = makeElement(document, 'input', { type: 'radio', name: 'status', value, required: '' });
choice.appendChild(radio);
choice.appendChild(makeElement(document, 'span', {}, label));
decisions.appendChild(choice);
}
review.appendChild(decisions);
const profile = makeElement(document, 'fieldset', { class: 'aksrt-verify-profile', 'data-aksrt-verify-profile': '', hidden: '', disabled: '' });
const nameField = makeElement(document, 'label', { class: 'aksrt-native-field' });
nameField.appendChild(makeElement(document, 'span', {}, '认证名称'));
nameField.appendChild(makeElement(document, 'input', { type: 'text', name: 'name', maxlength: '64', required: '', value: 'Existing profile name', 'data-required-error': '请填写认证名称' }));
profile.appendChild(nameField);
const descriptionField = makeElement(document, 'label', { class: 'aksrt-native-field' });
descriptionField.appendChild(makeElement(document, 'span', {}, '认证说明'));
const description = makeElement(document, 'textarea', { name: 'description' });
description.value = 'Existing profile description';
descriptionField.appendChild(description);
profile.appendChild(descriptionField);
review.appendChild(profile);
const noteLabel = makeElement(document, 'label', { class: 'aksrt-native-field' });
noteLabel.appendChild(makeElement(document, 'span', { 'data-aksrt-verify-note-label': '', 'data-optional': '处理留言(选填)', 'data-required': '拒绝原因(必填)' }, '处理留言(选填)'));
noteLabel.appendChild(makeElement(document, 'textarea', { name: 'note', 'data-required-error': '请填写拒绝原因' }));
review.appendChild(noteLabel);
body.appendChild(review);
form.appendChild(body);
const foot = makeElement(document, 'footer', { class: 'aksrt-verify-dialog__foot' });
foot.appendChild(makeElement(document, 'button', { type: 'submit', class: 'button button-primary', 'data-aksrt-verify-submit': '', 'data-idle-label': '请选择审核结果', 'data-approve-label': '确认通过', 'data-reject-label': '确认拒绝', 'data-busy-label': '正在提交…', disabled: '' }, '请选择审核结果'));
form.appendChild(foot);
form.appendChild(makeElement(document, 'input', { type: 'hidden', name: '_wpnonce', value: 'nonce-17' }));
form.appendChild(makeElement(document, 'input', { type: 'hidden', name: 'application_uuid', value: 'uuid-17' }));
form.appendChild(makeElement(document, 'input', { type: 'hidden', name: 'user_id', value: '17' }));
return form;
}
function makeDeleteFrame(document) {
const form = makeElement(document, 'form', { method: 'post', class: 'aksrt-verify-dialog__frame', 'data-aksrt-verify-delete-form': '' });
const head = makeElement(document, 'header', { class: 'aksrt-verify-dialog__head' });
head.appendChild(makeElement(document, 'h2', { id: 'aksrt-verify-dialog-title' }, '删除认证记录'));
head.appendChild(makeElement(document, 'p', { id: 'aksrt-verify-dialog-description' }, '请确认后再继续。'));
head.appendChild(makeCloseButton(document));
form.appendChild(head);
const body = makeElement(document, 'div', { class: 'aksrt-verify-dialog__body' });
body.appendChild(makeElement(document, 'div', { class: 'aksrt-native-callout is-danger' }, '删除不可撤销,只删除记录、材料与事件,不撤销已完成的认证。'));
form.appendChild(body);
const foot = makeElement(document, 'footer', { class: 'aksrt-verify-dialog__foot' });
foot.appendChild(makeElement(document, 'button', { type: 'submit', 'data-aksrt-verify-submit': '', 'data-idle-label': '确认删除', 'data-busy-label': '正在提交…' }, '确认删除'));
form.appendChild(foot);
form.appendChild(makeElement(document, 'input', { type: 'hidden', name: '_wpnonce', value: 'delete-nonce-17' }));
form.appendChild(makeElement(document, 'input', { type: 'hidden', name: 'application_uuid', value: 'delete-uuid-17' }));
return form;
}
function makeReadonlyFrame(document) {
const frame = makeElement(document, 'div', { class: 'aksrt-verify-dialog__frame' });
const head = makeElement(document, 'header', { class: 'aksrt-verify-dialog__head' });
head.appendChild(makeElement(document, 'h2', { id: 'aksrt-verify-dialog-title' }, '认证记录详情'));
head.appendChild(makeElement(document, 'p', { id: 'aksrt-verify-dialog-description' }, '此记录仅供查看。'));
head.appendChild(makeCloseButton(document));
frame.appendChild(head);
frame.appendChild(makeElement(document, 'div', { class: 'aksrt-verify-dialog__body' }, '只读内容'));
frame.appendChild(makeElement(document, 'footer', { class: 'aksrt-verify-dialog__foot' }));
return frame;
}
function setup() {
const document = new FakeDocument();
const rootElement = makeElement(document, 'main', { class: 'aksrt-verify-page' });
const view17 = makeElement(document, 'button', { type: 'button', 'data-aksrt-verify-view': '17', 'aria-controls': 'aksrt-verify-dialog' }, '查看审核');
const view18 = makeElement(document, 'button', { type: 'button', 'data-aksrt-verify-view': '18', 'aria-controls': 'aksrt-verify-dialog' }, '查看只读记录');
const delete17 = makeElement(document, 'button', { type: 'button', 'data-aksrt-verify-delete': '17', 'aria-controls': 'aksrt-verify-dialog' }, '删除');
rootElement.appendChild(view17);
rootElement.appendChild(view18);
rootElement.appendChild(delete17);
const detailTemplate = addTemplate(rootElement, document, 'aksrt-verify-detail-17', makeDetailFrame(document));
const readonlyTemplate = addTemplate(rootElement, document, 'aksrt-verify-detail-18', makeReadonlyFrame(document));
const deleteTemplate = addTemplate(rootElement, document, 'aksrt-verify-delete-17', makeDeleteFrame(document));
const dialog = makeElement(document, 'dialog', {
id: 'aksrt-verify-dialog',
class: 'aksrt-verify-dialog',
'aria-labelledby': 'aksrt-verify-dialog-title',
'aria-describedby': 'aksrt-verify-dialog-description',
});
document.body.appendChild(rootElement);
document.body.appendChild(dialog);
const windowListeners = Object.create(null);
const window = {
addEventListener(type, callback) {
if (!windowListeners[type]) windowListeners[type] = [];
windowListeners[type].push(callback);
},
dispatchEvent(event) {
(windowListeners[event.type] || []).forEach((callback) => callback(event));
},
};
vm.runInNewContext(runtimeSource, { document, window });
return { document, root: rootElement, dialog, view17, view18, delete17, detailTemplate, readonlyTemplate, deleteTemplate, window };
}
function open(button) { button.click(); }
function submit(form) {
const event = new FakeEvent('submit');
form.dispatchEvent(event);
return event;
}
function fire(element, type, options = {}) { return element.dispatchEvent(new FakeEvent(type, options)); }
function reviewParts(form) {
return {
radios: form.querySelectorAll('input[name="status"]'),
profile: form.querySelector('[data-aksrt-verify-profile]'),
name: form.querySelector('input[name="name"]'),
description: form.querySelector('textarea[name="description"]'),
note: form.querySelector('textarea[name="note"]'),
noteLabel: form.querySelector('[data-aksrt-verify-note-label]'),
submit: form.querySelector('[data-aksrt-verify-submit]'),
};
}
test('details and deletion clone only their own inert templates; no markup parsing or stale review state', () => {
const app = setup();
assert.equal(app.dialog.children.length, 0, 'the shared dialog starts empty');
const sourceForm = app.detailTemplate.content.querySelector('form[data-aksrt-verify-review]');
open(app.view17);
const firstForm = app.dialog.querySelector('form[data-aksrt-verify-review]');
assert.ok(firstForm);
assert.notEqual(firstForm, sourceForm, 'dialog content is a deep clone, not the inert template node');
assert.equal(app.dialog.querySelectorAll('script').length, 0);
assert.match(app.dialog.querySelector('.aksrt-verify-copy').textContent, /<img src=x onerror=alert\(1\)>/);
assert.equal(app.dialog.querySelector('.aksrt-verify-copy').querySelector('img'), null, 'untrusted copy stays text');
const parts = reviewParts(firstForm);
assert.equal(parts.name.value, 'Existing profile name', 'the clone keeps server-prefilled profile data');
assert.equal(parts.description.value, 'Existing profile description');
parts.radios[1].checked = true;
parts.note.value = '已核对';
fire(parts.radios[1], 'change');
assert.equal(parts.submit.textContent, '确认拒绝');
assert.equal(sourceForm.querySelector('textarea[name="note"]').value, '', 'editing the clone does not mutate the template');
app.dialog.querySelector('[data-aksrt-dialog-close]').click();
assert.equal(app.dialog.children.length, 0, 'closing removes every cloned node');
assert.equal(app.document.activeElement, app.view17, 'close restores focus to the opener');
open(app.view17);
const secondForm = app.dialog.querySelector('form[data-aksrt-verify-review]');
assert.notEqual(secondForm, firstForm, 'each open builds a fresh clone');
assert.equal(reviewParts(secondForm).radios.some((radio) => radio.checked), false);
assert.equal(reviewParts(secondForm).note.value, '', 'the previous note is not retained');
assert.equal(reviewParts(secondForm).name.value, 'Existing profile name', 'fresh clones preserve server-provided profile defaults');
app.dialog.querySelector('[data-aksrt-dialog-close]').click();
open(app.delete17);
const deleteForm = app.dialog.querySelector('form[data-aksrt-verify-delete-form]');
const sourceDeleteForm = app.deleteTemplate.content.querySelector('form[data-aksrt-verify-delete-form]');
assert.ok(deleteForm);
assert.notEqual(deleteForm, sourceDeleteForm, 'deletion actions also clone their own inert template');
assert.equal(app.dialog.querySelector('form[data-aksrt-verify-review]'), null);
assert.equal(app.dialog.querySelector('.aksrt-native-callout.is-danger').textContent.includes('不撤销已完成的认证'), true);
assert.equal(app.dialog.querySelector('[data-aksrt-verify-submit]').textContent, '确认删除');
assert.equal(app.dialog.querySelectorAll('input[name="status"]').length, 0);
});
test('cancel, Escape, and backdrop close the dialog; backdrop must begin and end outside', () => {
const app = setup();
open(app.view17);
const cancel = new FakeEvent('cancel', { bubbles: false });
app.dialog.dispatchEvent(cancel);
assert.equal(cancel.defaultPrevented, true, 'cancel is handled through the shared close path');
assert.equal(app.dialog.open, false);
assert.equal(app.document.activeElement, app.view17);
open(app.view17);
fire(app.dialog, 'keydown', { key: 'Escape' });
assert.equal(app.dialog.open, true, 'Escape is completed by the native cancel event');
fire(app.dialog, 'pointerdown', { clientX: 120, clientY: 130 });
fire(app.dialog, 'click', { clientX: 700, clientY: 800 });
assert.equal(app.dialog.open, true, 'a gesture that began inside cannot dismiss the dialog');
fire(app.dialog, 'pointerdown', { clientX: 700, clientY: 800 });
fire(app.dialog, 'click', { clientX: 120, clientY: 130 });
assert.equal(app.dialog.open, true, 'a gesture that ended inside cannot dismiss the dialog');
fire(app.dialog, 'pointerdown', { clientX: 700, clientY: 800 });
fire(app.dialog, 'click', { clientX: 700, clientY: 800 });
assert.equal(app.dialog.open, false, 'a full outside pointer gesture closes the dialog');
assert.equal(app.dialog.children.length, 0);
assert.equal(app.document.activeElement, app.view17);
});
test('review starts unselected and cannot submit before a decision', () => {
const app = setup();
open(app.view17);
const form = app.dialog.querySelector('form[data-aksrt-verify-review]');
const parts = reviewParts(form);
assert.equal(parts.radios.some((radio) => radio.checked), false);
assert.equal(parts.submit.disabled, true);
assert.equal(parts.submit.textContent, '请选择审核结果');
assert.equal(parts.profile.hidden, true);
assert.equal(parts.profile.disabled, true);
assert.equal(parts.note.required, false);
const event = submit(form);
assert.equal(event.defaultPrevented, true);
assert.equal(form.reportValidityCalls, 1, 'native validity is reported even when no decision is selected');
assert.notEqual(form.dataset.submitting, '1');
});
test('approve reveals and enables profile fields; trimmed name validation and successful hidden fields are preserved', () => {
const app = setup();
open(app.view17);
const form = app.dialog.querySelector('form[data-aksrt-verify-review]');
const parts = reviewParts(form);
parts.radios[0].checked = true;
fire(parts.radios[0], 'change');
assert.equal(parts.profile.hidden, false);
assert.equal(parts.profile.disabled, false);
assert.equal(parts.note.required, false);
assert.equal(parts.noteLabel.textContent, '处理留言(选填)');
assert.equal(parts.submit.disabled, false);
assert.equal(parts.submit.textContent, '确认通过');
parts.name.value = ' ';
fire(parts.name, 'input');
const blankName = submit(form);
assert.equal(blankName.defaultPrevented, true, 'whitespace-only names fail native reportValidity');
assert.equal(parts.name.customValidity, '请填写认证名称');
assert.equal(form.reportValidityCalls, 1);
parts.name.value = '认证名称';
parts.description.value = '认证说明';
fire(parts.name, 'input');
const accepted = submit(form);
assert.equal(accepted.defaultPrevented, false, 'valid review submits through the browser form navigation');
assert.equal(form.dataset.submitting, '1');
assert.equal(parts.submit.disabled, true);
assert.equal(parts.submit.textContent, '正在提交…');
assert.equal(parts.name.disabled, false, 'successful user fields are not disabled while submitting');
assert.equal(parts.description.disabled, false);
assert.equal(parts.note.disabled, false);
assert.equal(form.querySelector('[name="_wpnonce"]').value, 'nonce-17');
assert.equal(form.querySelector('[name="_wpnonce"]').disabled, false);
assert.equal(form.querySelector('[name="application_uuid"]').value, 'uuid-17');
assert.equal(form.querySelector('[name="application_uuid"]').disabled, false);
assert.equal(form.querySelector('[name="user_id"]').disabled, false);
parts.radios[0].checked = false;
parts.radios[1].checked = true;
fire(parts.radios[1], 'change');
parts.note.value = '提交中不应切换审核状态';
fire(parts.note, 'input');
assert.equal(parts.submit.disabled, true, 'change/input events cannot re-enable actions after submission starts');
assert.equal(parts.submit.textContent, '正在提交…', 'change/input events cannot overwrite the busy label');
assert.equal(parts.profile.hidden, false, 'a busy review does not resync profile visibility');
assert.equal(parts.note.required, false, 'a busy review does not resync required fields');
assert.equal(parts.noteLabel.textContent, '处理留言(选填)');
const repeated = submit(form);
assert.equal(repeated.defaultPrevented, true, 'second submission is stopped');
assert.equal(form.reportValidityCalls, 2, 'duplicate submits do not run additional validation or navigation');
});
test('reject hides profile fields, requires a trimmed reason, and applies danger action state', () => {
const app = setup();
open(app.view17);
const form = app.dialog.querySelector('form[data-aksrt-verify-review]');
const parts = reviewParts(form);
parts.radios[1].checked = true;
fire(parts.radios[1], 'change');
assert.equal(parts.profile.hidden, true);
assert.equal(parts.profile.disabled, true);
assert.equal(parts.note.required, true);
assert.equal(parts.noteLabel.textContent, '拒绝原因(必填)');
assert.equal(parts.submit.disabled, false);
assert.equal(parts.submit.textContent, '确认拒绝');
assert.equal(parts.submit.classList.contains('is-danger'), true);
parts.note.value = ' \n ';
fire(parts.note, 'input');
const blankReason = submit(form);
assert.equal(blankReason.defaultPrevented, true);
assert.equal(parts.note.customValidity, '请填写拒绝原因');
assert.equal(form.reportValidityCalls, 1);
parts.note.value = '材料不完整';
fire(parts.note, 'input');
const rejected = submit(form);
assert.equal(rejected.defaultPrevented, false);
assert.equal(parts.note.disabled, false);
assert.equal(form.querySelector('[name="_wpnonce"]').value, 'nonce-17');
assert.equal(form.querySelector('[name="application_uuid"]').value, 'uuid-17');
});
test('delete submits without confirm, blocks repeat/close while busy, and pageshow restores actions and scroll lock', () => {
const app = setup();
open(app.delete17);
const form = app.dialog.querySelector('form[data-aksrt-verify-delete-form]');
const submitButton = form.querySelector('[data-aksrt-verify-submit]');
const closeButton = form.querySelector('[data-aksrt-dialog-close]');
const first = submit(form);
assert.equal(first.defaultPrevented, false);
assert.equal(form.dataset.submitting, '1');
assert.equal(submitButton.disabled, true);
assert.equal(closeButton.disabled, true);
assert.equal(submitButton.textContent, '正在提交…');
assert.equal(form.querySelector('[name="_wpnonce"]').value, 'delete-nonce-17');
assert.equal(form.querySelector('[name="application_uuid"]').value, 'delete-uuid-17');
assert.equal(form.querySelector('[name="_wpnonce"]').disabled, false);
assert.equal(form.querySelector('[name="application_uuid"]').disabled, false);
assert.equal(app.document.documentElement.classList.contains('aksrt-verify-modal-open'), true);
assert.equal(submit(form).defaultPrevented, true, 'duplicate delete submission is blocked');
const escape = new FakeEvent('keydown', { key: 'Escape', bubbles: false });
app.dialog.dispatchEvent(escape);
assert.equal(escape.defaultPrevented, true);
const cancel = new FakeEvent('cancel', { bubbles: false });
app.dialog.dispatchEvent(cancel);
assert.equal(cancel.defaultPrevented, true);
assert.equal(app.dialog.open, true, 'busy submissions cannot close through keyboard');
closeButton.click();
assert.equal(app.dialog.open, true, 'busy submissions cannot close through the close action');
fire(app.dialog, 'pointerdown', { clientX: 700, clientY: 800 });
fire(app.dialog, 'click', { clientX: 700, clientY: 800 });
assert.equal(app.dialog.open, true, 'busy submissions cannot close through the backdrop');
app.window.dispatchEvent(new FakeEvent('pageshow'));
assert.equal(form.dataset.submitting, undefined);
assert.equal(submitButton.disabled, false);
assert.equal(closeButton.disabled, false);
assert.equal(submitButton.textContent, '确认删除');
assert.equal(app.dialog.open, true);
assert.equal(app.document.documentElement.classList.contains('aksrt-verify-modal-open'), true, 'an open dialog retains the scroll lock');
app.dialog.querySelector('[data-aksrt-dialog-close]').click();
assert.equal(app.document.documentElement.classList.contains('aksrt-verify-modal-open'), false);
app.window.dispatchEvent(new FakeEvent('pageshow'));
assert.equal(app.document.documentElement.classList.contains('aksrt-verify-modal-open'), false, 'closed pageshow state is unlocked');
});
test('read-only detail has no review submit behavior', () => {
const app = setup();
open(app.view18);
assert.ok(app.dialog.querySelector('.aksrt-verify-dialog__frame'));
assert.equal(app.dialog.querySelector('form'), null);
assert.equal(app.dialog.querySelector('[data-aksrt-verify-submit]'), null);
assert.match(app.dialog.textContent, /此记录仅供查看/);
app.dialog.querySelector('[data-aksrt-dialog-close]').click();
assert.equal(app.dialog.children.length, 0);
});