diff --git a/docs/designs/ui04-implementation/actual/server/server-desktop-heart.png b/docs/designs/ui04-implementation/actual/server/server-desktop-heart.png new file mode 100644 index 0000000..51d0afb Binary files /dev/null and b/docs/designs/ui04-implementation/actual/server/server-desktop-heart.png differ diff --git a/docs/designs/ui04-implementation/actual/server/server-desktop-widget.png b/docs/designs/ui04-implementation/actual/server/server-desktop-widget.png new file mode 100644 index 0000000..61a4ab5 Binary files /dev/null and b/docs/designs/ui04-implementation/actual/server/server-desktop-widget.png differ diff --git a/docs/designs/ui04-implementation/server-verify.spec.ts b/docs/designs/ui04-implementation/server-verify.spec.ts new file mode 100644 index 0000000..1014012 --- /dev/null +++ b/docs/designs/ui04-implementation/server-verify.spec.ts @@ -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(` + + 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` }); + }); +});