test(web): cover workspace and file interaction workflows
This commit is contained in:
1 parent
c2ea1b7f64
commit
937b774a93
4 files changed
+337
No files matched your search
@@ -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(); }
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
Reference in new issue
Block a user