124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
|
|
import { createServer, type Server } from 'node:http';
|
|||
|
|
import { expect, test, type Page } from '@playwright/test';
|
|||
|
|
import {
|
|||
|
|
cleanupSeededAlbums,
|
|||
|
|
createEmbedSlot,
|
|||
|
|
flushOutbox,
|
|||
|
|
flushSync,
|
|||
|
|
seedAlbum,
|
|||
|
|
seedPhoto,
|
|||
|
|
updateEmbedSlot,
|
|||
|
|
} from './helpers';
|
|||
|
|
|
|||
|
|
// UI-04 v2(vertical-stream 照片流窗)跨源 Playwright:宿主 127.0.0.1:3999 ←
|
|||
|
|
// viewer 5175 真跨源,loader /embed/v2/loader.js。随 E01-E07 逐任务扩展:
|
|||
|
|
// E01 出口门 = 固定 195×280 小窗跨源可见且无 footer(本文件首批用例)。
|
|||
|
|
const HOST_PORT = 3999;
|
|||
|
|
const VIEWER_ORIGIN = 'http://127.0.0.1:5175';
|
|||
|
|
const MINUTE = 60_000;
|
|||
|
|
|
|||
|
|
let slotToken = '';
|
|||
|
|
|
|||
|
|
async function seedFeed(alias: string): Promise<string> {
|
|||
|
|
const album = await seedAlbum({
|
|||
|
|
alias,
|
|||
|
|
defaultLocale: 'zh-CN',
|
|||
|
|
translations: [
|
|||
|
|
{ locale: 'zh-CN', title: `${alias} 中文相册` },
|
|||
|
|
{ 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),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
await flushSync(album.id);
|
|||
|
|
const slot = await createEmbedSlot(`ui04-v2-${alias}`);
|
|||
|
|
// v2 模式显式提交(E01 契约:displayMode vertical-stream + 速度/气泡)
|
|||
|
|
await updateEmbedSlot(slot.id, {
|
|||
|
|
albumId: album.id,
|
|||
|
|
settings: {
|
|||
|
|
position: 'right',
|
|||
|
|
size: 'md',
|
|||
|
|
intervalMs: 5000,
|
|||
|
|
lang: 'auto',
|
|||
|
|
collapsedDefault: false,
|
|||
|
|
imageFit: 'contain',
|
|||
|
|
offsetX: 16,
|
|||
|
|
offsetBottom: 24,
|
|||
|
|
zIndex: 1000,
|
|||
|
|
displayMode: 'vertical-stream',
|
|||
|
|
scrollSpeed: 16,
|
|||
|
|
reactionsEnabled: true,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
await flushOutbox();
|
|||
|
|
return slot.token;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let currentHtml = '';
|
|||
|
|
const server: Server = createServer((_req, res) => {
|
|||
|
|
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|||
|
|
res.end(currentHtml);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function openHost(page: Page, viewport: { width: number; height: number }): Promise<void> {
|
|||
|
|
await page.setViewportSize(viewport);
|
|||
|
|
currentHtml = `<!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 v2 host</title></head>
|
|||
|
|
<body style="min-height: 1200px; font-family: system-ui">
|
|||
|
|
<h1>Host page (v2 stream)</h1>
|
|||
|
|
<script src="${VIEWER_ORIGIN}/embed/v2/loader.js" data-slot="${slotToken}" defer></script>
|
|||
|
|
</body>
|
|||
|
|
</html>`;
|
|||
|
|
await page.goto(`http://127.0.0.1:${HOST_PORT}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test.beforeAll(async () => {
|
|||
|
|
slotToken = await seedFeed('ui04v2');
|
|||
|
|
await new Promise<void>((resolve) => server.listen(HOST_PORT, '127.0.0.1', resolve));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.afterAll(async () => {
|
|||
|
|
await cleanupSeededAlbums();
|
|||
|
|
server.close();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe.serial('E01 exit gate: fixed window, no footer', () => {
|
|||
|
|
for (const [name, viewport] of [
|
|||
|
|
['desktop 1440x900', { width: 1440, height: 900 }],
|
|||
|
|
['mobile 390x844', { width: 390, height: 844 }],
|
|||
|
|
] as const) {
|
|||
|
|
test(`v2 window is fixed 195x280 on ${name}`, async ({ page }) => {
|
|||
|
|
const mobile = viewport.width < 480;
|
|||
|
|
await openHost(page, viewport);
|
|||
|
|
const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
|
|||
|
|
const wrapper = frame.locator('..');
|
|||
|
|
// 手机默认收起(沿用 v1 策略):点击把手展开后测量。
|
|||
|
|
if (mobile) {
|
|||
|
|
await page.locator('[role="button"]').first().click();
|
|||
|
|
}
|
|||
|
|
await expect(frame).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);
|
|||
|
|
// iframe 内:无 header/footer(E01 静态壳);右上收起按钮存在。
|
|||
|
|
const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]');
|
|||
|
|
await expect(inside.getByRole('button', { name: /收起照片窗|Collapse photo window/ })).toBeVisible();
|
|||
|
|
// v1 结构不出现(判别渲染 DOM 而非原始 HTML——dev 模式内联 CSS 含全部
|
|||
|
|
// scoped 类名,字符串断言会误报;「活动照片」字样亦存在于照片 alt 文案)。
|
|||
|
|
await expect(inside.locator('.embed-head')).toHaveCount(0);
|
|||
|
|
await expect(inside.locator('.embed-footer')).toHaveCount(0);
|
|||
|
|
await expect(inside.getByText('查看相册')).toHaveCount(0);
|
|||
|
|
await expect(inside.getByText('照片轮播')).toHaveCount(0);
|
|||
|
|
await expect(inside.locator('.stream-viewport')).toHaveCount(1);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|