import { createServer, type Server } from 'node:http'; import { expect, test, type Page } from '@playwright/test'; import { cleanupSeededAlbums, createEmbedSlot, flushOutbox, flushSync, seedAlbum, seedPhoto, updateEmbedSlot, } from './helpers'; // UI-04 v2(vertical-stream 照片流窗)跨源 Playwright:宿主 127.0.0.1:3999 ← // viewer 5175 真跨源,loader /embed/v2/loader.js。随 E01-E07 逐任务扩展: // E01 出口门 = 固定 195×280 小窗跨源可见且无 footer(本文件首批用例)。 const HOST_PORT = 3999; const VIEWER_ORIGIN = 'http://127.0.0.1:5175'; const MINUTE = 60_000; let slotToken = ''; async function seedFeed(alias: string): Promise { const album = await seedAlbum({ alias, defaultLocale: 'zh-CN', translations: [ { locale: 'zh-CN', title: `${alias} 中文相册` }, { locale: 'en', title: `${alias} Album` }, ], }); for (let i = 1; i <= 6; i++) { await seedPhoto(album.id, `${alias}-p${i}`, { publishedAt: new Date(Date.now() - (7 - i) * MINUTE), }); } await flushSync(album.id); const slot = await createEmbedSlot(`ui04-v2-${alias}`); // v2 模式显式提交(E01 契约:displayMode vertical-stream + 速度/气泡) await updateEmbedSlot(slot.id, { albumId: album.id, settings: { position: 'right', size: 'md', intervalMs: 5000, lang: 'auto', collapsedDefault: false, imageFit: 'contain', offsetX: 16, offsetBottom: 24, zIndex: 1000, displayMode: 'vertical-stream', scrollSpeed: 16, reactionsEnabled: true, }, }); await flushOutbox(); return slot.token; } let currentHtml = ''; const server: Server = createServer((_req, res) => { res.setHeader('content-type', 'text/html; charset=utf-8'); res.end(currentHtml); }); async function openHost(page: Page, viewport: { width: number; height: number }): Promise { await page.setViewportSize(viewport); currentHtml = ` UI-04 v2 host

Host page (v2 stream)

`; await page.goto(`http://127.0.0.1:${HOST_PORT}`); } test.beforeAll(async () => { slotToken = await seedFeed('ui04v2'); await new Promise((resolve) => server.listen(HOST_PORT, '127.0.0.1', resolve)); }); test.afterAll(async () => { await cleanupSeededAlbums(); server.close(); }); test.describe.serial('E01 exit gate: fixed window, no footer', () => { for (const [name, viewport] of [ ['desktop 1440x900', { width: 1440, height: 900 }], ['mobile 390x844', { width: 390, height: 844 }], ] as const) { test(`v2 window is fixed 195x280 on ${name}`, async ({ page }) => { const mobile = viewport.width < 480; await openHost(page, viewport); const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]'); const wrapper = frame.locator('..'); // 手机默认收起(沿用 v1 策略):点击把手展开后测量。 if (mobile) { await page.locator('[role="button"]').first().click(); } await expect(frame).toBeVisible(); await expect .poll(async () => Math.round((await wrapper.boundingBox())?.width ?? 0)) .toBe(195); await expect .poll(async () => Math.round((await wrapper.boundingBox())?.height ?? 0)) .toBe(280); // iframe 内:无 header/footer(E01 静态壳);右上收起按钮存在。 const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]'); await expect(inside.getByRole('button', { name: /收起照片窗|Collapse photo window/ })).toBeVisible(); // v1 结构不出现(判别渲染 DOM 而非原始 HTML——dev 模式内联 CSS 含全部 // scoped 类名,字符串断言会误报;「活动照片」字样亦存在于照片 alt 文案)。 await expect(inside.locator('.embed-head')).toHaveCount(0); await expect(inside.locator('.embed-footer')).toHaveCount(0); await expect(inside.getByText('查看相册')).toHaveCount(0); await expect(inside.getByText('照片轮播')).toHaveCount(0); await expect(inside.locator('.stream-viewport')).toHaveCount(1); }); } });