docs(ui04): test-server deployment and live verification — both stacks on 1cf12be images, 5/5 real-environment checks incl. WS-via-OpenResty hearts

- server image 1cf12be-20260922 + web ui04-1cf12be-20260922 (server-side builds, entrypoint/syntax adaptations), cn+ov rolled, webs restarted
- v2 slot created via real admin API (vertical-stream/16px/s/reactions), bound to album with real photos (live-album OSS)
- live e2e 5/5: entries, 195x280 + real OSS rendering + auto scroll, collapse/expand, deep-link popup, real like -> WS through 32006 proxy -> heart
- screenshots + reusable server-verify spec archived; acceptance record section added

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 19:44:18 +08:00
parent 1cf12be904
commit fe2428e290
3 changed files with 137 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

@@ -0,0 +1,137 @@
// 临时服务器验证规格(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(`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI-04 server host</title></head>
<body style="min-height: 1200px; font-family: system-ui">
<h1>Host page (server-deployed v2 widget)</h1>
<script src="${SERVER_VIEWER}/embed/v2/loader.js" data-slot="${SLOT_TOKEN}" defer></script>
</body></html>`);
});
async function openHost(page: Page): Promise<void> {
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<void>((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` });
});
});