Files
WebDrop/scripts/browser-chat-files-smoke.js

63 lines
6.7 KiB
JavaScript

async (page) => {
const origin = new URL(page.url()).origin; const browser = page.context().browser();
const contexts = [await browser.newContext({ viewport: { width: 1440, height: 900 } }), await browser.newContext({ viewport: { width: 1440, height: 900 } })];
for (const context of contexts) context.setDefaultTimeout(10000);
const sender = await contexts[0].newPage(); const viewer = await contexts[1].newPage();
const stamp = Date.now(); const alice = `uploader_${stamp}`; const bob = `reader_${stamp}`;
async function register(target, username, name) {
await target.goto(origin); await target.getByLabel('用户名', { exact: true }).fill(username); await target.getByLabel('密码', { exact: true }).fill('Chat-files-smoke-password-2026');
await target.getByRole('button', { name: '创建账号', exact: true }).click(); await target.getByRole('button', { name: '新建空间', exact: true }).waitFor();
await target.getByLabel('个人名称', { exact: true }).fill(name);
const avatar = await target.evaluate(() => { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256;
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#447788'; ctx.fillRect(0, 0, 256, 256); return canvas.toDataURL('image/png').split(',')[1]; });
await target.getByLabel('头像', { exact: true }).setInputFiles({ name: 'profile.png', mimeType: 'image/png', buffer: Buffer.from(avatar, 'base64') });
await target.getByRole('img', { name: '个人头像', exact: true }).waitFor(); await target.getByRole('button', { name: '保存个人资料', exact: true }).click();
await target.getByText('个人资料已保存并同步。', { exact: true }).waitFor();
}
const results = [];
try {
await register(sender, alice, '小白'); await register(viewer, bob, '小黑');
await sender.getByRole('button', { name: '新建空间', exact: true }).click(); await sender.getByLabel('空间名称(加入者和服务端可见)').fill('文件聊天验收');
await sender.getByRole('button', { name: '创建', exact: true }).click(); await sender.getByRole('button', { name: '常规', exact: true }).waitFor();
const path = new URL(sender.url()).pathname;
await sender.getByRole('region', { name: '常规 聊天', exact: true }).locator('textarea').fill('你好,这是一份项目文件。');
await sender.getByRole('button', { name: '发送消息', exact: true }).click();
await sender.getByRole('region', { name: '频道文件管理器' }).locator('input[type=file]').setInputFiles({ name: 'project-notes.txt', mimeType: 'text/plain', buffer: Buffer.from('project-content\n'.repeat(140000)) });
await sender.getByRole('button', { name: '继续上传', exact: true }).click(); await sender.getByRole('article', { name: '小白 上传了 project-notes.txt', exact: true }).waitFor();
await sender.getByRole('button', { name: '空间设置', exact: true }).click(); await sender.getByLabel('被邀请用户的用户名').fill(bob);
await sender.getByRole('button', { name: '发送邀请', exact: true }).click(); await sender.getByText(`${bob} · 已邀请`, { exact: true }).waitFor();
await sender.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).click();
let downloads = 0; viewer.on('request', (r) => { if (/\/items\/[^/]+\/frames/.test(r.url())) downloads++; });
await viewer.goto(origin + path); await viewer.getByRole('button', { name: '接受并加入空间', exact: true }).click();
const file = viewer.getByRole('article', { name: '小白 上传了 project-notes.txt', exact: true }); await file.waitFor();
await file.getByRole('img', { name: '小白 的头像', exact: true }).waitFor();
const avatar = await file.getByRole('img').evaluate((img) => ({ width: img.naturalWidth, source: img.src.length }));
if (avatar.width !== 64 || avatar.source > 8000) throw new Error('Avatar was not safely resized');
await viewer.getByRole('article', { name: '小白 的消息', exact: true }).getByText('你好,这是一份项目文件。', { exact: true }).waitFor();
if (downloads !== 0) throw new Error('Displaying a file message consumed a download');
results.push('file and text bubbles show the uploader name and bounded avatar on another account');
await file.getByRole('button', { name: '预览', exact: true }).click(); await viewer.getByRole('button', { name: '解密并预览文件', exact: true }).click();
await viewer.getByRole('dialog').locator('pre').filter({ hasText: 'project-content' }).waitFor(); await viewer.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).click();
if (downloads !== 1) throw new Error('Explicit file preview did not use exactly one download');
await file.getByRole('img', { name: '小白 的头像', exact: true }).waitFor();
await viewer.reload(); await viewer.getByRole('article', { name: '小白 上传了 project-notes.txt', exact: true }).getByRole('img', { name: '小白 的头像', exact: true }).waitFor();
if (downloads !== 1) throw new Error('Reloading file bubbles consumed a download');
results.push('server-owned identity survives download updates and reload without automatic content reads');
await viewer.getByRole('region', { name: '常规 聊天', exact: true }).locator('textarea').fill('我收到了。'); await viewer.getByRole('button', { name: '发送消息', exact: true }).click();
await sender.getByRole('article', { name: '小黑 的消息', exact: true }).getByText('我收到了。', { exact: true }).waitFor();
results.push('each participant keeps their own sender identity');
await viewer.screenshot({ path: 'C:/Users/JustMe/Desktop/webdrop-proj/.playwright-mcp/chat-file-messages.png', fullPage: true });
for (const width of [1440, 390]) { await viewer.setViewportSize({ width, height: 900 }); for (const colorScheme of ['light', 'dark']) {
await viewer.emulateMedia({ colorScheme });
// Allow the media-query subscription to switch the React workspace layout.
await viewer.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
if (await viewer.evaluate(() => document.documentElement.scrollWidth > innerWidth)) {
const overflow = await viewer.evaluate(() => ({ width: innerWidth, scroll: document.documentElement.scrollWidth,
elements: [...document.querySelectorAll('body *')].filter(e => e.getBoundingClientRect().right > innerWidth + 1).slice(0, 12).map(e => ({ tag: e.tagName, cls: e.className, right: e.getBoundingClientRect().right })) }));
throw new Error(`Chat layout overflow: ${JSON.stringify(overflow)}`);
}
} }
return { passed: results };
} finally { for (const context of contexts) await context.close(); }
}