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