import { test, expect } from '@playwright/test'; import { createServer, type ViteDevServer } from 'vite'; import { resolve } from 'node:path'; let server: ViteDevServer; let baseUrl: string; test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' }); test.beforeAll(async () => { server = await createServer({ configFile: false, cacheDir: 'node_modules/.vite-community-layout', resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' }, server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'isolated-community', 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 ` export async function createHostEventSource() { return { close() {}, addEventListener() {} }; } export async function ensureHostApiToken() { return 'fixture'; } export function getHostApiBase() { return ''; } export function reportDesktopActivity() {} export function setDesktopBackgroundLease() {} export async function hostApiFetch() { return {}; } `; }, configureServer(vite) { vite.middlewares.use((req, res, next) => { if (req.url !== '/' && !req.url?.startsWith('/?')) return next(); res.setHeader('Content-Type', 'text/html'); res.end('
'); }); }, }], }); await server.listen(); baseUrl = server.resolvedUrls!.local[0]; }); test.afterAll(async () => { await server?.close(); }); test.beforeEach(async ({ page }) => { page.on('pageerror', error => { throw error; }); await page.addInitScript(() => { const scope = window as typeof window & { __communityCalls: unknown[][] }; scope.__communityCalls = []; Object.defineProperty(window, 'electron', { value: { ipcRenderer: { invoke: async (...args: unknown[]) => { scope.__communityCalls.push(args); return undefined; }, } } }); }); }); for (const width of [375, 768, 1280, 1470]) { test(`welcome, composer and community link clear teacher introductions at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 850 }); await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto(baseUrl + '?teachers'); const heading = page.getByTestId('coding-start-heading'); const teachers = page.getByTestId('teacher-welcomes'); await expect(teachers.locator('.teacher-welcome-card')).toHaveCount(2); await expect.poll(async () => { const a = await heading.boundingBox(), b = await teachers.boundingBox(); return !!a && !!b && (a.x + a.width <= b.x || a.y >= b.y + b.height); }).toBe(true); await expect(page.locator('.coding-start-mark')).toHaveCSS('animation-name', 'none'); await page.getByTestId('community-invitation').scrollIntoViewIfNeeded(); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await expect(page.getByRole('button', { name: '去作品广场逛逛' })).toBeVisible(); await teachers.getByRole('button', { name: '先自己试试' }).click(); await heading.scrollIntoViewIfNeeded(); const composer = page.getByTestId('coding-message-composer'); await expect(composer).toBeInViewport({ ratio: 1 }); const box = (await composer.boundingBox())!; expect(850 - box.y - box.height).toBeLessThanOrEqual(40); expect((await page.getByTestId('community-invitation').boundingBox())!.y).toBeLessThan(box.y); await page.screenshot({ path: test.info().outputPath(`community-welcome-${width}.png`) }); }); } test('a short window scrolls the welcome while keeping the composer at the bottom', async ({ page }) => { await page.setViewportSize({ width: 375, height: 600 }); await page.goto(baseUrl + '?teachers'); const composer = page.getByTestId('coding-message-composer'); await expect(composer).toBeInViewport({ ratio: 1 }); const before = (await composer.boundingBox())!; await page.getByTestId('community-invitation').scrollIntoViewIfNeeded(); const after = (await composer.boundingBox())!; expect(after.y).toBeCloseTo(before.y, 0); expect(600 - after.y - after.height).toBeLessThanOrEqual(40); }); test('the community link opens only on click and leaves the draft mounted', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 850 }); const communityRequests: string[] = []; page.on('request', request => { if (request.url().includes('square.nianxx.cn')) communityRequests.push(request.url()); }); await page.goto(baseUrl); const composer = page.getByRole('textbox'); await composer.fill('我想做一个自己的小镇'); await composer.evaluate(node => node.setAttribute('data-mounted', 'same')); await page.getByRole('button', { name: '去作品广场逛逛' }).click(); expect(await page.evaluate(() => (window as typeof window & { __communityCalls: unknown[][] }).__communityCalls)) .toContainEqual(['shell:openExternal', 'https://square.nianxx.cn/#explore']); expect(communityRequests).toEqual([]); await expect(page.locator('iframe')).toHaveCount(0); await expect(composer).toHaveValue('我想做一个自己的小镇'); await expect(composer).toHaveAttribute('data-mounted', 'same'); await page.getByRole('button', { name: '发送', exact: true }).click(); await expect(page.getByTestId('community-invitation')).toHaveCount(0); await expect(composer).toHaveAttribute('data-mounted', 'same'); });