// 临时服务器验证规格(UI-04 部署后真机验证,采集后删除): // 本地浏览器 → 测试服务器(219.157.255.213)真实链路: // · v2 loader/iframe/ov feed/OSS 图片全部来自服务器与真实 OSS; // · 覆盖:195×280 固定窗、照片渲染、连续上滚、收起展开、点击深链弹窗、 // 真实点赞(ov 公开 API)→ WS 经 OpenResty 32006 代理 → 心形气泡、 // viewer 相册页与管理后台登录页可达。 import { createServer, type Server } from 'node:http'; import { expect, test, type Page } from '@playwright/test'; const HOST_PORT = 3997; const SERVER_VIEWER = 'http://219.157.255.213:32006'; const SERVER_ADMIN = 'http://219.157.255.213:32005'; const SLOT_TOKEN = 'dg0mo4i5gov39g1vv0wgisgb'; const ALBUM_TOKEN = 'nzmk2kr4trzemiwmv65uwxva'; const OUT = 'docs/designs/ui04-implementation/actual/server'; const server: Server = createServer((_req, res) => { res.setHeader('content-type', 'text/html; charset=utf-8'); res.end(` UI-04 server host

Host page (server-deployed v2 widget)

`); }); async function openHost(page: Page): Promise { await page.setViewportSize({ width: 1440, height: 900 }); await page.goto(`http://127.0.0.1:${HOST_PORT}`); } test.describe.serial('UI-04 server deployment verification', () => { test.beforeAll(async () => { await new Promise((resolve) => { server.once('error', () => resolve()); // 残留进程容错 server.listen(HOST_PORT, '127.0.0.1', () => resolve()); }); }); test.afterAll(() => server.close()); test('entries reachable: viewer album page + admin login + loader artifact', async ({ page, request }) => { const viewer = await page.goto(`${SERVER_VIEWER}/a/${ALBUM_TOKEN}`); expect(viewer?.status()).toBe(200); const loader = await request.get(`${SERVER_VIEWER}/embed/v2/loader.js`); expect(loader.status()).toBe(200); expect((await loader.text()).length).toBeGreaterThan(1000); const admin = await page.goto(`${SERVER_ADMIN}/admin/`); expect(admin?.status()).toBe(200); }); test('v2 widget on server: 195x280, real photos render, auto scrolls', async ({ page }) => { await openHost(page); const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]'); await frame.waitFor({ timeout: 15_000 }); const wrapper = frame.locator('..'); await expect .poll(async () => Math.round((await wrapper.boundingBox())?.width ?? 0), { timeout: 10_000 }) .toBe(195); await expect .poll(async () => Math.round((await wrapper.boundingBox())?.height ?? 0)) .toBe(280); const embedFrame = page.frames().find((f) => f.url().includes('v=2')); expect(embedFrame).toBeDefined(); // 真实 OSS 图片加载出真实尺寸(等待首图 complete)。 await expect .poll( async () => embedFrame!.locator('.stream-photo__img').first().evaluate((el) => (el as HTMLImageElement).complete), { timeout: 20_000 }, ) .toBe(true); // 自动上滚:2s 位移 ~32px(transform 更负)。 await page.waitForTimeout(1_500); const t1 = await embedFrame!.locator('.stream-track').evaluate((el) => el.style.transform); await page.waitForTimeout(2_000); const t2 = await embedFrame!.locator('.stream-track').evaluate((el) => el.style.transform); expect(t2).not.toBe(t1); await page.screenshot({ path: `${OUT}/server-desktop-widget.png` }); }); test('collapse via in-iframe button, expand via host handle', async ({ page }) => { await openHost(page); const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]'); await frame.waitFor({ timeout: 15_000 }); const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]'); await inside.getByRole('button', { name: /收起照片窗|Collapse photo window/ }).click(); await expect(frame).toBeHidden(); await expect(page.locator('[role="button"]').first()).toBeVisible(); await page.locator('[role="button"]').first().click(); await expect(frame).toBeVisible(); }); test('photo click opens server viewer deep link', async ({ page }) => { await openHost(page); const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]'); await frame.waitFor({ timeout: 15_000 }); const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]'); const box = await frame.boundingBox(); await page.mouse.move((box!.x ?? 0) + 90, (box!.y ?? 0) + 140); // 悬停冻结 await page.waitForTimeout(500); const target = inside.locator('a.stream-photo').first(); const photoId = await target.getAttribute('data-photo-id'); const popupPromise = page.waitForEvent('popup', { timeout: 10_000 }); await target.click(); const popup = await popupPromise; expect(popup.url()).toContain(`219.157.255.213:32006/a/${ALBUM_TOKEN}/p/${photoId}`); await popup.close(); }); test('real like via ov API → WS through 32006 proxy → heart in widget', async ({ page }) => { await openHost(page); const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]'); await frame.waitFor({ timeout: 15_000 }); const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]'); const box = await frame.boundingBox(); await page.mouse.move((box!.x ?? 0) + 90, (box!.y ?? 0) + 140); await page.waitForTimeout(800); const embedFrame = page.frames().find((f) => f.url().includes('v=2'))!; const photoId = await embedFrame.locator('a.stream-photo').first().getAttribute('data-photo-id'); const like = await page.request.post( `${SERVER_VIEWER}/v1/public/albums/${ALBUM_TOKEN}/photos/${photoId}/likes`, { headers: { 'content-type': 'application/json' }, data: { sessionId: `ui04-server-${Date.now()}`, batchId: `batch-server-${Date.now()}`, delta: 2, }, }, ); expect(like.status()).toBeLessThan(300); // WS 经 OpenResty 32006 反代(升级头)到达 iframe → 真实 burst → 心形。 await expect(inside.locator('.heart').first()).toBeVisible({ timeout: 8_000 }); await page.screenshot({ path: `${OUT}/server-desktop-heart.png` }); }); });