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:
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` });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user