Files
jiapu/tests/attachment-editor-browser.mjs
T
2026-09-13 19:41:18 +08:00

150 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { createServer } from 'node:http';
import { readFile, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { resolve, extname, join, dirname, basename } from 'node:path';
const project = resolve(import.meta.dirname, '..');
const errors = [];
const server = createServer(async (req, res) => {
try {
const path = new URL(req.url, 'http://localhost').pathname;
let content = await readFile(join(project, path));
if (path.endsWith('.html')) {
// 使用真实编辑页和页面控制器,仅替换网络、导航及富文本依赖。
content = content.toString().replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, (tag) =>
/(?:wangeditor5\/index|rich-editor|md5|attachment-editor|upload-pages|media-display|feed-pages|article-pages|video-pages|memo-pages|growth-pages|relative-pages|album-pages|ceremony-admin-pages)\.js/.test(tag) ? tag : '');
}
res.setHeader('Content-Type', { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.png': 'image/png' }[extname(path)] || 'application/octet-stream');
res.end(content);
} catch { res.writeHead(404); res.end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
const userData = await mkdtemp(join(tmpdir(), 'jiapu-attachment-test-'));
const chrome = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-gpu-sandbox', '--use-angle=swiftshader',
'--disable-features=Vulkan', '--remote-debugging-port=0', `--user-data-dir=${userData}`, 'about:blank'
], { windowsHide: true });
let socket;
try {
const endpoint = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Chrome 启动超时')), 15000);
chrome.stderr.on('data', chunk => {
const match = chunk.toString().match(/DevTools listening on (ws:\/\/\S+)/);
if (match) { clearTimeout(timer); resolve(match[1]); }
});
chrome.on('error', reject);
});
const tabs = await (await fetch(endpoint.replace('ws:', 'http:').replace(/\/devtools\/browser\/.*/, '/json'))).json();
socket = new WebSocket(tabs.find(tab => tab.type === 'page').webSocketDebuggerUrl);
await new Promise(resolve => socket.addEventListener('open', resolve, { once: true }));
const pending = new Map();
let id = 0;
socket.addEventListener('message', event => {
const message = JSON.parse(event.data);
if (message.method === 'Runtime.exceptionThrown') errors.push(message.params.exceptionDetails);
if (pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id); }
});
async function send(method, params = {}) {
const requestId = ++id;
const promise = new Promise(resolve => pending.set(requestId, resolve));
socket.send(JSON.stringify({ id: requestId, method, params }));
const response = await promise;
if (response.error) throw new Error(JSON.stringify(response.error));
return response.result;
}
async function evaluate(expression) {
const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
return result.result.value;
}
await send('Runtime.enable');
await send('Page.enable');
await send('Page.addScriptToEvaluateOnNewDocument', { source: `
window.saved = []; window.alert = function () {}; window.NavigationUtil = { open: function () {} };
window.fixtureFiles = [
{ ossId: '2062179707935264771', fileName: '原照片.png', mediaType: 'image/png', accessUrl: '${origin}/public/images/logo-mark.png' },
{ ossId: '2062179707935264772', fileName: '说明.pdf', mediaType: 'application/pdf', accessUrl: '${origin}/document.pdf' }
];
const record = { genealogyId: '2', feedId: '3', feedContent: '原动态', feedType: 'text',
articleId: '3', articleTitle: '谱文', articleContent: '正文',
videoId: '3', videoTitle: '影像', durationSeconds: 5,
memoId: '3', memoTitle: '备忘', memoContent: '正文', completed: '0',
recordId: '3', recordTitle: '成长', recordContent: '正文', recordType: 'other',
relativeId: '3', relativeName: '亲友', ceremonyId: '3', ceremonyTitle: '活动', ceremonyType: 'other',
albumId: '3', albumName: '相册', status: '0', mediaFiles: fixtureFiles, coverFile: fixtureFiles[0],
videoFile: { ossId: '2062179707935264773', fileName: '原视频.mp4', mediaType: 'video/mp4', accessUrl: '${origin}/video.mp4' }
};
window.GenealogyApi = { defaultClient: {
getToken: () => 'mock', genealogyDetail: async () => ({ canEditContent: true, canManage: true }),
currentProfile: async () => ({ userId: '1' }),
feedDetail: async () => record, articleDetail: async () => record, videoDetail: async () => record,
memoDetail: async () => record, relativeRecordDetail: async () => record, growthRecordDetail: async () => record,
ceremonyDetail: async () => record, albums: async () => [record], lineagePersonOptions: async () => [],
updateFeed: async (g, f, body) => { saved.push(body); return record; },
updateArticle: async (g, f, body) => { saved.push(body); return record; },
updateVideo: async (g, f, body) => { saved.push(body); return record; },
initResumableUpload: async () => { await new Promise(r => setTimeout(r, 150)); return { instant: true, ossId: '2062179707935264774' }; }
} };
` });
async function open(page, query) {
await send('Page.navigate', { url: `${origin}/${page}?genealogyId=2&${query}=3` });
for (let attempt = 0; attempt < 100; attempt++) {
if (await evaluate('document.querySelectorAll(".attachment-edit-item").length > 0')) return;
await new Promise(resolve => setTimeout(resolve, 50));
}
throw new Error(page + ': 附件没有回显 ' + await evaluate('document.body.innerText'));
}
await open('profile-feed-edit.html', 'feedId');
assert.equal(await evaluate('AppRichEditor.init(document.querySelector("[name=feedContent]")).editor.getText().trim()'), '原动态', '编辑器应回显原正文');
assert.equal(await evaluate('document.querySelector("#feedMedia").value'), '2062179707935264771,2062179707935264772');
assert.equal(await evaluate('document.querySelectorAll(".attachment-edit-item img").length'), 1);
await evaluate('AppRichEditor.init(document.querySelector("[name=feedContent]")).editor.setHtml("<p>仅改文字</p>"); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved[0].mediaOssIds'), '2062179707935264771,2062179707935264772');
await evaluate('document.querySelector(".attachment-edit-remove").click()');
assert.equal(await evaluate('document.querySelector("#feedMedia").value'), '2062179707935264772');
await evaluate(`const input=document.querySelector('#feedMediaFile'); const transfer=new DataTransfer(); transfer.items.add(new File(['image'], '新增.png', {type:'image/png'})); input.files=transfer.files; input.dispatchEvent(new Event('change',{bubbles:true})); document.querySelector('form').requestSubmit();`);
assert.equal(await evaluate('saved.length'), 1, '上传中应阻止保存');
await evaluate('new Promise(r=>setTimeout(r,250))');
assert.equal(await evaluate('document.querySelector("#feedMedia").value'), '2062179707935264772,2062179707935264774');
await evaluate('document.querySelectorAll(".attachment-edit-remove").forEach(button=>button.click()); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.at(-1).mediaOssIds'), '');
await open('profile-article-edit.html', 'articleId');
await evaluate('document.querySelector(".attachment-edit-remove").click(); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.at(-1).coverOssId'), null);
await open('profile-video-edit.html', 'videoId');
assert.equal(await evaluate('document.querySelectorAll(".attachment-edit-item video").length'), 1);
assert.equal(await evaluate('document.querySelector("#video-oss-id").value'), '2062179707935264773');
await evaluate('document.querySelector("#video-cover-oss-id").closest(".editor-field").querySelector(".attachment-edit-remove").click(); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.at(-1).coverOssId'), null);
assert.equal(await evaluate('saved.at(-1).videoOssId'), '2062179707935264773');
await evaluate('document.querySelector(".attachment-edit-item video").parentElement.querySelector("button").click(); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.length'), 1, '必填视频移除后不能保存,必须先重新上传');
for (const [page, query] of [['memo','memoId'], ['growth','recordId'], ['relative','relativeId'], ['album','albumId'], ['gift','ceremonyId']]) {
await open('profile-' + page + '-edit.html', query);
if (page === 'album') assert.equal(await evaluate('document.querySelector(".attachment-edit-remove").hidden'), true);
if (['memo', 'growth', 'relative'].includes(page)) {
await evaluate(`document.querySelector('[data-${page}-clear-media]').click()`);
assert.equal(await evaluate('document.querySelector("[name=mediaOssIds]").value'), '');
assert.equal(await evaluate('document.querySelectorAll(".attachment-edit-item").length'), 0);
}
}
await send('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
await open('profile-feed-edit.html', 'feedId');
assert.equal(await evaluate('document.documentElement.scrollWidth <= innerWidth'), true, '移动端不能横向溢出');
await evaluate(`try { AttachmentEditor.setFiles('#feedMedia', [{fileName:'缺少标识.png'}]); } catch {} document.querySelector('form').requestSubmit();`);
assert.equal(await evaluate('saved.length'), 0, '原附件信息不完整时应阻止保存');
assert.deepEqual(errors, []);
console.log('PASS: 编辑页原附件回显、逐项移除、追加上传、上传中阻止保存、全部移除、封面 null、视频预览与移动端布局(模拟 API)');
} finally {
if (socket) socket.close();
chrome.kill();
server.close();
await new Promise(resolve => chrome.exitCode !== null ? resolve() : chrome.once('exit', resolve));
if (dirname(userData) === resolve(tmpdir()) && basename(userData).startsWith('jiapu-attachment-test-')) {
await rm(userData, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
}