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-message-timestamps', resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' }, server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'isolated-message-timestamps', enforce: 'pre', load(id) { if (id.endsWith('/src/lib/coding-teacher.ts')) return ` export function teacherTopicsPath() { return '/fixture'; } export function legacyTopicBase(base) { return base; } export const teacherApi = { conversation: async () => ({ topic: window.__timestampTopic, before: null }), seenConversation: async () => window.__timestampTopic, events: async () => ({ close() {}, addEventListener() {} }), }; `; 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 {}; } export async function hostApiFetchBytes() { return new Uint8Array(); } `; }, 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, 1280]) { test(`both chat surfaces show stable local times at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await page.goto(baseUrl); await expect(page.locator('time')).toHaveCount(4); const times = await page.locator('time').evaluateAll(elements => elements.map(element => ({ time: element.getAttribute('datetime'), title: element.getAttribute('title') }))); expect(times.map(item => item.time)).toEqual(['2026-09-28T08:01:02.000Z', '2026-09-28T08:01:42.000Z', '2026-09-28T08:01:02.000Z', '2026-09-28T08:01:42.000Z']); for (const item of times) expect(item.title).toMatch(/^2026年9月28日 \d{2}:\d{2}:(02|42)$/); for (const time of await page.locator('time').all()) { await expect(time).toHaveCSS('font-size', '11px'); expect(await time.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true); } await expect(page.getByRole('button', { name: '我想让它陪我讲故事。' })).toBeEnabled(); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await page.screenshot({ path: test.info().outputPath(`message-timestamps-${width}.png`), fullPage: true }); await page.reload(); await expect(page.locator('time')).toHaveCount(4); expect(await page.locator('time').evaluateAll(elements => elements.map(element => ({ time: element.getAttribute('datetime'), title: element.getAttribute('title') })))).toEqual(times); }); }