feat(frontend): upgrade report dialog experience
Replace manual report links with automatic target association, improve validation and evidence uploads, and remove retired report UI code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59ddf677b6
commit
80aecab586
10 files changed
+1121
-462
No files matched your search
@@ -391,35 +391,32 @@ test('native ban page uses the scoped template-and-dialog admin flow', () => {
|
||||
assert.match(banJs, /if \(!submitting\) dialog\.close\(\)/);
|
||||
assert.match(banCss, /html\.aksrt-ban-modal-open \{ overflow: hidden/);
|
||||
});
|
||||
test('report modal mirrors the reference theme: preset reasons, image evidence, notice, geometry', () => {
|
||||
test('report modal uses accessible reasons, automatic target association and optional evidence', () => {
|
||||
const scripts = read('src/Core/ScriptsPart1c.php')
|
||||
// 预置原因真实数据源是 ban_preset_reason(UserRuntime::ban_settings),
|
||||
// 不再读取 report_settings 中不存在的 categories 键(短路已修)。
|
||||
assert.match(scripts, /UserRuntime::ban_settings\(\)\['reasons'\]/)
|
||||
assert.ok(!scripts.includes("$report['categories']"), 'dead report.categories source must be removed')
|
||||
// 图片举证开关与举报说明随 aksrtData 注入。
|
||||
assert.match(scripts, /imgEnabled/)
|
||||
assert.match(scripts, /report_settings\(\)/)
|
||||
|
||||
const reportJs = read('assets/js/report.js')
|
||||
// Zibll 同构标记:edit-header 警示头、原因徽章 active 焦点色、其它原因、
|
||||
// 图片举证入口(后台开启时)、举报说明注入、but-average modal-buts 底栏。
|
||||
for (const part of ['edit-header', 'toggle-radius mr10 c-red', 'data-aksrt-report-reason', 'form-but-radio', '其它原因', '违规链接', '详细描述', '添加图片举证', 'but-average modal-buts']) {
|
||||
assert.ok(reportJs.includes(part), `report.js missing reference-theme part: ${part}`)
|
||||
for (const part of ['aksrt-report-target', 'name="report_reason"', 'type="radio"', '其他原因', '补充说明', '图片举证', '提交举报', '无需填写链接']) {
|
||||
assert.ok(reportJs.includes(part), `report.js missing form contract: ${part}`)
|
||||
}
|
||||
assert.doesNotMatch(reportJs, /report_url|违规链接|data-aksrt-report-reason=|bindAutoHeight/)
|
||||
for (const source of [reportJs, commentItem, authorHero]) assert.doesNotMatch(source, /target_avatar/)
|
||||
assert.match(reportJs, /description\) reason \+= '\\n详细描述:\\n' \+ description/)
|
||||
assert.match(reportJs, /report\/upload-image/)
|
||||
assert.ok(!reportJs.includes('is-active'), 'chips must toggle the reference .active class, not a custom one')
|
||||
|
||||
// 举报图片上传端点存在,复用评论上传回调、独立权限(登录 + 举报 + 图片开关)。
|
||||
assert.match(reportJs, /window\.AksrtModalRuntime\.getActive\(\) === shell/)
|
||||
assert.match(userController, /'\/report\/upload-image'/)
|
||||
|
||||
// 几何 1:1:桌面端 600px(参考主题 Bootstrap 默认模态宽)。
|
||||
const css = read('assets/css/frontend/modal-runtime.css')
|
||||
const css = read('assets/css/frontend/report.css')
|
||||
assert.match(css, /\.aksrt-report-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||||
assert.match(css, /600px/)
|
||||
// 加号图标单一来源:main.css 的 .preview .add:before/:after(inset:0 + margin:auto),
|
||||
// modal-runtime 不得重复定义(否则过约束冲突,加号偏移只剩“L”形)。
|
||||
assert.ok(!/form-upload \.preview \.add::?(before|after)/.test(css), 'duplicated add plus rules must stay out of modal-runtime.css')
|
||||
assert.match(css, /:focus-visible/)
|
||||
assert.match(css, /safe-area-inset-bottom/)
|
||||
assert.match(css, /\[hidden\] \{ display: none !important;/)
|
||||
assert.doesNotMatch(modalRuntimeCss, /aksrt-report-modal/, 'retired report styles must not remain in the shared stylesheet')
|
||||
assert.match(scriptsPart1b, /assets\/css\/frontend\/report\.css/)
|
||||
})
|
||||
|
||||
test('comments expose a report entry for other users when reporting is enabled', () => {
|
||||
@@ -525,15 +522,13 @@ test('suspend (小黑屋) enforcement blocks publishing end to end', () => {
|
||||
assert.match(restriction, /您已被关入小黑屋,暂时无法评论/)
|
||||
})
|
||||
|
||||
test('report modal close icon and textarea autoHeight match the reference theme', () => {
|
||||
test('report modal delegates its labelled title, close button and focus return to the shared runtime', () => {
|
||||
const reportJs = read('assets/js/report.js')
|
||||
// 关闭按钮走 SVG 雪碧图 icon-close(与参考主题 .ic-close 同构,fa-times 退役)。
|
||||
assert.match(reportJs, /<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"><\/use><\/svg>/)
|
||||
assert.ok(!reportJs.includes('fa-times'), 'close icon must use the SVG sprite, not FontAwesome')
|
||||
// textarea 自动增高(参考主题 autoHeight 行为),隐藏中的“其它原因”跳过首次计算。
|
||||
assert.match(reportJs, /function bindAutoHeight\(/)
|
||||
assert.match(reportJs, /querySelectorAll\('textarea\.form-control'\)\.forEach\(bindAutoHeight\)/)
|
||||
assert.match(reportJs, /offsetHeight > 0\) grow\(\)/)
|
||||
assert.match(reportJs, /title: TARGETS\[data\.target_type\]\.title/)
|
||||
assert.match(reportJs, /mobileBottom: true, focusReturn: opener/)
|
||||
assert.match(reportJs, /<label class="aksrt-report-label" for=/)
|
||||
assert.match(reportJs, /data-aksrt-report-reason-error role="alert"/)
|
||||
assert.doesNotMatch(reportJs, /class="close"|style="display:|setTimeout/)
|
||||
})
|
||||
|
||||
test('wechat template messages cover report and account restriction types', () => {
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
'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 = [
|
||||
{ slug: 'spam', name: '垃圾信息' },
|
||||
{ slug: 'abuse', 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: [{ slug: 'unsafe', 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'));
|
||||
});
|
||||
|
||||
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: 'abuse',
|
||||
reason: '人身攻击\n详细描述:\nMore context',
|
||||
});
|
||||
});
|
||||
|
||||
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]').textContent, 'Server rejected report');
|
||||
|
||||
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('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');
|
||||
});
|
||||
Reference in new issue
Block a user