import { test, expect, type Page } from '@playwright/test'; import { createServer, type ViteDevServer } from 'vite'; import { resolve } from 'node:path'; let server: ViteDevServer; let baseUrl: string; test.use({ channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' }); test.beforeAll(async () => { server = await createServer({ configFile: false, cacheDir: 'node_modules/.vite-preview-layout', resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' }, server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'isolated-preview-layout', enforce: 'pre', load(id) { if (id.endsWith('/src/lib/host-events.ts')) return 'export function subscribeHostEvent() { return () => {}; }'; if (id.endsWith('/src/lib/host-api.ts')) return ` const browser = { browserId: 'fixture', projectId: 'layout-fixture', state: 'attached', generation: 1, url: 'http://127.0.0.1:4173/', visible: true, bounds: null, eventCursor: 0 }; export async function hostApiFetch(path) { return path.includes('ensure-work') ? { status: 'ready', browser } : { success: true, browser }; } `; }, configureServer(vite) { vite.middlewares.use((req, res, next) => { if (req.url !== '/') return next(); res.setHeader('Content-Type', 'text/html'); res.end('
'); }); }, }], }); await server.listen(); baseUrl = server.resolvedUrls!.local[0]; }); test.afterAll(async () => { await server?.close(); }); async function geometry(page: Page) { return page.evaluate(() => { const box = (selector: string) => document.querySelector(selector)!.getBoundingClientRect(); const bubble = box('[data-testid="teacher-presence"]'); const viewport = box('[data-testid="agent-browser-viewport"]'); const panel = box('[data-testid="agent-browser-panel"]'); const work = box('[data-testid="work-view"]'); return { gap: panel.top - work.top, clearance: Number.parseFloat((document.querySelector('[data-testid="agent-browser-panel"]') as HTMLElement).style.marginTop) || 0, nativeGap: viewport.top - bubble.bottom, sideBySide: bubble.left >= viewport.right || bubble.right <= viewport.left }; }); } for (const size of [{ width: 1440, height: 900 }, { width: 1180, height: 720 }, { width: 1000, height: 640 }]) { test(`preview only clears overlapping speech at ${size.width}x${size.height}`, async ({ page }) => { await page.setViewportSize(size); const errors: string[] = []; page.on('pageerror', (error) => errors.push(error.message)); await page.goto(baseUrl); await expect(page.getByTestId('teacher-invitation')).toBeVisible(); await expect.poll(async () => (await geometry(page)).nativeGap).toBeGreaterThanOrEqual(8); expect((await geometry(page)).gap).toBeLessThan(65); await page.getByRole('button', { name: '老师', exact: true }).click(); await expect.poll(async () => (await geometry(page)).sideBySide).toBe(true); await expect.poll(async () => (await geometry(page)).gap).toBe(20); // A narrow dock can overlap again; increasing its width must give the // preview all its space back without hiding the unread message. await page.getByRole('slider', { name: '老师栏宽度' }).press('Home'); await expect.poll(async () => { const g = await geometry(page); return g.sideBySide || g.nativeGap >= 8; }).toBe(true); await page.getByRole('slider', { name: '老师栏宽度' }).press('End'); await expect.poll(async () => (await geometry(page)).gap).toBe(20); await page.screenshot({ path: test.info().outputPath('work-with-teacher-no-gap.png') }); await page.getByRole('button', { name: '关闭老师栏' }).click(); await expect.poll(async () => (await geometry(page)).nativeGap).toBeGreaterThanOrEqual(8); await page.getByRole('button', { name: '切换消息长度' }).click(); await expect.poll(async () => (await geometry(page)).gap).toBe(20); await page.getByRole('button', { name: '等会儿聊' }).click(); await expect(page.getByTestId('teacher-invitation')).toHaveCount(0); await expect(page.getByTestId('agent-browser-panel')).toHaveCSS('margin-top', '0px'); expect(errors).toEqual([]); }); }