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>
547 lines
21 KiB
JavaScript
547 lines
21 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 { MiniDocument, MiniElement } = require('../helpers/mini-dom.cjs');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
|
|
class ReportElement extends MiniElement {
|
|
constructor(tagName) {
|
|
super(tagName);
|
|
this._value = null;
|
|
this.files = [];
|
|
}
|
|
|
|
get value() { return this._value ?? this.getAttribute('value') ?? ''; }
|
|
set value(value) { this._value = String(value ?? ''); }
|
|
get name() { return this.getAttribute('name') || ''; }
|
|
get type() { return this.getAttribute('type') || ''; }
|
|
get checked() { return this.hasAttribute('checked'); }
|
|
set checked(value) { value ? this.setAttribute('checked', '') : this.removeAttribute('checked'); }
|
|
get disabled() { return this.hasAttribute('disabled'); }
|
|
set disabled(value) { value ? this.setAttribute('disabled', '') : this.removeAttribute('disabled'); }
|
|
get hidden() { return this.hasAttribute('hidden'); }
|
|
set hidden(value) { value ? this.setAttribute('hidden', '') : this.removeAttribute('hidden'); }
|
|
|
|
focus() { this.ownerDocument.activeElement = this; }
|
|
click() { this.dispatch('click'); }
|
|
|
|
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
|
|
|
|
querySelectorAll(selector) {
|
|
const groups = String(selector).split(',');
|
|
return [...new Set(groups.flatMap((group) => {
|
|
const checked = /:checked\b/.test(group);
|
|
const disabled = /:disabled\b/.test(group);
|
|
const plainSelector = group.replace(/:(?:checked|disabled)\b/g, '').trim();
|
|
return super.querySelectorAll(plainSelector).filter((element) =>
|
|
(!checked || element.checked) && (!disabled || element.disabled));
|
|
}))];
|
|
}
|
|
}
|
|
|
|
class ReportDocument extends MiniDocument {
|
|
constructor() {
|
|
super();
|
|
this.documentElement = this;
|
|
this.activeElement = null;
|
|
}
|
|
|
|
createElement(tagName) {
|
|
const element = new ReportElement(tagName);
|
|
element.ownerDocument = this;
|
|
return element;
|
|
}
|
|
}
|
|
|
|
class MiniFormData {
|
|
constructor() { this.values = []; }
|
|
append(name, value) { this.values.push([name, value]); }
|
|
get(name) { return this.values.find(([key]) => key === name)?.[1] ?? null; }
|
|
}
|
|
|
|
function createRuntime({ imgEnabled = true, categories = [
|
|
{ name: '垃圾信息' },
|
|
{ name: '人身攻击' },
|
|
] } = {}) {
|
|
const document = new ReportDocument();
|
|
const modals = [];
|
|
const fetchCalls = [];
|
|
const notyfCalls = [];
|
|
const closeCalls = [];
|
|
let activeModal = null;
|
|
let autoInit = null;
|
|
|
|
function fetch(url, options = {}) {
|
|
let resolveRequest;
|
|
let rejectRequest;
|
|
const promise = new Promise((resolve, reject) => {
|
|
resolveRequest = resolve;
|
|
rejectRequest = reject;
|
|
});
|
|
const call = {
|
|
url: String(url),
|
|
options,
|
|
resolve(data) {
|
|
resolveRequest({ json: () => Promise.resolve(data) });
|
|
},
|
|
reject(error) { rejectRequest(error); },
|
|
};
|
|
fetchCalls.push(call);
|
|
if (options.method === 'DELETE') call.resolve({ success: true });
|
|
return promise;
|
|
}
|
|
|
|
const modalRuntime = {
|
|
open(options) {
|
|
const shell = document.createElement('div');
|
|
shell.className = options.className;
|
|
shell.innerHTML = options.html;
|
|
document.body.appendChild(shell);
|
|
activeModal = shell;
|
|
modals.push({ shell, options });
|
|
return shell;
|
|
},
|
|
getActive() { return activeModal; },
|
|
close(reason, shell = activeModal) {
|
|
closeCalls.push({ reason, shell });
|
|
if (!shell) return;
|
|
if (activeModal === shell) activeModal = null;
|
|
shell.remove();
|
|
document.dispatch('jio:modal:close', { detail: { shell } });
|
|
},
|
|
};
|
|
|
|
const window = {
|
|
jioData: {
|
|
apiUrl: '/wp-json/jio/v1',
|
|
restNonce: 'test-nonce',
|
|
reportCategories: categories,
|
|
report: { imgEnabled },
|
|
},
|
|
JioModalRuntime: modalRuntime,
|
|
fetch,
|
|
};
|
|
const jquery = () => ({
|
|
off() { return this; },
|
|
on(event, callback) {
|
|
if (event === 'auto_fun.jio-report') autoInit = callback;
|
|
return this;
|
|
},
|
|
});
|
|
|
|
vm.runInNewContext(fs.readFileSync(path.join(root, 'assets/js/report.js'), 'utf8'), {
|
|
window,
|
|
document,
|
|
jQuery: jquery,
|
|
FormData: MiniFormData,
|
|
Promise,
|
|
console,
|
|
notyf: (...args) => notyfCalls.push(args),
|
|
});
|
|
assert.equal(typeof autoInit, 'function');
|
|
autoInit();
|
|
|
|
return {
|
|
document,
|
|
window,
|
|
modalRuntime,
|
|
modals,
|
|
fetchCalls,
|
|
notyfCalls,
|
|
closeCalls,
|
|
get activeModal() { return activeModal; },
|
|
openReport(target = { target_type: 'post', target_id: 42, target_name: 'Article' }) {
|
|
const opener = document.createElement('button');
|
|
opener.setAttribute('data-jio-report', JSON.stringify(target));
|
|
document.body.appendChild(opener);
|
|
const event = opener.dispatch('click');
|
|
return { ...modals.at(-1), opener, event };
|
|
},
|
|
};
|
|
}
|
|
|
|
function selectReason(modal, value) {
|
|
const radios = modal.shell.querySelectorAll('[name="report_reason"]');
|
|
const selected = radios.find((radio) => radio.value === String(value));
|
|
assert.ok(selected, `missing reason ${value}`);
|
|
radios.forEach((radio) => { radio.checked = radio === selected; });
|
|
selected.dispatch('change');
|
|
return selected;
|
|
}
|
|
|
|
function submit(modal) {
|
|
return modal.shell.querySelector('.jio-report-form').dispatch('submit');
|
|
}
|
|
|
|
function callsFor(runtime, endpoint) {
|
|
return runtime.fetchCalls.filter((call) => call.url.includes(endpoint));
|
|
}
|
|
|
|
function payloadFor(call) {
|
|
assert.equal(typeof call.options.body, 'string');
|
|
return JSON.parse(call.options.body);
|
|
}
|
|
|
|
async function flushPromises() {
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
}
|
|
|
|
test('opens from an article button, associates the target, omits URL input, and escapes rendered names', () => {
|
|
const runtime = createRuntime({ categories: [{ name: '<svg onload="run()">reason</svg>' }] });
|
|
const targetName = '<img src=x onerror="run()"> &';
|
|
const modal = runtime.openReport({ target_type: 'post', target_id: 73, target_name: targetName });
|
|
const target = modal.shell.querySelector('.jio-report-target__name');
|
|
const reason = modal.shell.querySelector('.jio-report-reason span');
|
|
|
|
assert.equal(modal.event.defaultPrevented, true);
|
|
assert.equal(modal.options.title, '举报文章');
|
|
assert.equal(modal.options.focusReturn, modal.opener);
|
|
assert.equal(runtime.activeModal, modal.shell);
|
|
assert.ok(target.textContent.includes('<img'));
|
|
assert.equal(target.querySelector('img'), null);
|
|
assert.equal(reason.querySelector('svg'), null);
|
|
assert.equal(modal.shell.querySelector('[name="report_url"]'), null);
|
|
assert.equal(modal.shell.querySelector('input[type="url"]'), null);
|
|
assert.ok(modal.shell.querySelector('.jio-report-target'));
|
|
assert.equal(modal.shell.querySelector('.jio-report-target__label').textContent, '关联对象:');
|
|
assert.equal(modal.shell.querySelector('.jio-report-target__icon'), null);
|
|
assert.equal(target.getAttribute('title'), target.textContent);
|
|
});
|
|
|
|
test('requires a radio reason and other text, then sends canonical fields with description', async () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
const form = modal.shell.querySelector('.jio-report-form');
|
|
const reasonError = form.querySelector('[data-jio-report-reason-error]');
|
|
const otherBox = form.querySelector('.jio-report-other');
|
|
const otherInput = form.querySelector('[name="reason_other"]');
|
|
const description = form.querySelector('[name="report_desc"]');
|
|
|
|
submit(modal);
|
|
assert.equal(reasonError.hidden, false);
|
|
assert.equal(reasonError.textContent, '请选择一项举报原因');
|
|
assert.equal(runtime.document.activeElement, form.querySelector('[name="report_reason"]'));
|
|
assert.equal(callsFor(runtime, '/report/submit').length, 0);
|
|
|
|
selectReason(modal, 'other');
|
|
assert.equal(otherBox.hidden, false);
|
|
assert.equal(otherInput.disabled, false);
|
|
submit(modal);
|
|
assert.equal(reasonError.textContent, '请填写举报原因');
|
|
assert.equal(otherInput.getAttribute('aria-invalid'), 'true');
|
|
assert.equal(runtime.document.activeElement, otherInput);
|
|
assert.equal(callsFor(runtime, '/report/submit').length, 0);
|
|
|
|
otherInput.value = '自定义原因';
|
|
otherInput.dispatch('input');
|
|
description.value = '补充背景';
|
|
description.dispatch('input');
|
|
submit(modal);
|
|
const call = callsFor(runtime, '/report/submit')[0];
|
|
const payload = payloadFor(call);
|
|
|
|
assert.deepEqual(Object.keys(payload).sort(), ['category', 'reason', 'target_id', 'target_type']);
|
|
assert.deepEqual(payload, {
|
|
target_type: 'post',
|
|
target_id: 42,
|
|
category: '其他原因',
|
|
reason: '自定义原因\n\n补充背景',
|
|
});
|
|
call.resolve({ success: true, message: '已提交' });
|
|
await flushPromises();
|
|
});
|
|
|
|
test('preset reason ignores hidden custom text while including description in reason', () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
const otherInput = modal.shell.querySelector('[name="reason_other"]');
|
|
const description = modal.shell.querySelector('[name="report_desc"]');
|
|
otherInput.value = 'hidden custom text';
|
|
description.value = 'More context';
|
|
selectReason(modal, 1);
|
|
|
|
assert.equal(modal.shell.querySelector('.jio-report-other').hidden, true);
|
|
assert.equal(otherInput.disabled, true);
|
|
submit(modal);
|
|
const payload = payloadFor(callsFor(runtime, '/report/submit')[0]);
|
|
|
|
assert.deepEqual(payload, {
|
|
target_type: 'post',
|
|
target_id: 42,
|
|
category: '人身攻击',
|
|
reason: 'More context',
|
|
});
|
|
});
|
|
|
|
test('allows a preset-only report without copying the category into content', async () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
selectReason(modal, 0);
|
|
submit(modal);
|
|
const request = callsFor(runtime, '/report/submit')[0];
|
|
assert.deepEqual(payloadFor(request), {
|
|
target_type: 'post', target_id: 42, category: '垃圾信息', reason: '',
|
|
});
|
|
request.resolve({ success: true });
|
|
await flushPromises();
|
|
});
|
|
|
|
test('keeps a single other option and preserves explicitly entered category words in content', () => {
|
|
const runtime = createRuntime({ categories: [{ name: '其他原因' }, { name: '垃圾信息' }] });
|
|
const modal = runtime.openReport();
|
|
const radios = modal.shell.querySelectorAll('[name="report_reason"]');
|
|
assert.equal(radios.length, 2);
|
|
selectReason(modal, 0);
|
|
modal.shell.querySelector('[name="report_desc"]').value = '垃圾信息中包含了违规内容';
|
|
submit(modal);
|
|
assert.deepEqual(payloadFor(callsFor(runtime, '/report/submit')[0]), {
|
|
target_type: 'post', target_id: 42, category: '垃圾信息', reason: '垃圾信息中包含了违规内容',
|
|
});
|
|
});
|
|
|
|
test('omits all image controls when image evidence is disabled', () => {
|
|
const runtime = createRuntime({ imgEnabled: false });
|
|
const modal = runtime.openReport();
|
|
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-file]'), null);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-add]'), null);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-images]'), null);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]'), null);
|
|
});
|
|
|
|
test('counts in-flight images once, accepts exactly three, and blocks submit until ready', async () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
selectReason(modal, 0);
|
|
const fileInput = modal.shell.querySelector('[data-jio-report-file]');
|
|
fileInput.files = [1, 2, 3].map((index) => ({ name: `evidence-${index}.png`, type: 'image/png' }));
|
|
fileInput.dispatch('change');
|
|
|
|
assert.equal(callsFor(runtime, '/report/upload-image').length, 3);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]').textContent, '3 / 3');
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-submit]').disabled, true);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-status]').textContent, '图片正在上传,完成后即可提交');
|
|
submit(modal);
|
|
assert.equal(callsFor(runtime, '/report/submit').length, 0);
|
|
|
|
callsFor(runtime, '/report/upload-image').forEach((call, index) => call.resolve({
|
|
success: true,
|
|
data: { id: 800 + index, url: `https://evidence.test/${index + 1}.png` },
|
|
}));
|
|
await flushPromises();
|
|
assert.equal(modal.shell.querySelectorAll('.jio-report-image').length, 3);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]').textContent, '3 / 3');
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-add]').disabled, true);
|
|
|
|
fileInput.files = [{ name: 'fourth.png', type: 'image/png' }];
|
|
fileInput.dispatch('change');
|
|
assert.equal(callsFor(runtime, '/report/upload-image').length, 3);
|
|
|
|
submit(modal);
|
|
const call = callsFor(runtime, '/report/submit')[0];
|
|
const payload = payloadFor(call);
|
|
for (const index of [1, 2, 3]) assert.ok(payload.reason.includes(`https://evidence.test/${index}.png`));
|
|
call.resolve({ success: true });
|
|
await flushPromises();
|
|
});
|
|
|
|
test('retries a failed upload and deletes its pending attachment when removed', async () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
const fileInput = modal.shell.querySelector('[data-jio-report-file]');
|
|
fileInput.files = [{ name: 'retry.png', type: 'image/png' }];
|
|
fileInput.dispatch('change');
|
|
|
|
callsFor(runtime, '/report/upload-image')[0].resolve({ success: false, message: 'Upload failed' });
|
|
await flushPromises();
|
|
const image = modal.shell.querySelector('.jio-report-image');
|
|
assert.equal(image.getAttribute('data-upload-state'), 'error');
|
|
assert.match(modal.shell.querySelector('[data-jio-report-error]').textContent, /Upload failed/);
|
|
|
|
image.querySelector('[data-jio-report-retry]').dispatch('click');
|
|
const retry = callsFor(runtime, '/report/upload-image')[1];
|
|
assert.ok(retry);
|
|
retry.resolve({ success: true, data: { id: 901, url: 'https://evidence.test/retry.png' } });
|
|
await flushPromises();
|
|
assert.equal(image.getAttribute('data-upload-state'), 'ready');
|
|
|
|
image.querySelector('[data-jio-report-remove]').dispatch('click');
|
|
const deletion = callsFor(runtime, '/upload/pending/901')[0];
|
|
assert.ok(deletion);
|
|
assert.equal(deletion.options.method, 'DELETE');
|
|
assert.equal(modal.shell.querySelectorAll('.jio-report-image').length, 0);
|
|
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]').textContent, '0 / 3');
|
|
});
|
|
|
|
test('server failure preserves values, reenables controls, and allows a retry', async () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
selectReason(modal, 'other');
|
|
const form = modal.shell.querySelector('.jio-report-form');
|
|
const otherInput = form.querySelector('[name="reason_other"]');
|
|
const description = form.querySelector('[name="report_desc"]');
|
|
const submitButton = form.querySelector('[data-jio-report-submit]');
|
|
otherInput.value = 'A custom report reason';
|
|
description.value = 'Additional context';
|
|
|
|
submit(modal);
|
|
const failedRequest = callsFor(runtime, '/report/submit')[0];
|
|
assert.equal(submitButton.disabled, true);
|
|
assert.equal(otherInput.disabled, true);
|
|
failedRequest.resolve({ success: false, message: 'Server rejected report' });
|
|
await flushPromises();
|
|
|
|
assert.equal(otherInput.value, 'A custom report reason');
|
|
assert.equal(description.value, 'Additional context');
|
|
assert.equal(form.querySelector('[name="report_reason"]:checked').value, 'other');
|
|
assert.equal(otherInput.disabled, false);
|
|
assert.equal(description.disabled, false);
|
|
assert.equal(submitButton.disabled, false);
|
|
assert.equal(form.querySelector('[data-jio-report-error]').hidden, true);
|
|
assert.deepEqual(runtime.notyfCalls.at(-1).slice(0, 3), ['Server rejected report', 'danger', 5]);
|
|
|
|
submit(modal);
|
|
const retry = callsFor(runtime, '/report/submit')[1];
|
|
assert.equal(payloadFor(retry).reason, 'A custom report reason\n\nAdditional context');
|
|
retry.resolve({ success: true });
|
|
await flushPromises();
|
|
});
|
|
|
|
test('uses a persistent shared toast per submit attempt without inline submitting text', async () => {
|
|
const runtime = createRuntime();
|
|
const modal = runtime.openReport();
|
|
selectReason(modal, 0);
|
|
const form = modal.shell.querySelector('.jio-report-form');
|
|
const button = form.querySelector('[data-jio-report-submit]');
|
|
const status = form.querySelector('[data-jio-report-status]');
|
|
|
|
submit(modal);
|
|
const firstToast = runtime.notyfCalls.at(-1);
|
|
assert.deepEqual(firstToast.slice(0, 3), ['正在提交举报…', 'load', '']);
|
|
assert.ok(firstToast[3].startsWith(form.id + '-submit-'));
|
|
assert.equal(button.textContent, '提交举报');
|
|
assert.equal(button.disabled, true);
|
|
assert.equal(status.textContent, '');
|
|
assert.equal(form.getAttribute('aria-busy'), 'true');
|
|
|
|
submit(modal);
|
|
assert.equal(runtime.notyfCalls.length, 1);
|
|
callsFor(runtime, '/report/submit')[0].resolve({ success: false, message: 'Retry report' });
|
|
await flushPromises();
|
|
assert.deepEqual(runtime.notyfCalls.at(-1), ['Retry report', 'danger', 5, firstToast[3]]);
|
|
assert.equal(button.disabled, false);
|
|
assert.equal(status.textContent, '');
|
|
|
|
submit(modal);
|
|
const secondToast = runtime.notyfCalls.at(-1);
|
|
assert.equal(secondToast[1], 'load');
|
|
assert.notEqual(secondToast[3], firstToast[3]);
|
|
callsFor(runtime, '/report/submit')[1].resolve({ success: true, message: 'Report accepted' });
|
|
await flushPromises();
|
|
assert.deepEqual(runtime.notyfCalls.at(-1), ['Report accepted', 'success', 5, secondToast[3]]);
|
|
});
|
|
|
|
test('blocks duplicate submits and closing an old submitting dialog cannot close the new one', async () => {
|
|
const runtime = createRuntime();
|
|
const oldModal = runtime.openReport();
|
|
selectReason(oldModal, 0);
|
|
submit(oldModal);
|
|
submit(oldModal);
|
|
assert.equal(callsFor(runtime, '/report/submit').length, 1);
|
|
|
|
const newModal = runtime.openReport({ target_type: 'comment', target_id: 88, target_name: 'Comment' });
|
|
runtime.modalRuntime.close('dismiss', oldModal.shell);
|
|
callsFor(runtime, '/report/submit')[0].resolve({ success: true });
|
|
await flushPromises();
|
|
|
|
assert.equal(runtime.activeModal, newModal.shell);
|
|
assert.equal(runtime.closeCalls.length, 1);
|
|
assert.equal(runtime.closeCalls[0].shell, oldModal.shell);
|
|
});
|
|
|
|
test('cleans pending attachments on close and when an upload completes after close', async () => {
|
|
const runtime = createRuntime();
|
|
const readyModal = runtime.openReport();
|
|
const readyFile = readyModal.shell.querySelector('[data-jio-report-file]');
|
|
readyFile.files = [{ name: 'ready.png', type: 'image/png' }];
|
|
readyFile.dispatch('change');
|
|
callsFor(runtime, '/report/upload-image')[0].resolve({
|
|
success: true,
|
|
data: { id: 1001, url: 'https://evidence.test/ready.png' },
|
|
});
|
|
await flushPromises();
|
|
runtime.modalRuntime.close('cancel', readyModal.shell);
|
|
|
|
const readyDeletion = callsFor(runtime, '/upload/pending/1001')[0];
|
|
assert.ok(readyDeletion);
|
|
assert.equal(readyDeletion.options.method, 'DELETE');
|
|
|
|
const pendingModal = runtime.openReport();
|
|
const pendingFile = pendingModal.shell.querySelector('[data-jio-report-file]');
|
|
pendingFile.files = [{ name: 'pending.png', type: 'image/png' }];
|
|
pendingFile.dispatch('change');
|
|
const pendingUpload = callsFor(runtime, '/report/upload-image')[1];
|
|
runtime.modalRuntime.close('cancel', pendingModal.shell);
|
|
assert.equal(callsFor(runtime, '/upload/pending/1002').length, 0);
|
|
|
|
pendingUpload.resolve({
|
|
success: true,
|
|
data: { id: 1002, url: 'https://evidence.test/pending.png' },
|
|
});
|
|
await flushPromises();
|
|
const lateDeletion = callsFor(runtime, '/upload/pending/1002')[0];
|
|
assert.ok(lateDeletion);
|
|
assert.equal(lateDeletion.options.method, 'DELETE');
|
|
});
|
|
|
|
test('keeps a live child modal open and marks the finished report after success', async () => {
|
|
const runtime = createRuntime();
|
|
const report = runtime.openReport();
|
|
selectReason(report, 0);
|
|
submit(report);
|
|
const request = callsFor(runtime, '/report/submit')[0];
|
|
const child = runtime.modalRuntime.open({ className: 'jio-child-modal', html: '<p>Child dialog</p>' });
|
|
request.resolve({ success: true });
|
|
await flushPromises();
|
|
|
|
const form = report.shell.querySelector('.jio-report-form');
|
|
assert.equal(runtime.activeModal, child);
|
|
assert.equal(runtime.closeCalls.length, 0);
|
|
assert.equal(form.getAttribute('aria-busy'), 'false');
|
|
assert.equal(form.querySelector('[data-jio-report-submit]').textContent, '举报已提交');
|
|
assert.ok(report.shell.querySelector('[data-jio-report-status]'));
|
|
});
|
|
|
|
test('retains claimed evidence on post-close success and deletes it on post-close failure', async () => {
|
|
const runtime = createRuntime();
|
|
async function submitClosedReport(id, result) {
|
|
const modal = runtime.openReport();
|
|
selectReason(modal, 0);
|
|
const fileInput = modal.shell.querySelector('[data-jio-report-file]');
|
|
fileInput.files = [{ name: `${id}.png`, type: 'image/png' }];
|
|
fileInput.dispatch('change');
|
|
callsFor(runtime, '/report/upload-image').at(-1).resolve({
|
|
success: true,
|
|
data: { id, url: `https://evidence.test/${id}.png` },
|
|
});
|
|
await flushPromises();
|
|
submit(modal);
|
|
const request = callsFor(runtime, '/report/submit').at(-1);
|
|
runtime.modalRuntime.close('dismiss', modal.shell);
|
|
request.resolve(result);
|
|
await flushPromises();
|
|
}
|
|
|
|
await submitClosedReport(1101, { success: true });
|
|
assert.equal(callsFor(runtime, '/upload/pending/1101').length, 0);
|
|
await submitClosedReport(1102, { success: false, message: 'Rejected' });
|
|
const deletion = callsFor(runtime, '/upload/pending/1102')[0];
|
|
assert.ok(deletion);
|
|
assert.equal(deletion.options.method, 'DELETE');
|
|
});
|