Files
photo-live/docs/designs/ui04-implementation/server-verify.spec.ts
T
浅念 fe2428e290 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>
2026-09-22 19:44:18 +08:00

138 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 临时服务器验证规格(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` });
});
});