Store readable category snapshots separately from details and evidence, validate report categories, and preserve context in admin displays and moderation actions. Widen report categories to TEXT and bump the schema to 3.2.0 without legacy data conversion. Co-Authored-By: Claude Fable 5 <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('aksrt:modal:close', { detail: { shell } });
|
|
},
|
|
};
|
|
|
|
const window = {
|
|
aksrtData: {
|
|
apiUrl: '/wp-json/aksrt/v1',
|
|
restNonce: 'test-nonce',
|
|
reportCategories: categories,
|
|
report: { imgEnabled },
|
|
},
|
|
AksrtModalRuntime: modalRuntime,
|
|
fetch,
|
|
};
|
|
const jquery = () => ({
|
|
off() { return this; },
|
|
on(event, callback) {
|
|
if (event === 'auto_fun.aksrt-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-aksrt-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('.aksrt-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('.aksrt-report-target__name');
|
|
const reason = modal.shell.querySelector('.aksrt-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('.aksrt-report-target'));
|
|
assert.equal(modal.shell.querySelector('.aksrt-report-target__label').textContent, '关联对象:');
|
|
assert.equal(modal.shell.querySelector('.aksrt-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('.aksrt-report-form');
|
|
const reasonError = form.querySelector('[data-aksrt-report-reason-error]');
|
|
const otherBox = form.querySelector('.aksrt-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('.aksrt-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-aksrt-report-file]'), null);
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-report-add]'), null);
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-report-images]'), null);
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-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-aksrt-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-aksrt-report-image-count]').textContent, '3 / 3');
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-report-submit]').disabled, true);
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-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('.aksrt-report-image').length, 3);
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '3 / 3');
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-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-aksrt-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('.aksrt-report-image');
|
|
assert.equal(image.getAttribute('data-upload-state'), 'error');
|
|
assert.match(modal.shell.querySelector('[data-aksrt-report-error]').textContent, /Upload failed/);
|
|
|
|
image.querySelector('[data-aksrt-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-aksrt-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('.aksrt-report-image').length, 0);
|
|
assert.equal(modal.shell.querySelector('[data-aksrt-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('.aksrt-report-form');
|
|
const otherInput = form.querySelector('[name="reason_other"]');
|
|
const description = form.querySelector('[name="report_desc"]');
|
|
const submitButton = form.querySelector('[data-aksrt-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-aksrt-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('.aksrt-report-form');
|
|
const button = form.querySelector('[data-aksrt-report-submit]');
|
|
const status = form.querySelector('[data-aksrt-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-aksrt-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-aksrt-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: 'aksrt-child-modal', html: '<p>Child dialog</p>' });
|
|
request.resolve({ success: true });
|
|
await flushPromises();
|
|
|
|
const form = report.shell.querySelector('.aksrt-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-aksrt-report-submit]').textContent, '举报已提交');
|
|
assert.ok(report.shell.querySelector('[data-aksrt-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-aksrt-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');
|
|
});
|