mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 21:40:50 +00:00
1163 lines
42 KiB
JavaScript
1163 lines
42 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import vm from 'node:vm';
|
||
import { parse as parseJavaScript } from '@babel/parser';
|
||
import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc';
|
||
import { reactive, ref } from 'vue';
|
||
|
||
// 运行真实页面脚本,API、表单实例和生命周期使用替身;不连接后台或云账户。
|
||
const sourceRoot = new URL('../src/', import.meta.url);
|
||
let passed = 0;
|
||
const flush = async () => {
|
||
for (let i = 0; i < 20; i++) await Promise.resolve();
|
||
};
|
||
function deferred() {
|
||
let resolve;
|
||
let reject;
|
||
const promise = new Promise((yes, no) => {
|
||
resolve = yes;
|
||
reject = no;
|
||
});
|
||
return { promise, resolve, reject };
|
||
}
|
||
function loadPage(name, bindings, exported) {
|
||
const url = new URL(`pages/driver/${name}.vue`, sourceRoot);
|
||
const source = readFileSync(url, 'utf8');
|
||
const { descriptor, errors } = parse(source, { filename: fileURLToPath(url) });
|
||
assert.deepEqual(errors, []);
|
||
const compiled = compileScript(descriptor, { id: name });
|
||
const template = compileTemplate({
|
||
source: descriptor.template.content,
|
||
filename: fileURLToPath(url),
|
||
id: name,
|
||
compilerOptions: { bindingMetadata: compiled.bindings },
|
||
});
|
||
assert.deepEqual(template.errors, []);
|
||
const body = descriptor.scriptSetup.content;
|
||
const ast = parseJavaScript(body, { sourceType: 'module' });
|
||
const imports = ast.program.body.filter((node) => node.type === 'ImportDeclaration');
|
||
const messages = [];
|
||
const events = [];
|
||
const context = {
|
||
ref,
|
||
reactive,
|
||
URL,
|
||
useRoute: () => ({ params: {}, query: {} }),
|
||
getCurrentInstance: () => ({ proxy: { $copyText: async () => {} } }),
|
||
defineOptions: () => {},
|
||
defineExpose: () => {},
|
||
defineProps: () => ({ driverId: 7 }),
|
||
defineEmits: () => (event) => events.push(event),
|
||
onMounted: () => {},
|
||
watch: () => {},
|
||
ElMessage: Object.fromEntries(
|
||
['success', 'error', 'warning'].map((type) => [type, (msg) => messages.push([type, msg])]),
|
||
),
|
||
ElMessageBox: async () => {},
|
||
...bindings,
|
||
};
|
||
for (const item of imports) {
|
||
for (const specifier of item.specifiers) {
|
||
if (!(specifier.local.name in context)) context[specifier.local.name] = {};
|
||
}
|
||
}
|
||
let script = body;
|
||
for (const item of [...imports].reverse()) {
|
||
script = script.slice(0, item.start) + ' '.repeat(item.end - item.start) + script.slice(item.end);
|
||
}
|
||
vm.runInNewContext(`${script}\nthis.page = { ${exported.join(', ')} };`, context, { filename: fileURLToPath(url) });
|
||
return { ...context.page, messages, events };
|
||
}
|
||
function compressionFixture(mode = 'success') {
|
||
const source = readFileSync(new URL('utils/compressImg.js', sourceRoot), 'utf8');
|
||
const ast = parseJavaScript(source, { sourceType: 'module' });
|
||
const exported = ast.program.body.find((node) => node.type === 'ExportDefaultDeclaration');
|
||
const timers = new Map();
|
||
const readers = [];
|
||
const images = [];
|
||
let aborts = 0;
|
||
let encode;
|
||
class FixtureReader {
|
||
constructor() {
|
||
this.readyState = 0;
|
||
readers.push(this);
|
||
}
|
||
readAsDataURL() {
|
||
if (mode === 'read-throw') throw new Error('fixture read failure');
|
||
this.readyState = mode === 'read-timeout' ? 1 : 2;
|
||
this.result = 'data:image/png;base64,isolated';
|
||
if (mode === 'read-timeout') return;
|
||
if (mode === 'read-error') this.onerror();
|
||
else if (mode === 'read-abort') this.onabort();
|
||
else this.onload();
|
||
}
|
||
abort() {
|
||
aborts++;
|
||
this.readyState = 2;
|
||
this.onabort?.();
|
||
}
|
||
}
|
||
class FixtureImage {
|
||
constructor() {
|
||
if (mode === 'image-throw') throw new Error('fixture image failure');
|
||
this.naturalWidth = mode === 'too-large' ? 20000001 : mode === 'zero-width' ? 0 : 100;
|
||
this.naturalHeight = mode === 'too-large' ? 1 : 100;
|
||
images.push(this);
|
||
}
|
||
set src(value) {
|
||
if (mode === 'src-throw') throw new Error('fixture source failure');
|
||
if (mode === 'image-timeout') return;
|
||
if (mode === 'image-error') this.onerror();
|
||
else this.onload();
|
||
}
|
||
}
|
||
const context2d = {
|
||
fillRect() {
|
||
if (mode === 'fill-throw') throw new Error('fixture fill failure');
|
||
},
|
||
drawImage() {
|
||
if (mode === 'draw-throw') throw new Error('fixture draw failure');
|
||
},
|
||
};
|
||
const canvas = {
|
||
width: 0,
|
||
height: 0,
|
||
getContext() {
|
||
if (mode === 'context-throw') throw new Error('fixture context failure');
|
||
return mode === 'context-null' ? null : context2d;
|
||
},
|
||
toBlob(callback, type, quality) {
|
||
assert.equal(type, 'image/jpeg');
|
||
assert.equal(quality, 0.8);
|
||
encode = callback;
|
||
if (mode === 'encode-throw') throw new Error('fixture encode failure');
|
||
if (mode === 'encode-timeout') return;
|
||
if (mode === 'blob-null') callback(null);
|
||
else
|
||
callback(
|
||
new Blob([mode === 'blob-empty' ? '' : 'isolated-jpeg'], { type: mode === 'blob-type' ? 'image/png' : type }),
|
||
);
|
||
},
|
||
};
|
||
const context = {
|
||
FileReader: FixtureReader,
|
||
Image: FixtureImage,
|
||
File:
|
||
mode === 'file-throw'
|
||
? function () {
|
||
throw new Error('fixture file failure');
|
||
}
|
||
: File,
|
||
document: {
|
||
createElement(name) {
|
||
assert.equal(name, 'canvas');
|
||
if (mode === 'canvas-throw') throw new Error('fixture canvas failure');
|
||
return canvas;
|
||
},
|
||
},
|
||
setTimeout(callback, duration) {
|
||
assert.equal(duration, 30000);
|
||
timers.set(1, callback);
|
||
return 1;
|
||
},
|
||
clearTimeout: (id) => timers.delete(id),
|
||
};
|
||
const script = source.slice(0, exported.start) + source.slice(exported.declaration.start);
|
||
vm.runInNewContext(`${script}\nthis.compressImg = compressImg;`, context);
|
||
return {
|
||
compressImg: context.compressImg,
|
||
timers,
|
||
readers,
|
||
images,
|
||
canvas,
|
||
aborts: () => aborts,
|
||
lateEncode: () => encode?.(new Blob(['late'], { type: 'image/jpeg' })),
|
||
};
|
||
}
|
||
async function scenario(name, run) {
|
||
await run();
|
||
passed++;
|
||
console.log(`PASS: ${name}`);
|
||
}
|
||
const editExports = [
|
||
'props',
|
||
'formRef',
|
||
'editForm',
|
||
'configGroups',
|
||
'commonConfig',
|
||
'editLoading',
|
||
'loadFailed',
|
||
'submitLoading',
|
||
'getDetail',
|
||
'handleSubmit',
|
||
];
|
||
const detail = (id = 7, key = 'oss') => ({
|
||
data: {
|
||
id,
|
||
driver_key: key,
|
||
driver_name: key,
|
||
sort: 0,
|
||
status: 0,
|
||
is_builtin: key === 'local' ? 1 : 0,
|
||
config_fields: [
|
||
{ field: 'accessKey', type: 'text', group: 'keys', value: '********', required: true },
|
||
{ field: 'secretKey', type: 'password', group: 'keys', value: '********', required: true },
|
||
],
|
||
bucket_fields: [{ field: 'name', type: 'text', value: '', required: true }],
|
||
},
|
||
});
|
||
|
||
async function run() {
|
||
await scenario('edit submits displayed driver fields and common config in one request', async () => {
|
||
const calls = [];
|
||
const pending = deferred();
|
||
const page = loadPage(
|
||
'uploadDriver/edit',
|
||
{
|
||
uploadDriverDetailApi: async () => detail(),
|
||
getUploadCommonConfigApi: async () => ({ data: { watermark_opacity: 0, watermark_x: 0 } }),
|
||
saveDriverConfigApi: (...args) => {
|
||
calls.push(args);
|
||
return pending.promise;
|
||
},
|
||
saveUploadCommonConfigApi: () => assert.fail('A second save must not be requested'),
|
||
},
|
||
editExports,
|
||
);
|
||
page.getDetail();
|
||
await flush();
|
||
assert.equal(page.commonConfig.value.watermark_opacity, 0);
|
||
assert.equal(page.commonConfig.value.watermark_x, 0);
|
||
page.configGroups.value[0].fields[1].value = 'synthetic-updated-key';
|
||
page.formRef.value = { validate: (callback) => callback(true) };
|
||
page.handleSubmit();
|
||
page.handleSubmit();
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(calls[0][1].secretKey, 'synthetic-updated-key');
|
||
assert.equal(calls[0][1].accessKey, '********');
|
||
assert.equal(calls[0][1].thumb_watermark_config.watermark_opacity, 0);
|
||
assert.equal(page.submitLoading.value, true);
|
||
pending.resolve({ msg: 'saved' });
|
||
await flush();
|
||
assert.deepEqual(page.events, ['success']);
|
||
assert.equal(page.submitLoading.value, false);
|
||
});
|
||
await scenario('edit failed or invalid forms cannot submit and save failure stays open', async () => {
|
||
let saves = 0;
|
||
const page = loadPage(
|
||
'uploadDriver/edit',
|
||
{
|
||
uploadDriverDetailApi: async () => {
|
||
throw new Error('fixture load failure');
|
||
},
|
||
saveDriverConfigApi: async () => {
|
||
saves++;
|
||
throw new Error('fixture save failure');
|
||
},
|
||
},
|
||
editExports,
|
||
);
|
||
page.getDetail();
|
||
await flush();
|
||
assert.equal(page.loadFailed.value, true);
|
||
assert.equal(page.editLoading.value, false);
|
||
page.formRef.value = { validate: (callback) => callback(true) };
|
||
page.handleSubmit();
|
||
assert.equal(saves, 0);
|
||
page.loadFailed.value = false;
|
||
page.formRef.value.validate = (callback) => callback(false);
|
||
page.handleSubmit();
|
||
assert.equal(saves, 0);
|
||
page.formRef.value.validate = (callback) => callback(true);
|
||
page.handleSubmit();
|
||
await flush();
|
||
assert.equal(saves, 1);
|
||
assert.equal(page.submitLoading.value, false);
|
||
assert.deepEqual(page.events, []);
|
||
});
|
||
await scenario('out-of-order edit responses cannot overwrite the selected driver', async () => {
|
||
const requests = [deferred(), deferred()];
|
||
let next = 0;
|
||
const page = loadPage(
|
||
'uploadDriver/edit',
|
||
{
|
||
uploadDriverDetailApi: () => requests[next++].promise,
|
||
getUploadCommonConfigApi: async () => ({ data: {} }),
|
||
},
|
||
editExports,
|
||
);
|
||
page.getDetail();
|
||
page.props.driverId = 8;
|
||
page.getDetail();
|
||
requests[1].resolve(detail(8, 'local'));
|
||
await flush();
|
||
requests[0].resolve(detail(7));
|
||
await flush();
|
||
assert.equal(page.editForm.value.id, 8);
|
||
assert.equal(page.editLoading.value, false);
|
||
});
|
||
const storageExports = [
|
||
'configFormRef',
|
||
'addStorageFormRef',
|
||
'driverInfo',
|
||
'configFormData',
|
||
'addStorageFormData',
|
||
'loading',
|
||
'storageList',
|
||
'configSaving',
|
||
'addSaving',
|
||
'syncSaving',
|
||
'configData',
|
||
'showConfigModal',
|
||
'showAddStorageModal',
|
||
'getDriverDetail',
|
||
'getlist',
|
||
'synchro',
|
||
'changeSwitch',
|
||
'submitConfigForm',
|
||
'submitAddStorageForm',
|
||
'canConfigureCname',
|
||
'config',
|
||
];
|
||
await scenario('storage list failure clears loading and stale results cannot replace newer ones', async () => {
|
||
const requests = [deferred(), deferred()];
|
||
let next = 0;
|
||
const page = loadPage(
|
||
'uploadDriver/storage',
|
||
{
|
||
storageListApi: () => requests[next++].promise,
|
||
},
|
||
storageExports,
|
||
);
|
||
page.driverInfo.value = detail().data;
|
||
page.getlist();
|
||
page.getlist();
|
||
requests[1].resolve({ data: { list: [{ id: 2 }], count: 1 } });
|
||
await flush();
|
||
requests[0].resolve({ data: { list: [{ id: 1 }], count: 1 } });
|
||
await flush();
|
||
assert.equal(page.storageList.value[0].id, 2);
|
||
assert.equal(page.loading.value, false);
|
||
const failure = loadPage(
|
||
'uploadDriver/storage',
|
||
{
|
||
storageListApi: async () => {
|
||
throw new Error('fixture list failure');
|
||
},
|
||
},
|
||
storageExports,
|
||
);
|
||
failure.driverInfo.value = detail().data;
|
||
await failure.getlist();
|
||
assert.equal(failure.loading.value, false);
|
||
assert.equal(failure.messages[0][0], 'error');
|
||
});
|
||
await scenario('storage detail failure disables actions and releases loading', async () => {
|
||
const page = loadPage(
|
||
'uploadDriver/storage',
|
||
{
|
||
uploadDriverDetailApi: async () => {
|
||
throw new Error('fixture detail failure');
|
||
},
|
||
storageSaveApi: () => assert.fail('Do not submit before driver detail loads'),
|
||
},
|
||
storageExports,
|
||
);
|
||
await page.getDriverDetail();
|
||
assert.equal(page.driverInfo.value, null);
|
||
assert.equal(page.loading.value, false);
|
||
page.submitAddStorageForm();
|
||
});
|
||
await scenario('storage sends explicit desired status and rolls back failure or cancellation', async () => {
|
||
const calls = [];
|
||
const page = loadPage(
|
||
'uploadDriver/storage',
|
||
{
|
||
storageStatusApi: async (...args) => {
|
||
calls.push(args);
|
||
throw new Error('fixture status failure');
|
||
},
|
||
},
|
||
storageExports,
|
||
);
|
||
const row = { id: 2, status: 0 };
|
||
page.changeSwitch(row);
|
||
await flush();
|
||
assert.deepEqual(calls, [[2, 0]]);
|
||
assert.equal(row.status, 1);
|
||
assert.equal(row._statusSaving, false);
|
||
const canceled = loadPage(
|
||
'uploadDriver/storage',
|
||
{
|
||
ElMessageBox: async () => {
|
||
throw 'cancel';
|
||
},
|
||
storageStatusApi: () => assert.fail('Canceled operation must not call API'),
|
||
},
|
||
storageExports,
|
||
);
|
||
const canceledRow = { id: 3, status: 1 };
|
||
canceled.changeSwitch(canceledRow);
|
||
await flush();
|
||
assert.equal(canceledRow.status, 0);
|
||
assert.equal(canceled.messages.length, 0);
|
||
});
|
||
await scenario('storage config and bucket forms prevent duplicate submits and release failure states', async () => {
|
||
const pending = deferred();
|
||
let configSaves = 0;
|
||
let bucketSaves = 0;
|
||
const page = loadPage(
|
||
'uploadDriver/storage',
|
||
{
|
||
saveDriverConfigApi: () => {
|
||
configSaves++;
|
||
return pending.promise;
|
||
},
|
||
storageSaveApi: async () => {
|
||
bucketSaves++;
|
||
throw new Error('fixture bucket failure');
|
||
},
|
||
},
|
||
storageExports,
|
||
);
|
||
page.driverInfo.value = detail().data;
|
||
page.configFormRef.value = { validate: (callback) => callback(true) };
|
||
page.addStorageFormRef.value = { validate: (callback) => callback(true) };
|
||
page.showConfigModal.value = true;
|
||
page.showAddStorageModal.value = true;
|
||
page.submitConfigForm();
|
||
page.submitConfigForm();
|
||
page.submitAddStorageForm();
|
||
page.submitAddStorageForm();
|
||
assert.equal(configSaves, 1);
|
||
assert.equal(bucketSaves, 1);
|
||
pending.reject(new Error('fixture config failure'));
|
||
await flush();
|
||
assert.equal(page.configSaving.value, false);
|
||
assert.equal(page.addSaving.value, false);
|
||
assert.equal(page.showConfigModal.value, true);
|
||
assert.equal(page.showAddStorageModal.value, true);
|
||
});
|
||
await scenario('default OSS endpoints do not display self-referencing CNAME instructions', async () => {
|
||
const page = loadPage('uploadDriver/storage', {}, storageExports);
|
||
assert.equal(
|
||
page.canConfigureCname({
|
||
domain: 'https://bucket.oss-cn-hangzhou.aliyuncs.com',
|
||
cname: 'bucket.oss-cn-hangzhou.aliyuncs.com',
|
||
}),
|
||
false,
|
||
);
|
||
const row = { domain: 'https://cdn.test.invalid/assets', cname: 'bucket.oss-cn-hangzhou.aliyuncs.com' };
|
||
assert.equal(page.canConfigureCname(row), true);
|
||
page.config(row);
|
||
assert.equal(page.configData.value.hostname, 'cdn.test.invalid');
|
||
});
|
||
const listExports = ['list', 'loading', 'statusSaving', 'getList', 'handleStatusChange'];
|
||
await scenario('driver list ignores stale responses and serializes mutually exclusive switches', async () => {
|
||
const requests = [deferred(), deferred()];
|
||
let next = 0;
|
||
const pending = deferred();
|
||
let statusCalls = 0;
|
||
const page = loadPage(
|
||
'uploadDriver/index',
|
||
{
|
||
uploadDriverListApi: () => requests[next++].promise,
|
||
uploadDriverSetStatusApi: () => {
|
||
statusCalls++;
|
||
return pending.promise;
|
||
},
|
||
},
|
||
listExports,
|
||
);
|
||
page.getList();
|
||
page.getList();
|
||
requests[1].resolve({ data: { list: [{ id: 8 }] } });
|
||
await flush();
|
||
requests[0].resolve({ data: { list: [{ id: 7 }] } });
|
||
await flush();
|
||
assert.equal(page.list.value[0].id, 8);
|
||
const row = { id: 7, status: 1 };
|
||
page.handleStatusChange(row);
|
||
page.handleStatusChange({ id: 8, status: 1 });
|
||
assert.equal(statusCalls, 1);
|
||
assert.equal(page.statusSaving.value, true);
|
||
pending.reject(new Error('fixture switch failure'));
|
||
await flush();
|
||
assert.equal(row.status, 0);
|
||
assert.equal(page.statusSaving.value, false);
|
||
});
|
||
await scenario('storage entry reuses the driver page and status/switch API contracts match routes', async () => {
|
||
loadPage('storage/index', {}, []);
|
||
const legacy = readFileSync(new URL('pages/driver/storage/index.vue', sourceRoot), 'utf8');
|
||
assert.match(legacy, /import UploadDriverList from '\.\.\/uploadDriver\/index\.vue'/);
|
||
const script = readFileSync(new URL('api/setting.js', sourceRoot), 'utf8');
|
||
const ast = parseJavaScript(script, { sourceType: 'module' });
|
||
for (const name of ['storageStatusApi', 'saveType']) {
|
||
const node = ast.program.body.find(
|
||
(item) => item.type === 'ExportNamedDeclaration' && item.declaration?.id?.name === name,
|
||
);
|
||
const request = node.declaration.body.body[0].argument.arguments[0];
|
||
assert.equal(request.properties.find((item) => item.key.name === 'method').value.value, 'put');
|
||
if (name === 'storageStatusApi') assert.ok(request.properties.some((item) => item.key.name === 'data'));
|
||
}
|
||
const uploads = parseJavaScript(readFileSync(new URL('api/uploadPictures.js', sourceRoot), 'utf8'), {
|
||
sourceType: 'module',
|
||
});
|
||
const cancel = uploads.program.body.find((item) => item.declaration?.id?.name === 'scanUploadCode');
|
||
const cancellation = cancel.declaration.body.body[0].argument.arguments[0];
|
||
assert.equal(
|
||
cancellation.properties.find((item) => item.key.name === 'url').value.value,
|
||
'file/scan_upload/qrcode',
|
||
);
|
||
assert.equal(cancellation.properties.find((item) => item.key.name === 'method').value.value, 'delete');
|
||
const selector = parse(readFileSync(new URL('components/uploadImg/index.vue', sourceRoot), 'utf8')).descriptor;
|
||
assert.match(selector.template.content, /@click="scanUploadGetFn">刷新图库<\/el-button>/);
|
||
});
|
||
const scannerExports = [
|
||
'imgList',
|
||
'loading',
|
||
'uploading',
|
||
'submitUpload',
|
||
'handleRemove',
|
||
'fileChange',
|
||
'preparing',
|
||
'allSize',
|
||
];
|
||
const scannerBindings = {
|
||
FormData,
|
||
document: { title: '' },
|
||
Setting: { apiBaseURL: 'https://browser.test.invalid/adminapi' },
|
||
useRoute: () => ({ params: {}, query: { token: 'a'.repeat(64), pid: '0' } }),
|
||
};
|
||
await scenario(
|
||
'phone scanner failures settle, release loading and allow retry without a false completion',
|
||
async () => {
|
||
for (const responseFailure of [false, true]) {
|
||
let fail = true;
|
||
const page = loadPage(
|
||
'../app/upload/index',
|
||
{
|
||
...scannerBindings,
|
||
scanUpload: async () => {
|
||
if (fail) {
|
||
if (responseFailure) return { status: 400, msg: 'fixture response failure' };
|
||
throw new Error('fixture transport failure');
|
||
}
|
||
return { status: 200 };
|
||
},
|
||
},
|
||
scannerExports,
|
||
);
|
||
page.imgList.value = [{ raw: new Blob(['isolated-upload-bytes']) }];
|
||
await page.submitUpload();
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploading.value, true);
|
||
assert.equal(page.messages.filter(([type]) => type === 'error').length, 1);
|
||
fail = false;
|
||
await page.submitUpload();
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploading.value, false);
|
||
}
|
||
},
|
||
);
|
||
await scenario(
|
||
'phone scanner serializes files, rejects duplicate submission and only completes after all uploads',
|
||
async () => {
|
||
const pending = [deferred(), deferred()];
|
||
const calls = [];
|
||
const page = loadPage(
|
||
'../app/upload/index',
|
||
{
|
||
...scannerBindings,
|
||
scanUpload: (data) => {
|
||
calls.push(data);
|
||
return pending[calls.length - 1].promise;
|
||
},
|
||
},
|
||
scannerExports,
|
||
);
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 0);
|
||
assert.equal(page.messages[0][0], 'warning');
|
||
page.imgList.value = [{ raw: new Blob(['first']) }, { raw: new Blob(['second']) }];
|
||
const submission = page.submitUpload();
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(calls[0].get('uploadToken'), 'a'.repeat(64));
|
||
assert.equal(calls[0].get('pid'), '0');
|
||
assert.equal(page.loading.value, true);
|
||
pending[0].resolve({ status: 200 });
|
||
await flush();
|
||
assert.equal(calls.length, 2);
|
||
assert.equal(page.uploading.value, true);
|
||
pending[1].resolve({ status: 200 });
|
||
await submission;
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploading.value, false);
|
||
},
|
||
);
|
||
await scenario('phone scanner retries only unsuccessful files after a partial batch failure', async () => {
|
||
let calls = 0;
|
||
const page = loadPage(
|
||
'../app/upload/index',
|
||
{
|
||
...scannerBindings,
|
||
scanUpload: async () => {
|
||
calls++;
|
||
if (calls === 2) throw new Error('isolated second-file failure');
|
||
return { status: 200 };
|
||
},
|
||
},
|
||
scannerExports,
|
||
);
|
||
page.imgList.value = [{ raw: new Blob(['first']) }, { raw: new Blob(['second']) }];
|
||
await page.submitUpload();
|
||
assert.equal(calls, 2);
|
||
assert.equal(page.uploading.value, true);
|
||
assert.equal(page.imgList.value[0]._scanUploaded, true);
|
||
assert.equal(page.imgList.value[1]._scanUploaded, undefined);
|
||
await page.submitUpload();
|
||
assert.equal(calls, 3);
|
||
assert.equal(page.uploading.value, false);
|
||
assert.equal(page.loading.value, false);
|
||
});
|
||
const selectorExports = [
|
||
'ruleForm',
|
||
'uploadModal',
|
||
'scanToken',
|
||
'qrCodeUrl',
|
||
'radioChange',
|
||
'scanUploadQrcodeFn',
|
||
'scanUploadGetFn',
|
||
'closed',
|
||
'handleWebRemove',
|
||
'fileChange',
|
||
'preparing',
|
||
'loading',
|
||
'submitUpload',
|
||
'beforeClose',
|
||
];
|
||
function selectorPage(bindings = {}) {
|
||
const timers = new Map();
|
||
const codes = [];
|
||
const unmounts = [];
|
||
let timerId = 0;
|
||
class FixtureQRCode {
|
||
constructor(element, options) {
|
||
codes.push(options.text);
|
||
}
|
||
}
|
||
FixtureQRCode.CorrectLevel = { H: 2 };
|
||
const page = loadPage(
|
||
'../../components/uploadImg/index',
|
||
{
|
||
...scannerBindings,
|
||
location: { origin: 'https://browser.test.invalid' },
|
||
defineProps: () => ({ categoryId: '' }),
|
||
getCookies: () => '',
|
||
onBeforeUnmount: (callback) => unmounts.push(callback),
|
||
setInterval: (callback) => {
|
||
timers.set(++timerId, callback);
|
||
return timerId;
|
||
},
|
||
clearInterval: (id) => timers.delete(id),
|
||
QRCode: FixtureQRCode,
|
||
scanUploadQrcode: async () => ({ data: { url: `/app/upload?pid=0&token=${'a'.repeat(64)}` } }),
|
||
scanUploadGet: async () => ({ data: [] }),
|
||
scanUploadCode: async () => {},
|
||
...bindings,
|
||
},
|
||
selectorExports,
|
||
);
|
||
page.uploadModal.value = true;
|
||
page.ruleForm.type = 2;
|
||
page.qrCodeUrl.value = { innerHTML: '' };
|
||
return { ...page, timers, codes, unmounts };
|
||
}
|
||
function fileFixture(uid, large = false) {
|
||
const raw = new Blob([large ? new Uint8Array(2097152) : `isolated-image-${uid}`], { type: 'image/png' });
|
||
return { uid, raw, size: raw.size, url: `blob:isolated-image-${uid}` };
|
||
}
|
||
await scenario(
|
||
'real compression helper returns JPEG files with matching names and releases canvas resources',
|
||
async () => {
|
||
for (const name of ['photo.png', 'photo.jpeg', 'photo.JPG', 'image', '.png']) {
|
||
const fixture = compressionFixture();
|
||
const compressed = await fixture.compressImg(new File(['isolated-original'], name, { type: 'image/png' }));
|
||
assert.equal(compressed.name, name === '.png' || name === 'image' ? 'image.jpg' : 'photo.jpg');
|
||
assert.equal(compressed.type, 'image/jpeg');
|
||
assert.equal(await compressed.text(), 'isolated-jpeg');
|
||
assert.equal(fixture.timers.size, 0);
|
||
assert.equal(fixture.canvas.width, 0);
|
||
assert.equal(fixture.canvas.height, 0);
|
||
assert.equal(fixture.readers[0].onload, null);
|
||
assert.equal(fixture.images[0].onload, null);
|
||
}
|
||
},
|
||
);
|
||
await scenario('real compression helper settles every reader, decoder, canvas and encoder failure', async () => {
|
||
for (const mode of [
|
||
'read-throw',
|
||
'read-error',
|
||
'read-abort',
|
||
'image-throw',
|
||
'src-throw',
|
||
'image-error',
|
||
'too-large',
|
||
'zero-width',
|
||
'canvas-throw',
|
||
'context-throw',
|
||
'context-null',
|
||
'fill-throw',
|
||
'draw-throw',
|
||
'encode-throw',
|
||
'blob-null',
|
||
'blob-empty',
|
||
'blob-type',
|
||
'file-throw',
|
||
'read-timeout',
|
||
'image-timeout',
|
||
'encode-timeout',
|
||
]) {
|
||
const fixture = compressionFixture(mode);
|
||
const outcome = fixture.compressImg(new File(['isolated-invalid'], 'invalid.png')).then(
|
||
() => assert.fail(`${mode} must not succeed`),
|
||
(error) => error,
|
||
);
|
||
if (mode.endsWith('-timeout')) fixture.timers.get(1)();
|
||
const error = await outcome;
|
||
assert.equal(typeof error.message, 'string');
|
||
assert.ok(error.message.length > 0);
|
||
assert.equal(fixture.timers.size, 0);
|
||
assert.equal(fixture.readers[0].onload, null);
|
||
assert.equal(fixture.readers[0].onerror, null);
|
||
assert.equal(fixture.readers[0].onabort, null);
|
||
if (fixture.images.length) assert.equal(fixture.images[0].onerror, null);
|
||
assert.equal(fixture.canvas.width, 0);
|
||
assert.equal(fixture.canvas.height, 0);
|
||
if (mode === 'read-timeout') assert.equal(fixture.aborts(), 1);
|
||
fixture.lateEncode();
|
||
assert.equal(fixture.timers.size, 0);
|
||
}
|
||
});
|
||
await scenario(
|
||
'picker cannot user-close during preparation or upload and all files keep their submitted group',
|
||
async () => {
|
||
const pending = deferred();
|
||
const pids = [];
|
||
const page = selectorPage({
|
||
fileUpload: (data) => {
|
||
pids.push(data.get('pid'));
|
||
return pids.length === 1 ? pending.promise : Promise.resolve({ status: 200 });
|
||
},
|
||
});
|
||
let closes = 0;
|
||
page.preparing.value = 1;
|
||
page.beforeClose(() => closes++);
|
||
assert.equal(closes, 0);
|
||
page.preparing.value = 0;
|
||
page.ruleForm.type = 0;
|
||
page.ruleForm.region = 10;
|
||
page.ruleForm.imgList = [fileFixture(1), fileFixture(2)];
|
||
const submission = page.submitUpload();
|
||
page.beforeClose(() => closes++);
|
||
assert.equal(closes, 0);
|
||
page.ruleForm.region = 99;
|
||
pending.resolve({ status: 200 });
|
||
await submission;
|
||
assert.deepEqual(pids, ['10', '10']);
|
||
page.beforeClose(() => closes++);
|
||
assert.equal(closes, 1);
|
||
const template = parse(readFileSync(new URL('components/uploadImg/index.vue', sourceRoot), 'utf8')).descriptor
|
||
.template.content;
|
||
assert.match(template, /:before-close="beforeClose"/);
|
||
assert.match(template, /:close-on-click-modal="false"/);
|
||
assert.match(template, /:close-on-press-escape="!loading && !preparing"/);
|
||
},
|
||
);
|
||
for (const phone of [true, false]) {
|
||
const kind = phone ? 'phone' : 'picker';
|
||
function uploadPage(bindings) {
|
||
if (phone) return loadPage('../app/upload/index', { ...scannerBindings, ...bindings }, scannerExports);
|
||
const page = selectorPage({ isPicUpload: () => true, ...bindings });
|
||
page.ruleForm.type = 0;
|
||
return page;
|
||
}
|
||
const images = (page) => (phone ? page.imgList.value : page.ruleForm.imgList);
|
||
const removal = (page, file) => (phone ? page.handleRemove(file) : page.handleWebRemove(file));
|
||
await scenario(
|
||
`${kind} publishes selections while compression is pending and cannot complete an old batch`,
|
||
async () => {
|
||
const compression = deferred();
|
||
const calls = [];
|
||
const upload = async (data) => {
|
||
calls.push(data);
|
||
return { status: 200 };
|
||
};
|
||
const page = uploadPage({ compressImg: () => compression.promise, scanUpload: upload, fileUpload: upload });
|
||
const first = fileFixture(1);
|
||
const second = fileFixture(2, true);
|
||
await page.fileChange(first, [first]);
|
||
const preparation = page.fileChange(second, [first, second]);
|
||
assert.equal(page.preparing.value, 1);
|
||
assert.equal(images(page).length, 2);
|
||
removal(page, first);
|
||
assert.equal(images(page).length, 2);
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 0);
|
||
assert.equal(page.messages.filter(([type]) => type === 'success').length, 0);
|
||
const compressed = new Blob(['isolated-compressed-image'], { type: 'image/png' });
|
||
compression.resolve(compressed);
|
||
await preparation;
|
||
assert.equal(page.preparing.value, 0);
|
||
assert.equal(images(page)[1].raw, compressed);
|
||
if (phone) assert.equal(page.allSize.value, first.raw.size + compressed.size);
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 2);
|
||
assert.equal(calls[1].get('file').size, compressed.size);
|
||
assert.equal(page.loading.value, false);
|
||
if (phone) assert.equal(page.uploading.value, false);
|
||
else assert.deepEqual(page.events, ['uploadSuccess']);
|
||
},
|
||
);
|
||
await scenario(`${kind} concurrent compression completions preserve all file identities`, async () => {
|
||
const pending = [deferred(), deferred()];
|
||
let next = 0;
|
||
const page = uploadPage({ compressImg: () => pending[next++].promise });
|
||
const first = fileFixture(1, true);
|
||
const second = fileFixture(2, true);
|
||
const small = fileFixture(3);
|
||
const firstChange = page.fileChange(first, [first]);
|
||
const secondChange = page.fileChange(second, [first, second]);
|
||
await page.fileChange(small, [first, second, small]);
|
||
assert.equal(page.preparing.value, 2);
|
||
const compressed = [new Blob(['first-compressed']), new Blob(['second-compressed'])];
|
||
pending[1].resolve(compressed[1]);
|
||
await secondChange;
|
||
assert.equal(page.preparing.value, 1);
|
||
assert.equal(images(page).length, 3);
|
||
pending[0].resolve(compressed[0]);
|
||
await firstChange;
|
||
assert.equal(page.preparing.value, 0);
|
||
assert.deepEqual(
|
||
Array.from(images(page), (item) => item.uid),
|
||
[1, 2, 3],
|
||
);
|
||
assert.equal(images(page)[0].raw, compressed[0]);
|
||
assert.equal(images(page)[1].raw, compressed[1]);
|
||
assert.equal(images(page)[2].raw, small.raw);
|
||
if (phone) assert.equal(page.allSize.value, compressed[0].size + compressed[1].size + small.raw.size);
|
||
});
|
||
await scenario(`${kind} compression rejection releases preparation and removes only the failed file`, async () => {
|
||
const pending = [deferred(), deferred()];
|
||
let next = 0;
|
||
const page = uploadPage({ compressImg: () => pending[next++].promise });
|
||
const first = fileFixture(1, true);
|
||
const second = fileFixture(2, true);
|
||
const small = fileFixture(3);
|
||
const firstChange = page.fileChange(first, [first]);
|
||
const secondChange = page.fileChange(second, [first, second]);
|
||
await page.fileChange(small, [first, second, small]);
|
||
pending[0].reject(new Error('isolated compression failure'));
|
||
await firstChange;
|
||
assert.equal(page.preparing.value, 1);
|
||
assert.deepEqual(
|
||
Array.from(images(page), (item) => item.uid),
|
||
[2, 3],
|
||
);
|
||
pending[1].resolve(new Blob(['second-compressed']));
|
||
await secondChange;
|
||
assert.equal(page.preparing.value, 0);
|
||
assert.equal(page.messages.filter(([type]) => type === 'error').length, 1);
|
||
assert.equal(page.loading.value, false);
|
||
assert.deepEqual(
|
||
Array.from(images(page), (item) => item.uid),
|
||
[2, 3],
|
||
);
|
||
});
|
||
}
|
||
await scenario('picker local upload failures settle and retries do not repeat successful files', async () => {
|
||
for (const responseFailure of [false, true]) {
|
||
const calls = [];
|
||
const page = selectorPage({
|
||
fileUpload: async (data) => {
|
||
calls.push(data.get('file').size);
|
||
if (calls.length === 2) {
|
||
if (responseFailure) return { status: 400, msg: 'isolated response failure' };
|
||
throw new Error('isolated upload failure');
|
||
}
|
||
return { status: 200 };
|
||
},
|
||
});
|
||
page.ruleForm.type = 0;
|
||
page.ruleForm.imgList = [fileFixture(1), fileFixture(22)];
|
||
await page.submitUpload();
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploadModal.value, true);
|
||
assert.equal(page.ruleForm.imgList[0]._uploaded, true);
|
||
assert.equal(page.ruleForm.imgList[1]._uploaded, undefined);
|
||
assert.deepEqual(page.events, []);
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 3);
|
||
assert.equal(calls[1], calls[2]);
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploadModal.value, false);
|
||
assert.deepEqual(page.events, ['uploadSuccess']);
|
||
}
|
||
});
|
||
await scenario('picker invalid image rejection removes only the matching file', async () => {
|
||
const page = selectorPage({ isPicUpload: (file) => file.uid !== 2 });
|
||
page.ruleForm.type = 0;
|
||
const files = [fileFixture(1), fileFixture(2), fileFixture(3)];
|
||
await page.fileChange(files[1], files);
|
||
assert.deepEqual(
|
||
Array.from(page.ruleForm.imgList, (item) => item.uid),
|
||
[1, 3],
|
||
);
|
||
});
|
||
await scenario('picker network and scanner failures stay open, settle and reject duplicate submits', async () => {
|
||
for (const type of [1, 2]) {
|
||
const pending = deferred();
|
||
const calls = [];
|
||
const submit = (data) => {
|
||
calls.push(data);
|
||
return calls.length === 1 ? pending.promise : Promise.resolve({ status: 200 });
|
||
};
|
||
const page = selectorPage({ onlineUpload: submit, moveApi: submit });
|
||
page.ruleForm.type = type;
|
||
page.ruleForm.region = 9;
|
||
page.ruleForm.imgList = [{ att_id: 7, url: 'https://image.test.invalid/picture.png' }];
|
||
const first = page.submitUpload();
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(calls[0].pid, 9);
|
||
assert.equal(calls[0].images[0], type === 2 ? 7 : 'https://image.test.invalid/picture.png');
|
||
pending.reject(new Error('isolated network failure'));
|
||
await first;
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploadModal.value, true);
|
||
assert.equal(page.ruleForm.imgList.length, 1);
|
||
assert.deepEqual(page.events, []);
|
||
assert.equal(page.messages.filter(([messageType]) => messageType === 'error').length, 1);
|
||
await page.submitUpload();
|
||
assert.equal(calls.length, 2);
|
||
assert.equal(page.loading.value, false);
|
||
assert.equal(page.uploadModal.value, false);
|
||
assert.deepEqual(page.events, ['uploadSuccess']);
|
||
}
|
||
});
|
||
await scenario(
|
||
'scanner removal uses attachment identity and polling cannot restore excluded selections',
|
||
async () => {
|
||
let token = 'a';
|
||
const files = [{ att_id: 1 }, { att_id: 2 }, { att_id: 3 }];
|
||
const page = selectorPage({
|
||
scanUploadQrcode: async () => ({ data: { url: `/app/upload?token=${token.repeat(64)}` } }),
|
||
scanUploadGet: async () => ({ data: files }),
|
||
});
|
||
await page.scanUploadQrcodeFn();
|
||
await page.scanUploadGetFn();
|
||
page.handleWebRemove({ att_id: '2' });
|
||
assert.deepEqual(
|
||
Array.from(page.ruleForm.imgList, (item) => item.att_id),
|
||
[1, 3],
|
||
);
|
||
await page.scanUploadGetFn();
|
||
await page.scanUploadGetFn();
|
||
assert.deepEqual(
|
||
Array.from(page.ruleForm.imgList, (item) => item.att_id),
|
||
[1, 3],
|
||
);
|
||
token = 'b';
|
||
await page.scanUploadQrcodeFn();
|
||
await page.scanUploadGetFn();
|
||
assert.deepEqual(
|
||
Array.from(page.ruleForm.imgList, (item) => item.att_id),
|
||
[1, 2, 3],
|
||
);
|
||
page.unmounts[0]();
|
||
},
|
||
);
|
||
await scenario('scanner polling preserves drag order, refreshes metadata and appends only new files', async () => {
|
||
let files = [{ att_id: 1 }, { att_id: 2 }, { att_id: 3 }];
|
||
const page = selectorPage({ scanUploadGet: async () => ({ data: files }) });
|
||
await page.scanUploadQrcodeFn();
|
||
await page.scanUploadGetFn();
|
||
page.ruleForm.imgList = [page.ruleForm.imgList[2], page.ruleForm.imgList[0], page.ruleForm.imgList[1]];
|
||
files = [{ att_id: 2, att_dir: 'updated' }, { att_id: 1 }, { att_id: 3 }, { att_id: 4 }];
|
||
await page.scanUploadGetFn();
|
||
assert.deepEqual(
|
||
Array.from(page.ruleForm.imgList, (item) => item.att_id),
|
||
[3, 1, 2, 4],
|
||
);
|
||
assert.equal(page.ruleForm.imgList[2].att_dir, 'updated');
|
||
page.unmounts[0]();
|
||
});
|
||
await scenario('QR polling starts only after a valid link and parses token with URLSearchParams', async () => {
|
||
const pending = deferred();
|
||
const calls = [];
|
||
const polls = [];
|
||
const page = selectorPage({
|
||
scanUploadQrcode: (pid) => {
|
||
calls.push(pid);
|
||
return pending.promise;
|
||
},
|
||
scanUploadGet: async (token) => {
|
||
polls.push(token);
|
||
return { data: [] };
|
||
},
|
||
});
|
||
page.qrCodeUrl.value.innerHTML = 'isolated-previous-code';
|
||
const generation = page.scanUploadQrcodeFn();
|
||
await flush();
|
||
assert.equal(page.qrCodeUrl.value.innerHTML, '');
|
||
await page.scanUploadGetFn();
|
||
assert.deepEqual(calls, [0]);
|
||
assert.deepEqual(polls, []);
|
||
assert.equal(page.timers.size, 0);
|
||
pending.resolve({ data: { url: `/app/upload?pid=0&token=${'a'.repeat(64)}&after=1` } });
|
||
await generation;
|
||
assert.equal(page.scanToken.value, 'a'.repeat(64));
|
||
assert.equal(page.timers.size, 1);
|
||
assert.equal(page.codes.length, 1);
|
||
await page.scanUploadGetFn();
|
||
assert.deepEqual(polls, ['a'.repeat(64)]);
|
||
page.unmounts[0]();
|
||
assert.equal(page.timers.size, 0);
|
||
});
|
||
await scenario(
|
||
'QR signing is serialized so an old request cannot invalidate the current category capability',
|
||
async () => {
|
||
const pending = [deferred(), deferred()];
|
||
let calls = 0;
|
||
const page = selectorPage({ scanUploadQrcode: () => pending[calls++].promise });
|
||
const first = page.scanUploadQrcodeFn();
|
||
await flush();
|
||
page.ruleForm.region = 9;
|
||
const second = page.scanUploadQrcodeFn();
|
||
await flush();
|
||
assert.equal(calls, 1);
|
||
pending[0].resolve({ data: { url: `/app/upload?pid=0&token=${'a'.repeat(64)}` } });
|
||
await first;
|
||
await flush();
|
||
assert.equal(calls, 2);
|
||
pending[1].resolve({ data: { url: `/app/upload?pid=9&token=${'b'.repeat(64)}` } });
|
||
await second;
|
||
assert.equal(page.scanToken.value, 'b'.repeat(64));
|
||
assert.equal(page.codes.length, 1);
|
||
assert.equal(page.timers.size, 1);
|
||
page.unmounts[0]();
|
||
},
|
||
);
|
||
await scenario('QR transport and malformed-link failures settle without starting empty-token polling', async () => {
|
||
for (const malformed of [false, true]) {
|
||
const page = selectorPage({
|
||
scanUploadQrcode: async () => {
|
||
if (malformed) return { data: { url: '/app/upload?token=not-a-capability' } };
|
||
throw new Error('isolated QR transport failure');
|
||
},
|
||
scanUploadGet: () => assert.fail('An invalid QR must not poll'),
|
||
});
|
||
await page.scanUploadQrcodeFn();
|
||
await page.scanUploadGetFn();
|
||
assert.equal(page.timers.size, 0);
|
||
assert.equal(page.scanToken.value, '');
|
||
assert.equal(page.messages.filter(([type]) => type === 'error').length, 1);
|
||
}
|
||
});
|
||
await scenario('gallery polling rejects duplicates and ignores stale capability responses', async () => {
|
||
const pending = deferred();
|
||
let calls = 0;
|
||
let qr = 0;
|
||
const page = selectorPage({
|
||
scanUploadQrcode: async () => ({ data: { url: `/app/upload?token=${(++qr === 1 ? 'a' : 'b').repeat(64)}` } }),
|
||
scanUploadGet: () => {
|
||
calls++;
|
||
return pending.promise;
|
||
},
|
||
});
|
||
await page.scanUploadQrcodeFn();
|
||
const first = page.scanUploadGetFn();
|
||
await page.scanUploadGetFn();
|
||
assert.equal(calls, 1);
|
||
await page.scanUploadQrcodeFn();
|
||
pending.resolve({ data: [{ att_id: 99 }] });
|
||
await first;
|
||
assert.equal(page.ruleForm.imgList.length, 0);
|
||
assert.equal(page.scanToken.value, 'b'.repeat(64));
|
||
assert.equal(page.timers.size, 1);
|
||
page.unmounts[0]();
|
||
});
|
||
await scenario('expired polling stops its timer and a manual gallery refresh can retry', async () => {
|
||
let fail = true;
|
||
const page = selectorPage({
|
||
scanUploadGet: async () => {
|
||
if (fail) throw { status: 400, msg: 'isolated expired capability' };
|
||
return { data: [{ att_id: 7 }] };
|
||
},
|
||
});
|
||
await page.scanUploadQrcodeFn();
|
||
await page.scanUploadGetFn();
|
||
assert.equal(page.timers.size, 0);
|
||
assert.equal(page.messages.filter(([type]) => type === 'error').length, 1);
|
||
fail = false;
|
||
await page.scanUploadGetFn();
|
||
assert.equal(page.ruleForm.imgList[0].att_id, 7);
|
||
page.unmounts[0]();
|
||
});
|
||
await scenario('closing invalidates in-flight QR and reopening waits for own capability revocation', async () => {
|
||
const qr = deferred();
|
||
const revocation = deferred();
|
||
let calls = 0;
|
||
let deletions = 0;
|
||
const page = selectorPage({
|
||
scanUploadQrcode: () => {
|
||
calls++;
|
||
return qr.promise;
|
||
},
|
||
scanUploadCode: () => {
|
||
deletions++;
|
||
return revocation.promise;
|
||
},
|
||
});
|
||
const first = page.scanUploadQrcodeFn();
|
||
await flush();
|
||
page.closed();
|
||
qr.resolve({ data: { url: `/app/upload?token=${'a'.repeat(64)}` } });
|
||
await first;
|
||
assert.equal(page.scanToken.value, '');
|
||
assert.equal(page.codes.length, 0);
|
||
page.ruleForm.type = 2;
|
||
const reopened = page.scanUploadQrcodeFn();
|
||
await flush();
|
||
assert.equal(calls, 1);
|
||
assert.equal(deletions, 1);
|
||
revocation.resolve();
|
||
await reopened;
|
||
assert.equal(calls, 2);
|
||
assert.equal(page.timers.size, 1);
|
||
page.unmounts[0]();
|
||
});
|
||
await scenario('failed capability revocation is visible and cannot leave QR regeneration pending', async () => {
|
||
const page = selectorPage({
|
||
scanUploadCode: async () => {
|
||
throw new Error('isolated revocation failure');
|
||
},
|
||
});
|
||
page.closed();
|
||
page.ruleForm.type = 2;
|
||
await page.scanUploadQrcodeFn();
|
||
assert.equal(page.messages.filter(([type]) => type === 'error').length, 1);
|
||
assert.equal(page.scanToken.value, 'a'.repeat(64));
|
||
assert.equal(page.timers.size, 1);
|
||
page.unmounts[0]();
|
||
});
|
||
console.log(`Storage admin regression: ${passed} scenarios passed`);
|
||
}
|
||
|
||
run().catch((error) => {
|
||
console.error(error);
|
||
process.exitCode = 1;
|
||
});
|