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(); } }