test(web): cover workspace and file interaction workflows
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

This commit is contained in:
justme committed 2026-10-13 01:39:26 +08:00
1 parent c2ea1b7f64
commit 937b774a93
4 files changed
+337

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
// Run with the Playwright browser_run_code tool's filename option against an isolated development API.
async (page) => {
const origin = new URL(page.url()).origin;
const password = 'Architecture-test-password-2026';
const suffix = Date.now().toString();
const ownerName = `smoke_owner_${suffix}`;
const memberName = `smoke_member_${suffix}`;
const browser = page.context().browser();
const contexts = [];
const results = [];
async function login(target, username, create = false, path = '/') {
await target.goto(origin + path);
if (!create) await target.getByRole('button', { name: '已有账号', exact: true }).click();
await target.getByLabel('用户名', { exact: true }).fill(username);
await target.getByLabel('密码', { exact: true }).fill(password);
await target.getByRole('button', { name: create ? '创建账号' : '登录并同步', exact: true }).click();
await target.getByRole('button', { name: '新建空间', exact: true }).waitFor();
}
async function newDevice() {
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
contexts.push(context); return context.newPage();
}
try {
const owner = await newDevice();
await login(owner, ownerName, true);
await owner.getByLabel('个人名称', { exact: true }).fill('多设备测试用户');
const avatar = await owner.evaluate(() => {
const canvas = document.createElement('canvas'); canvas.width = 32; canvas.height = 32;
const context = canvas.getContext('2d'); context.fillRect(0, 0, 32, 32); return canvas.toDataURL('image/png').split(',')[1];
});
await owner.getByLabel('头像', { exact: true }).setInputFiles({ name: 'avatar.png', mimeType: 'image/png', buffer: Buffer.from(avatar, 'base64') });
await owner.getByRole('img', { name: '个人头像', exact: true }).waitFor();
await owner.getByRole('button', { name: '保存个人资料', exact: true }).click();
await owner.getByText('个人资料已保存并同步。', { exact: true }).waitFor();
await owner.reload();
await owner.getByRole('button', { name: '新建空间', exact: true }).waitFor();
if (await owner.getByLabel('个人名称', { exact: true }).inputValue() !== '多设备测试用户') throw new Error('Profile did not survive reload');
await owner.getByRole('img', { name: '个人头像', exact: true }).waitFor();
results.push('login, encrypted profile and avatar survive reload');
await owner.getByRole('button', { name: '新建空间', exact: true }).click();
await owner.getByLabel('空间名称(加入者和服务端可见)').fill('架构验收空间');
await owner.getByRole('button', { name: '创建', exact: true }).click();
await owner.getByRole('button', { name: '常规', exact: true }).waitFor();
await owner.getByRole('button', { name: '新建频道分组', exact: true }).click();
await owner.getByLabel('分组名称', { exact: true }).fill('工作');
await owner.getByRole('button', { name: '创建', exact: true }).click();
await owner.getByRole('button', { name: '在 工作 新建频道', exact: true }).click();
await owner.getByLabel('频道名称', { exact: true }).fill('讨论');
await owner.getByRole('button', { name: '创建', exact: true }).click();
await owner.getByRole('button', { name: '讨论', exact: true }).waitFor();
await owner.getByRole('button', { name: '常规', exact: true }).click();
await owner.getByRole('button', { name: '频道设置 常规', exact: true }).click();
await owner.getByLabel('所属分组').selectOption({ label: '工作' });
await owner.getByRole('button', { name: '保存', exact: true }).click();
await owner.getByRole('button', { name: '重命名分组 工作', exact: true }).click();
await owner.getByLabel('分组名称', { exact: true }).fill('项目');
await owner.getByRole('button', { name: '保存', exact: true }).click();
await owner.getByRole('button', { name: '分组 项目', exact: true }).click();
if (await owner.getByRole('button', { name: '讨论', exact: true }).isVisible()) throw new Error('Group did not collapse');
await owner.getByRole('button', { name: '分组 项目', exact: true }).click();
await owner.reload();
await owner.getByRole('region', { name: '频道分组 项目', exact: true }).getByRole('button', { name: '常规', exact: true }).waitFor();
await owner.getByRole('region', { name: '频道分组 项目', exact: true }).getByRole('button', { name: '讨论', exact: true }).waitFor();
await owner.getByRole('button', { name: '讨论', exact: true }).click();
await owner.getByRole('button', { name: '频道设置 讨论', exact: true }).click();
await owner.getByLabel('所属分组').selectOption({ label: '未分组' });
await owner.getByRole('button', { name: '保存', exact: true }).click();
if (await owner.getByRole('region', { name: '频道分组 项目', exact: true }).getByRole('button', { name: '讨论', exact: true }).count()) throw new Error('Ungrouping did not remove group membership');
await owner.getByRole('button', { name: '频道设置 讨论', exact: true }).click();
await owner.getByLabel('所属分组').selectOption({ label: '项目' });
await owner.getByRole('button', { name: '保存', exact: true }).click();
await owner.getByRole('button', { name: '常规', exact: true }).click();
results.push('channel groups create, rename, collapse and survive encrypted replay');
const path = new URL(owner.url()).pathname;
if (owner.url().includes('#')) throw new Error('Account channel navigation retained a secret fragment');
await owner.locator('textarea').last().fill('常规 thread 聊天验证');
await owner.getByRole('button', { name: '发送消息', exact: true }).click();
await owner.locator('article p').filter({ hasText: '常规 thread 聊天验证' }).waitFor();
const payload = '端到端文件验证:只有成员设备可解密。'.repeat(60000);
await owner.getByRole('region', { name: '频道文件管理器' }).locator('input[type=file]').setInputFiles({ name: 'architecture-file.txt', mimeType: 'text/plain', buffer: Buffer.from(payload) });
await owner.getByRole('button', { name: '继续上传', exact: true }).click();
await owner.getByRole('button', { name: 'architecture-file.txt', exact: true }).waitFor();
results.push('default thread, center chat and right file explorer work');
const second = await newDevice();
await login(second, ownerName, false, path);
await second.getByRole('button', { name: 'architecture-file.txt', exact: true }).waitFor();
await second.locator('article p').filter({ hasText: '常规 thread 聊天验证' }).waitFor();
await second.getByRole('region', { name: '频道分组 项目', exact: true }).getByRole('button', { name: '讨论', exact: true }).waitFor();
results.push('second independent device recovers owner channel without a secret link');
const member = await newDevice();
await login(member, memberName, true);
await owner.getByRole('button', { name: '空间设置', exact: true }).click();
await owner.getByLabel('被邀请用户的用户名').fill(memberName);
await owner.getByRole('button', { name: '发送邀请', exact: true }).click();
await owner.getByText(`${memberName} · 已邀请`, { exact: true }).waitFor();
await owner.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).click();
let downloads = 0;
member.on('request', (request) => { if (/\/items\/[^/]+\/frames/.test(request.url())) downloads++; });
await member.goto(origin + path);
await member.getByRole('button', { name: '接受并加入空间', exact: true }).click();
await member.getByRole('button', { name: 'architecture-file.txt', exact: true }).waitFor();
if (downloads !== 0) throw new Error('File catalog consumed a gated download');
await member.getByRole('button', { name: 'architecture-file.txt', exact: true }).dblclick();
await member.getByRole('button', { name: '解密并预览文件', exact: true }).click();
await member.locator('pre').filter({ hasText: '端到端文件验证' }).waitFor();
if (downloads !== 1) throw new Error(`Expected one explicit gated read, got ${downloads}`);
await member.reload();
await member.getByRole('button', { name: 'architecture-file.txt', exact: true }).waitFor();
if (downloads !== 1) throw new Error('Refreshing consumed another download');
results.push('account invitation automatically unlocks ciphertext and catalog never auto-downloads');
await owner.getByRole('button', { name: '新建文件夹', exact: true }).click();
await owner.getByLabel('文件夹名称', { exact: true }).fill('资料目录');
await owner.getByRole('button', { name: '创建', exact: true }).click();
await owner.getByRole('button', { name: '资料目录', exact: true }).waitFor();
await owner.getByRole('checkbox', { name: '选择文件 architecture-file.txt', exact: true }).check();
await owner.getByLabel('移动所选文件到文件夹').selectOption({ label: '资料目录' });
await owner.getByRole('button', { name: '资料目录', exact: true }).dblclick();
await owner.getByRole('button', { name: 'architecture-file.txt', exact: true }).waitFor();
results.push('folder navigation and encrypted file moves work');
await second.goto(origin);
const selfRow = second.getByRole('region', { name: '账号设备' }).locator('div').filter({ has: second.getByText('本机', { exact: true }) });
await selfRow.locator('input').fill('验收副设备');
await selfRow.getByRole('button', { name: '重命名', exact: true }).click();
await second.getByLabel('验收副设备 的设备名称', { exact: true }).waitFor();
await owner.goto(origin);
const otherRow = owner.getByRole('region', { name: '账号设备' }).locator('div').filter({ has: owner.getByLabel('验收副设备 的设备名称', { exact: true }) });
await owner.evaluate(() => { window.confirm = () => true; });
await otherRow.getByRole('button', { name: '移除设备', exact: true }).click();
await second.reload();
await second.getByRole('button', { name: '创建账号', exact: true }).waitFor();
await owner.reload();
await owner.getByRole('button', { name: '新建空间', exact: true }).waitFor();
results.push('revoked device loses remembered login while the other device stays logged in');
await member.goto(origin + path);
for (const width of [1440, 390]) {
await member.setViewportSize({ width, height: 900 });
for (const colorScheme of ['light', 'dark']) {
await member.emulateMedia({ colorScheme });
if (await member.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error(`Horizontal overflow at ${width}/${colorScheme}`);
}
}
results.push('desktop/mobile and light/dark layouts fit viewport');
return { passed: results };
} finally { for (const context of contexts) await context.close(); }
}
+55
View File
@@ -0,0 +1,55 @@
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('identity survives download updates and encrypted history replay 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 }); if (await viewer.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error('Chat layout overflow');
} }
return { passed: results };
} finally { for (const context of contexts) await context.close(); }
}
+99
View File
@@ -0,0 +1,99 @@
async (page) => {
const origin = new URL(page.url()).origin;
const browser = page.context().browser();
const firstContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const secondContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
firstContext.setDefaultTimeout(10000); secondContext.setDefaultTimeout(10000);
const owner = await firstContext.newPage(); const device = await secondContext.newPage();
const username = `explorer_${Date.now()}`; const password = 'Explorer-smoke-password-2026';
const results = [];
try {
await owner.goto(origin); await owner.getByLabel('用户名', { exact: true }).fill(username); await owner.getByLabel('密码', { exact: true }).fill(password);
await owner.getByRole('button', { name: '创建账号', exact: true }).click(); await owner.getByRole('button', { name: '新建空间', exact: true }).click();
await owner.getByLabel('空间名称(加入者和服务端可见)').fill('文件交互验收'); await owner.getByRole('button', { name: '创建', exact: true }).click();
await owner.getByRole('button', { name: '常规', exact: true }).waitFor();
const path = new URL(owner.url()).pathname;
const png = await owner.evaluate(() => { const c = document.createElement('canvas'); c.width = 32; c.height = 32; c.getContext('2d').fillRect(0, 0, 32, 32); return c.toDataURL('image/png').split(',')[1]; });
const explorer = owner.getByRole('region', { name: '频道文件管理器' });
await explorer.locator('input[type=file]').setInputFiles([
{ name: 'a-note.txt', mimeType: 'text/plain', buffer: Buffer.from('hover-alpha') },
{ name: 'b-note.txt', mimeType: 'text/plain', buffer: Buffer.from('hover-beta') },
{ name: 'c-image.png', mimeType: 'image/png', buffer: Buffer.from(png, 'base64') },
{ name: 'z-locked.bin', mimeType: 'application/octet-stream', buffer: Buffer.alloc(2 * 1024 * 1024, 7) },
]);
await owner.getByRole('button', { name: '继续上传', exact: true }).click(); await owner.getByRole('button', { name: 'z-locked.bin', exact: true }).waitFor();
await owner.getByRole('button', { name: '新建文件夹', exact: true }).click(); await owner.getByLabel('文件夹名称', { exact: true }).fill('资料');
await owner.getByRole('button', { name: '创建', exact: true }).click(); await owner.getByRole('button', { name: '资料', exact: true }).waitFor();
await device.goto(origin + path); await device.getByRole('button', { name: '已有账号', exact: true }).click();
await device.getByLabel('用户名', { exact: true }).fill(username); await device.getByLabel('密码', { exact: true }).fill(password);
await device.getByRole('button', { name: '登录并同步', exact: true }).click(); await device.getByRole('button', { name: 'z-locked.bin', exact: true }).waitFor();
let reads = 0; device.on('request', (r) => { if (/\/items\/[^/]+\/frames/.test(r.url())) reads++; });
await device.getByRole('button', { name: 'z-locked.bin', exact: true }).hover(); await device.getByRole('tooltip', { name: '悬浮文件预览' }).waitFor();
if (reads) throw new Error('Hover downloaded a gated file');
await device.getByRole('button', { name: 'a-note.txt', exact: true }).hover();
await device.getByRole('tooltip').locator('pre').filter({ hasText: 'hover-alpha' }).waitFor();
await device.getByRole('button', { name: 'c-image.png', exact: true }).hover(); await device.getByRole('img', { name: '文件缩略预览' }).waitFor();
if (reads) throw new Error('Hover unexpectedly fetched file frames'); results.push('hover previews synced text/images and never reads gated content');
const area = device.getByRole('region', { name: '频道文件管理器' });
await device.getByRole('button', { name: 'a-note.txt', exact: true }).click();
await device.getByRole('button', { name: 'b-note.txt', exact: true }).click({ modifiers: ['Control'] });
if (await area.locator('input[type=checkbox]:checked').count() !== 2) throw new Error('Ctrl multi-select failed');
await device.getByRole('button', { name: 'z-locked.bin', exact: true }).click({ modifiers: ['Shift'] });
if (await area.locator('input[type=checkbox]:checked').count() !== 3) throw new Error('Shift range selection failed');
await device.keyboard.press('Control+a'); if (await area.locator('input[type=checkbox]:checked').count() !== 4) throw new Error('Select all failed');
await device.keyboard.press('Escape'); if (await area.locator('input[type=checkbox]:checked').count()) throw new Error('Escape did not clear selection');
results.push('left click, Ctrl/Shift ranges and keyboard selection work');
await device.getByRole('button', { name: 'a-note.txt', exact: true }).click({ button: 'right' });
await device.getByRole('menu', { name: '文件操作菜单' }).waitFor(); await device.keyboard.press('ArrowDown'); await device.keyboard.press('Escape');
await device.getByRole('button', { name: '文件操作 a-note.txt', exact: true }).click(); await device.getByRole('menuitem', { name: '预览', exact: true }).click();
await device.getByRole('dialog').locator('pre').filter({ hasText: 'hover-alpha' }).waitFor(); await device.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).click();
results.push('right click, accessible more-menu and explicit preview work');
const row = (name) => device.getByRole('button', { name, exact: true }).locator('xpath=ancestor::tr');
async function drag(source, target) {
await source.locator('input[type=checkbox]').check();
const start = await source.boundingBox();
await device.mouse.move(start.x + 80, start.y + start.height / 2); await device.mouse.down();
await device.mouse.move(start.x + 95, start.y + start.height / 2, { steps: 4 });
const end = await target.boundingBox();
await device.mouse.move(end.x + end.width / 2, end.y + end.height / 2, { steps: 12 });
await device.mouse.move(end.x + end.width / 2 + 1, end.y + end.height / 2, { steps: 2 }); await device.mouse.up();
}
await row('a-note.txt').dragTo(row('资料'));
await device.getByRole('button', { name: 'a-note.txt', exact: true }).waitFor({ state: 'hidden' });
await device.getByRole('button', { name: '资料', exact: true }).dblclick(); await device.getByRole('button', { name: 'a-note.txt', exact: true }).waitFor();
await drag(row('a-note.txt'), area.locator('button[data-drop-folder="root"]'));
await device.getByRole('button', { name: 'a-note.txt', exact: true }).waitFor({ state: 'hidden', timeout: 10000 }); await area.getByRole('button', { name: '根目录', exact: true }).click();
await device.getByRole('button', { name: 'a-note.txt', exact: true }).waitFor();
results.push('drag moves files into folders and breadcrumb drops return to root');
const transfer = await device.evaluateHandle(() => { const value = new DataTransfer(); value.items.add(new File(['external drop'], 'external-note.txt', { type: 'text/plain' })); return value; });
await row('资料').dispatchEvent('dragover', { dataTransfer: transfer }); await row('资料').dispatchEvent('drop', { dataTransfer: transfer });
await device.getByRole('button', { name: '继续上传', exact: true }).click(); await device.getByRole('button', { name: '资料', exact: true }).dblclick();
await device.getByRole('button', { name: 'external-note.txt', exact: true }).waitFor(); await area.getByRole('button', { name: '根目录', exact: true }).click(); await transfer.dispose();
results.push('external file drops upload directly into the target folder');
await device.getByRole('button', { name: 'a-note.txt', exact: true }).click(); await device.getByRole('button', { name: 'b-note.txt', exact: true }).click({ modifiers: ['Control'] });
await row('a-note.txt').dragTo(row('资料')); await device.getByRole('button', { name: 'b-note.txt', exact: true }).waitFor({ state: 'hidden' });
await device.getByRole('button', { name: '资料', exact: true }).dblclick(); await device.getByRole('button', { name: 'b-note.txt', exact: true }).waitFor();
await device.getByRole('button', { name: 'a-note.txt', exact: true }).click(); await device.getByRole('button', { name: 'b-note.txt', exact: true }).click({ modifiers: ['Control'] });
await device.getByRole('button', { name: 'a-note.txt', exact: true }).click({ button: 'right' }); await device.getByRole('menuitem', { name: '移动到文件夹…', exact: true }).click();
await device.getByRole('button', { name: '移动', exact: true }).click(); await area.getByRole('button', { name: '根目录', exact: true }).click();
await device.getByRole('button', { name: 'a-note.txt', exact: true }).waitFor(); await device.getByRole('button', { name: 'b-note.txt', exact: true }).waitFor();
if (reads) throw new Error('Moving files read file contents'); results.push('batch drag and move dialog preserve encrypted content without downloading');
const handle = device.getByRole('separator', { name: '调整文件区宽度', exact: true }); const box = await handle.boundingBox();
const before = (await area.boundingBox()).width; await device.mouse.move(box.x + 3, box.y + 50); await device.mouse.down(); await device.mouse.move(box.x - 80, box.y + 50); await device.mouse.up();
const wider = (await area.boundingBox()).width; if (wider < before + 60) throw new Error('File panel did not resize');
await handle.focus(); await device.keyboard.press('ArrowRight'); if ((await area.boundingBox()).width >= wider) throw new Error('Keyboard resize failed');
results.push('pointer and keyboard resize the file pane');
await device.evaluate(() => { window.confirm = () => true; });
await device.getByRole('button', { name: 'a-note.txt', exact: true }).click(); await device.getByRole('button', { name: '文件操作 a-note.txt', exact: true }).click();
await device.getByRole('menuitem', { name: '删除', exact: true }).click(); await device.getByRole('button', { name: 'a-note.txt', exact: true }).waitFor({ state: 'hidden' });
await device.reload(); await device.getByRole('button', { name: 'b-note.txt', exact: true }).waitFor();
if (await device.getByRole('button', { name: 'a-note.txt', exact: true }).count()) throw new Error('Deleted file returned after replay');
results.push('context deletion persists after reload');
for (const width of [1440, 390]) {
await device.setViewportSize({ width, height: 900 });
if (width < 900) await device.getByRole('button', { name: '文件', exact: true }).click();
for (const colorScheme of ['light', 'dark']) { await device.emulateMedia({ colorScheme }); if (await device.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error('Explorer overflow'); }
}
return { passed: results };
} finally { await firstContext.close(); await secondContext.close(); }
}
+38
View File
@@ -0,0 +1,38 @@
async (page) => {
const origin = new URL(page.url()).origin; const browser = page.context().browser(); const context = await browser.newContext(); context.setDefaultTimeout(10000);
let target = await context.newPage(); const username = `remember_${Date.now()}`; const password = 'Remember-login-smoke-password'; const results = [];
async function stored() {
return target.evaluate(async () => {
const db = await new Promise((resolve, reject) => { const req = indexedDB.open('webdrop-account-local-2'); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); });
const tx = db.transaction(['sessions', 'credentials'], 'readonly');
const read = (name) => new Promise((resolve, reject) => { const req = tx.objectStore(name).getAll(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); });
const [sessions, credentials] = await Promise.all([read('sessions'), read('credentials')]); db.close();
return { sessions: sessions.length, credentials: credentials.length, plaintext: credentials.some((value) => 'password' in value || 'username' in value), exportable: credentials.some((value) => value.key.extractable) };
});
}
async function reopen() { await target.close(); target = await context.newPage(); await target.goto(origin); }
try {
await target.goto(origin); await target.getByLabel('用户名', { exact: true }).fill(username); await target.getByLabel('密码', { exact: true }).fill(password);
await target.getByRole('button', { name: '创建账号', exact: true }).click(); await target.getByRole('button', { name: '退出登录', exact: true }).waitFor();
const record = await stored(); if (record.credentials !== 1 || record.plaintext || record.exportable) throw new Error('Login was not stored as non-exportable encrypted data');
await reopen(); await target.getByRole('button', { name: '退出登录', exact: true }).waitFor(); results.push('fresh page restores the remembered account');
await target.getByRole('button', { name: '退出登录', exact: true }).click(); await target.getByRole('button', { name: '登录并同步', exact: true }).waitFor();
if (await target.getByLabel('用户名', { exact: true }).inputValue() !== username || await target.getByLabel('密码', { exact: true }).inputValue() !== password) throw new Error('Saved account/password were not filled');
results.push('normal logout preserves encrypted credentials and fills the login form');
await target.getByRole('checkbox', { name: '记住账号和密码(本机)', exact: true }).uncheck(); await target.getByRole('button', { name: '登录并同步', exact: true }).click();
await target.getByRole('button', { name: '退出登录', exact: true }).waitFor(); await target.getByRole('button', { name: '立即同步', exact: true }).click();
await target.getByText('资源保险库已同步。', { exact: true }).waitFor();
const temporary = await stored(); if (temporary.sessions || temporary.credentials) throw new Error('Unremembered login was persisted by sync');
await reopen(); await target.getByLabel('用户名', { exact: true }).waitFor();
if (await target.getByLabel('用户名', { exact: true }).inputValue() || await target.getByLabel('密码', { exact: true }).inputValue()) throw new Error('Temporary login reappeared');
results.push('unchecked remember only keeps the current page session');
await target.getByRole('button', { name: '已有账号', exact: true }).click(); await target.getByLabel('用户名', { exact: true }).fill(username); await target.getByLabel('密码', { exact: true }).fill(password);
await target.getByRole('button', { name: '登录并同步', exact: true }).click(); await target.getByRole('button', { name: '清除本机保存', exact: true }).click();
await target.getByRole('button', { name: '立即同步', exact: true }).click(); await target.getByText('资源保险库已同步。', { exact: true }).waitFor();
const cleared = await stored(); if (cleared.sessions || cleared.credentials) throw new Error('Cleared login was restored by sync');
await reopen(); await target.getByLabel('密码', { exact: true }).waitFor();
if (await target.getByLabel('密码', { exact: true }).inputValue()) throw new Error('Password returned after clearing');
results.push('clearing local saves survives synchronization and page restart');
return { passed: results };
} finally { await context.close(); }
}