Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.
Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)
Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
(dropped 3 stale entries, added the 17 missing ones)
Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
when it is empty
Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.
Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
675 lines
32 KiB
JavaScript
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-jio-dialog-close': '' }, '关闭');
|
|
}
|
|
|
|
function makeDetailFrame(document, text = '<img src=x onerror=alert(1)>') {
|
|
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, /<img src=x onerror=alert\(1\)>/);
|
|
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);
|
|
});
|