150 lines
10 KiB
JavaScript
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 });
|
|
}
|
|
}
|