'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-jio-dialog-close': '' }, '关闭'); } function makeDetailFrame(document, text = '') { const form = makeElement(document, 'form', { method: 'post', class: 'jio-verify-dialog__frame', 'data-jio-verify-review': '' }); const head = makeElement(document, 'header', { class: 'jio-verify-dialog__head' }); head.appendChild(makeElement(document, 'h2', { id: 'jio-verify-dialog-title' }, '认证资料审核')); head.appendChild(makeElement(document, 'p', { id: 'jio-verify-dialog-description' }, '核对申请人与所提交的材料。')); head.appendChild(makeCloseButton(document)); form.appendChild(head); const body = makeElement(document, 'div', { class: 'jio-verify-dialog__body' }); body.appendChild(makeElement(document, 'div', { class: 'jio-verify-applicant' }, '申请人 17')); const meta = makeElement(document, 'dl', { class: 'jio-verify-meta' }); meta.appendChild(makeElement(document, 'div', {}, '用户资料')); body.appendChild(meta); const section = makeElement(document, 'section', { class: 'jio-verify-section' }); section.appendChild(makeElement(document, 'h3', {}, '申请说明')); section.appendChild(makeElement(document, 'div', { class: 'jio-verify-copy' }, text)); body.appendChild(section); const evidence = makeElement(document, 'ul', { class: 'jio-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: 'jio-verify-review' }); const decisions = makeElement(document, 'fieldset', { class: 'jio-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: 'jio-verify-profile', 'data-jio-verify-profile': '', hidden: '', disabled: '' }); const nameField = makeElement(document, 'label', { class: 'jio-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: 'jio-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: 'jio-native-field' }); noteLabel.appendChild(makeElement(document, 'span', { 'data-jio-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: 'jio-verify-dialog__foot' }); foot.appendChild(makeElement(document, 'button', { type: 'submit', class: 'button button-primary', 'data-jio-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: 'jio-verify-dialog__frame', 'data-jio-verify-delete-form': '' }); const head = makeElement(document, 'header', { class: 'jio-verify-dialog__head' }); head.appendChild(makeElement(document, 'h2', { id: 'jio-verify-dialog-title' }, '删除认证记录')); head.appendChild(makeElement(document, 'p', { id: 'jio-verify-dialog-description' }, '请确认后再继续。')); head.appendChild(makeCloseButton(document)); form.appendChild(head); const body = makeElement(document, 'div', { class: 'jio-verify-dialog__body' }); body.appendChild(makeElement(document, 'div', { class: 'jio-native-callout is-danger' }, '删除不可撤销,只删除记录、材料与事件,不撤销已完成的认证。')); form.appendChild(body); const foot = makeElement(document, 'footer', { class: 'jio-verify-dialog__foot' }); foot.appendChild(makeElement(document, 'button', { type: 'submit', 'data-jio-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: 'jio-verify-dialog__frame' }); const head = makeElement(document, 'header', { class: 'jio-verify-dialog__head' }); head.appendChild(makeElement(document, 'h2', { id: 'jio-verify-dialog-title' }, '认证记录详情')); head.appendChild(makeElement(document, 'p', { id: 'jio-verify-dialog-description' }, '此记录仅供查看。')); head.appendChild(makeCloseButton(document)); frame.appendChild(head); frame.appendChild(makeElement(document, 'div', { class: 'jio-verify-dialog__body' }, '只读内容')); frame.appendChild(makeElement(document, 'footer', { class: 'jio-verify-dialog__foot' })); return frame; } function setup() { const document = new FakeDocument(); const rootElement = makeElement(document, 'main', { class: 'jio-verify-page' }); const view17 = makeElement(document, 'button', { type: 'button', 'data-jio-verify-view': '17', 'aria-controls': 'jio-verify-dialog' }, '查看审核'); const view18 = makeElement(document, 'button', { type: 'button', 'data-jio-verify-view': '18', 'aria-controls': 'jio-verify-dialog' }, '查看只读记录'); const delete17 = makeElement(document, 'button', { type: 'button', 'data-jio-verify-delete': '17', 'aria-controls': 'jio-verify-dialog' }, '删除'); rootElement.appendChild(view17); rootElement.appendChild(view18); rootElement.appendChild(delete17); const detailTemplate = addTemplate(rootElement, document, 'jio-verify-detail-17', makeDetailFrame(document)); const readonlyTemplate = addTemplate(rootElement, document, 'jio-verify-detail-18', makeReadonlyFrame(document)); const deleteTemplate = addTemplate(rootElement, document, 'jio-verify-delete-17', makeDeleteFrame(document)); const dialog = makeElement(document, 'dialog', { id: 'jio-verify-dialog', class: 'jio-verify-dialog', 'aria-labelledby': 'jio-verify-dialog-title', 'aria-describedby': 'jio-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-jio-verify-profile]'), name: form.querySelector('input[name="name"]'), description: form.querySelector('textarea[name="description"]'), note: form.querySelector('textarea[name="note"]'), noteLabel: form.querySelector('[data-jio-verify-note-label]'), submit: form.querySelector('[data-jio-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-jio-verify-review]'); open(app.view17); const firstForm = app.dialog.querySelector('form[data-jio-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('.jio-verify-copy').textContent, //); assert.equal(app.dialog.querySelector('.jio-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-jio-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-jio-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-jio-dialog-close]').click(); open(app.delete17); const deleteForm = app.dialog.querySelector('form[data-jio-verify-delete-form]'); const sourceDeleteForm = app.deleteTemplate.content.querySelector('form[data-jio-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-jio-verify-review]'), null); assert.equal(app.dialog.querySelector('.jio-native-callout.is-danger').textContent.includes('不撤销已完成的认证'), true); assert.equal(app.dialog.querySelector('[data-jio-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-jio-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-jio-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-jio-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-jio-verify-delete-form]'); const submitButton = form.querySelector('[data-jio-verify-submit]'); const closeButton = form.querySelector('[data-jio-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('jio-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('jio-verify-modal-open'), true, 'an open dialog retains the scroll lock'); app.dialog.querySelector('[data-jio-dialog-close]').click(); assert.equal(app.document.documentElement.classList.contains('jio-verify-modal-open'), false); app.window.dispatchEvent(new FakeEvent('pageshow')); assert.equal(app.document.documentElement.classList.contains('jio-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('.jio-verify-dialog__frame')); assert.equal(app.dialog.querySelector('form'), null); assert.equal(app.dialog.querySelector('[data-jio-verify-submit]'), null); assert.match(app.dialog.textContent, /此记录仅供查看/); app.dialog.querySelector('[data-jio-dialog-close]').click(); assert.equal(app.dialog.children.length, 0); });