Files
ARR-2.0-0918/tests/browser/arr_upload_emoji_notice.cjs
T

76 lines
3.6 KiB
JavaScript

// Execute the real upload handler against synthetic responses and a minimal DOM
// adapter; no browser, server, credentials or business uploads are used.
const {test} = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '../..');
const app = fs.readFileSync(path.join(root, 'arr_web/static/app.js'), 'utf8');
const i18n = fs.readFileSync(path.join(root, 'arr_web/static/i18n.js'), 'utf8');
const handler = app.slice(app.indexOf('async function handleUpload()'), app.indexOf('async function loadMonthsAndAnalytics()'));
const catalog = Object.fromEntries([...i18n.matchAll(/^\s*"([^"]+)": (\[.*\]),?$/gm)].map(match => [match[1], JSON.parse(match[2])]));
async function upload(receipt, locale = 0) {
const nodes = new Map();
const notices = [];
const progress = [];
const source = Buffer.from('<RES_DETAIL>synthetic</RES_DETAIL>');
const noop = () => {};
const context = {
state: {health: {processing_ready: true}, jobs: [], selectedFile: {name: 'ARR.XML', arrayBuffer: async () => source}},
$: selector => {
if (!nodes.has(selector)) nodes.set(selector, {classList: {add: noop, toggle: noop}, setAttribute: noop, removeAttribute: noop});
return nodes.get(selector);
},
I18N: {t: (key, values = {}) => catalog[key]?.[locale]?.replace(/\{(\w+)\}/g, (_, name) => values[name])},
api: async (url, options) => {
assert.equal(url, '/api/jobs');
assert.deepEqual(options.body, source);
return receipt;
},
encodedFilename: value => value,
clearTracePoll: noop, renderJobs: noop, renderTracePlaceholder: noop,
setTraceLiveState: noop, startUploadProgress: noop, renderHistoryMonthControl: noop,
validMonth: () => true, localMonth: () => '2026-07', loadJobs: async () => {},
selectJob: async () => {}, openDailyPriceReview: async () => {},
finishUploadProgress: (...args) => progress.push(args),
showToast: (...args) => notices.push(args),
};
vm.createContext(context);
vm.runInContext(handler, context);
await context.handleUpload();
assert.equal(context.state.uploadInFlight, false);
assert.equal(nodes.get('#xml-file').disabled, false);
assert.equal(notices.length, 1);
return {notice: notices[0], progress};
}
test('successful uploads show the verified emoji count in all three languages', async () => {
const expected = ['已忽略 4 处表情符号', 'Ignored 4 emoji sequences.', 'ละเว้นอีโมจิ 4 รายการแล้ว'];
for (let locale = 0; locale < 3; locale++) {
const {notice, progress} = await upload({status: 'succeeded', ignored_emoji_count: 4}, locale);
assert.equal(notice[1], false);
assert(notice[0].includes(expected[locale]));
assert.equal(progress[0][0], true);
}
});
test('review and failure keep their original status while disclosing cleanup', async () => {
for (const status of ['needs_review', 'failed']) {
const {notice, progress} = await upload({status, ignored_emoji_count: 1});
assert(notice[0].includes('已忽略 1 处表情符号'));
assert(notice[0].includes(status === 'failed' ? '处理失败' : '价格复核'));
assert.equal(notice[1], status === 'failed');
assert.equal(progress[0][0], status !== 'failed');
}
});
test('no cleanup notice is invented for old, empty or invalid metadata', async () => {
for (const count of [undefined, 0, -1, true, '4', 1.5]) {
const {notice} = await upload({status: 'succeeded', ignored_emoji_count: count});
assert.equal(notice[0], catalog['upload.success'][0]);
assert.equal(notice[1], false);
}
});