feat(viewer,ui04): E02 static fidelity — real photo fixtures, measured shell/photo/collapse specs, edge fade
- fixtures: 8 picsum-seeded real photos (mixed ratios); seedPhoto thumbBytes/browseBytes injection - e2e E02 block: wrapper shell (195x280/radius/border/#10161E), photo strip (193w, ratio-height, 3px gap), collapse (44/44 hit, 24 visual, sole button), desktop+mobile screenshots - EmbedStream: top/bottom 12px dark fade (0.12, §2.3 optional item) - fix(admin): pre-existing eslint globals gap for ElNotification (template drift, blocks lint gate) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { createServer, type Server } from 'node:http';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import {
|
||||
cleanupSeededAlbums,
|
||||
@@ -12,10 +15,26 @@ import {
|
||||
|
||||
// UI-04 v2(vertical-stream 照片流窗)跨源 Playwright:宿主 127.0.0.1:3999 ←
|
||||
// viewer 5175 真跨源,loader /embed/v2/loader.js。随 E01-E07 逐任务扩展:
|
||||
// E01 出口门 = 固定 195×280 小窗跨源可见且无 footer(本文件首批用例)。
|
||||
// E01 出口门 = 固定 195×280 小窗跨源可见且无 footer;E02 = 静态高保真测量。
|
||||
// 照片种子为真实摄影样本(e2e/fixtures,picsum 固定 seed 下载——验收 §11.1
|
||||
// 「无渐变色块假图」;比例覆盖横/竖/方/全景,EMB-A03 素材基础)。
|
||||
const HOST_PORT = 3999;
|
||||
const VIEWER_ORIGIN = 'http://127.0.0.1:5175';
|
||||
const MINUTE = 60_000;
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_ROOT = join(HERE, '../../../docs/designs/ui04-implementation');
|
||||
|
||||
/** 摄影样本(picsum seed 固定):[file, width, height]。 */
|
||||
const FIXTURES: Array<[string, number, number]> = [
|
||||
['photo-01.jpg', 800, 500],
|
||||
['photo-02.jpg', 800, 1100],
|
||||
['photo-03.jpg', 800, 800],
|
||||
['photo-04.jpg', 1200, 400],
|
||||
['photo-05.jpg', 800, 1400],
|
||||
['photo-06.jpg', 900, 600],
|
||||
['photo-07.jpg', 800, 1000],
|
||||
['photo-08.jpg', 1000, 700],
|
||||
];
|
||||
|
||||
let slotToken = '';
|
||||
|
||||
@@ -28,9 +47,15 @@ async function seedFeed(alias: string): Promise<string> {
|
||||
{ locale: 'en', title: `${alias} Album` },
|
||||
],
|
||||
});
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await seedPhoto(album.id, `${alias}-p${i}`, {
|
||||
publishedAt: new Date(Date.now() - (7 - i) * MINUTE),
|
||||
for (let i = 0; i < FIXTURES.length; i++) {
|
||||
const [file, width, height] = FIXTURES[i];
|
||||
const bytes = await readFile(join(HERE, 'fixtures', file));
|
||||
await seedPhoto(album.id, `${alias}-p${i + 1}`, {
|
||||
publishedAt: new Date(Date.now() - (FIXTURES.length - i) * MINUTE),
|
||||
width,
|
||||
height,
|
||||
thumbBytes: bytes,
|
||||
browseBytes: bytes,
|
||||
});
|
||||
}
|
||||
await flushSync(album.id);
|
||||
@@ -79,7 +104,15 @@ async function openHost(page: Page, viewport: { width: number; height: number })
|
||||
|
||||
test.beforeAll(async () => {
|
||||
slotToken = await seedFeed('ui04v2');
|
||||
await new Promise<void>((resolve) => server.listen(HOST_PORT, '127.0.0.1', resolve));
|
||||
await new Promise<void>((resolve) => {
|
||||
// 残留进程容错:上轮失败运行的夹具服务器可能仍占端口——同内容服务,
|
||||
// 复用即可(其后用例改写 currentHtml 对两个进程同值)。
|
||||
server.once('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EADDRINUSE') resolve();
|
||||
else throw err;
|
||||
});
|
||||
server.listen(HOST_PORT, '127.0.0.1', () => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
@@ -121,3 +154,73 @@ test.describe.serial('E01 exit gate: fixed window, no footer', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe.serial('E02 static fidelity: measured styles + screenshots', () => {
|
||||
test('desktop 1440x900: window shell, photo strip, collapse button measured', async ({ page }) => {
|
||||
await openHost(page, { width: 1440, height: 900 });
|
||||
const wrapper = page.locator('iframe[src*="/embed/"][src*="v=2"]').locator('..');
|
||||
await expect(page.locator('iframe[src*="/embed/"][src*="v=2"]')).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => Math.round((await wrapper.boundingBox())?.width ?? 0))
|
||||
.toBe(195);
|
||||
await expect
|
||||
.poll(async () => Math.round((await wrapper.boundingBox())?.height ?? 0))
|
||||
.toBe(280);
|
||||
|
||||
// 外壳视觉规格(§2.1):圆角 12 / 单圈 1px 白边 / 底色 #10161E / 阴影。
|
||||
const shell = await wrapper.evaluate((el) => {
|
||||
const cs = getComputedStyle(el);
|
||||
return {
|
||||
radius: cs.borderTopLeftRadius,
|
||||
borderWidth: cs.borderTopWidth,
|
||||
borderColor: cs.borderTopColor,
|
||||
background: cs.backgroundColor,
|
||||
boxShadow: cs.boxShadow,
|
||||
boxSizing: cs.boxSizing,
|
||||
};
|
||||
});
|
||||
expect(shell.radius).toBe('12px');
|
||||
expect(shell.borderWidth).toBe('1px');
|
||||
expect(shell.borderColor).toBe('rgba(255, 255, 255, 0.8)');
|
||||
expect(shell.background).toBe('rgb(16, 22, 30)');
|
||||
expect(shell.boxSizing).toBe('border-box');
|
||||
|
||||
// 照片带(§2.2/2.3):原比例、宽满内宽(195-2=193)、3px 缝。
|
||||
const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]');
|
||||
const firstImg = inside.locator('.stream-photo__img').first();
|
||||
await expect(firstImg).toBeVisible();
|
||||
const imgBox = await firstImg.boundingBox();
|
||||
expect(Math.round(imgBox?.width ?? 0)).toBe(193);
|
||||
// 首图 = 最新照片(publishedAt 倒序)= photo-08 1000×700 → 193*0.7 ≈ 135。
|
||||
expect(Math.round(imgBox?.height ?? 0)).toBe(135);
|
||||
const gap = await inside.locator('.stream-track').evaluate((el) => getComputedStyle(el).rowGap);
|
||||
expect(gap).toBe('3px');
|
||||
|
||||
// 收起入口(§2.4):可视圆 24×24 右上内缩 6px;有效触区 44×44。
|
||||
const collapse = inside.locator('.stream-collapse');
|
||||
const cbBox = await collapse.boundingBox();
|
||||
expect(Math.round(cbBox?.width ?? 0)).toBe(44);
|
||||
expect(Math.round(cbBox?.height ?? 0)).toBe(44);
|
||||
const vpBox = await inside.locator('.stream-viewport').boundingBox();
|
||||
expect(Math.round((vpBox?.x ?? 0) + (vpBox?.width ?? 0) - (cbBox?.x ?? 0) - 44)).toBe(0);
|
||||
expect(Math.round((cbBox?.y ?? 0) - (vpBox?.y ?? 0))).toBe(0);
|
||||
|
||||
// 表面无工具元素(§2.2):除收起外无其他 button/链接按钮形态。
|
||||
await expect(inside.locator('button')).toHaveCount(1);
|
||||
|
||||
await page.screenshot({ path: join(OUT_ROOT, 'actual/desktop/e02-static-desktop.png') });
|
||||
});
|
||||
|
||||
test('mobile 390x844: expanded window identical 195x280 + screenshot', async ({ page }) => {
|
||||
await openHost(page, { width: 390, height: 844 });
|
||||
await page.locator('[role="button"]').first().click();
|
||||
const wrapper = page.locator('iframe[src*="/embed/"][src*="v=2"]').locator('..');
|
||||
await expect
|
||||
.poll(async () => Math.round((await wrapper.boundingBox())?.width ?? 0))
|
||||
.toBe(195);
|
||||
await expect
|
||||
.poll(async () => Math.round((await wrapper.boundingBox())?.height ?? 0))
|
||||
.toBe(280);
|
||||
await page.screenshot({ path: join(OUT_ROOT, 'actual/mobile/e02-static-mobile.png') });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user