Files
photo-live/apps/admin/e2e/embed-slot-config.spec.ts
T
2026-09-21 19:26:48 +08:00

234 lines
11 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.
// 展示位配置表单 + 本地实时预览 e2e(UI-01 Task 7,真实后端:cn 8080 +
// admin dev 5176 由 playwright.config webServer 拉起或复用)。旅程:新建表单
// (UI-01 新字段 imageFit/offsetX/offsetBottom/zIndex)→ 预览即时随动 +
// 「未保存」脏态标记 → 保存 → GET /v1/admin/embed-slots 断言新字段持久化 →
// 嵌入代码卡只含显式非默认 data-* 覆盖(全默认 slot = 裸 loader 引用)→
// 表单交互窗口零 /v1/ 请求(预览纯本地,不写展示位不上报浏览)。
// 选择器一律 data-test;文案断言仅用稳定短语。
import { expect, test, type Page, type Request } from '@playwright/test'
import { adminCredentials, loginAsAdmin } from './helpers'
test.use({ testIdAttribute: 'data-test' })
const SUFFIX = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
const SLOT_NAME = `E2E配置展示位 ${SUFFIX}`
const DEFAULT_SLOT_NAME = `E2E默认展示位 ${SUFFIX}`
/** 列表内定位展示位行 */
function slotRow(page: Page, name: string) {
return page.getByTestId('embed-slots-table').locator('tr', { hasText: name })
}
/** 管理端会话令牌(GET 断言持久化用) */
async function sessionToken(page: Page): Promise<string> {
const { account, password } = adminCredentials()
const login = await page.request.post('/v1/auth/password', { data: { account, password } })
expect(login.ok()).toBeTruthy()
return ((await login.json()) as { sessionToken: string }).sessionToken
}
/** 管理端列表按名称取条目(createdAt 倒序首页即可命中新建条目) */
async function findSlotByName(page: Page, name: string): Promise<ApiShapeSlotItem> {
const token = await sessionToken(page)
const res = await page.request.get('/v1/admin/embed-slots', {
params: { limit: 20 },
headers: { Authorization: `Bearer ${token}` }
})
expect(res.ok()).toBeTruthy()
const body = (await res.json()) as { items: ApiShapeSlotItem[] }
const item = body.items.find((slot) => slot.name === name)
expect(item).toBeTruthy()
return item as ApiShapeSlotItem
}
/** 断言关心的响应子集(settings 恒为归一化全集) */
interface ApiShapeSlotItem {
id: string
name: string
settings: {
position: string
size: string
intervalMs: number
lang: string
collapsedDefault: boolean
imageFit: string
offsetX: number
offsetBottom: number
zIndex: number
}
}
test.describe.serial('embed slot config form + live preview (UI-01 Task 7)', () => {
test('新字段表单:预览即时随动 + 脏态标记 → 保存 → GET 返回新值', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('/#/system/embed-slots')
await expect(page.getByTestId('embed-slots-table')).toBeVisible()
await page.getByTestId('slot-create-btn').click()
await expect(page.getByTestId('slot-form-dialog')).toBeVisible()
// 打开即见双框预览;初始缺省不脏(新建空名 + 契约缺省设置)
const panel = page.getByTestId('embed-preview-panel')
await expect(panel).toBeVisible()
const desktop = page.getByTestId('embed-preview-desktop')
const mobile = page.getByTestId('embed-preview-mobile')
await expect(desktop).toBeVisible()
await expect(mobile).toBeVisible()
await expect(page.getByTestId('slot-form-dirty')).toBeHidden()
// 默认桌面展开(4:3 舞台 + 底栏)、手机窄屏收起为把手(§5.4)
await expect(desktop.getByTestId('embed-preview-stage')).toBeVisible()
await expect(desktop.getByTestId('embed-preview-indicator')).toHaveText('01 / 03')
await expect(mobile.getByTestId('embed-preview-handle')).toBeVisible()
await expect(mobile.getByTestId('embed-preview-stage')).toBeHidden()
// 输入名称 → 脏态标记出现
await page.getByTestId('slot-form-name').fill(SLOT_NAME)
await expect(page.getByTestId('slot-form-dirty')).toBeVisible()
// UI-01 新字段:图片适配 cover → 桌面预览舞台即时切 cover(未保存已随动)
await page.getByTestId('slot-form-image-fit-cover').click()
await expect(desktop.getByTestId('embed-preview-stage')).toHaveClass(/is-cover/)
// 位置 left + 水平偏移 40 → 停靠几何即时反映(默认 offset 16 → 40)
await page.getByTestId('slot-form-position').click()
await page.getByTestId('slot-form-position-left').click()
await expect(desktop.getByTestId('embed-preview-widget')).toHaveCSS('left', '16px')
await page.getByTestId('slot-form-offset-x').locator('input').fill('40')
await page.keyboard.press('Tab')
await expect(desktop.getByTestId('embed-preview-widget')).toHaveCSS('left', '40px')
// 底部偏移 80 → 距底即时反映
await page.getByTestId('slot-form-offset-bottom').locator('input').fill('80')
await page.keyboard.press('Tab')
await expect(desktop.getByTestId('embed-preview-widget')).toHaveCSS('bottom', '80px')
// 层级 2000 → z-index 徽标即时反映
await page.getByTestId('slot-form-z-index').locator('input').fill('2000')
await page.keyboard.press('Tab')
await expect(desktop.getByTestId('embed-preview-zindex')).toHaveText('z-index 2000')
// 尺寸 sm(280)+ 间隔 8000
await page.getByTestId('slot-form-size').click()
await page.getByTestId('slot-form-size-sm').click()
await expect(desktop.getByTestId('embed-preview-widget')).toHaveCSS('width', '280px')
await page.getByTestId('slot-form-interval').locator('input').fill('8000')
await page.keyboard.press('Tab')
// 保存 → 对话框关闭;GET 管理端断言新字段持久化(settings 归一化全集)
await page.getByTestId('slot-form-submit').click()
await expect(page.getByTestId('slot-form-dialog')).toBeHidden()
const saved = await findSlotByName(page, SLOT_NAME)
expect(saved.settings).toMatchObject({
position: 'left',
size: 'sm',
intervalMs: 8000,
lang: 'auto',
collapsedDefault: false,
imageFit: 'cover',
offsetX: 40,
offsetBottom: 80,
zIndex: 2000
})
})
test('嵌入代码卡:只含显式非默认 data-* 覆盖;全默认 slot 为裸 loader 引用', async ({ page }) => {
// 全默认 slot 经 API 创建(settings 整体缺省 → 归一化缺省落库)
const token = await sessionToken(page)
const created = await page.request.post('/v1/admin/embed-slots', {
headers: { Authorization: `Bearer ${token}` },
data: { name: DEFAULT_SLOT_NAME }
})
expect(created.ok()).toBeTruthy()
await loginAsAdmin(page)
await page.goto('/#/system/embed-slots')
// 显式覆盖 slot:position/size/interval/imageFit/offsetX/offsetBottom/zIndex
// 非默认 → 各自 data-*;lang=auto 与 collapsedDefault(无 data-* 支持)省略
const overrideRow = slotRow(page, SLOT_NAME)
await expect(overrideRow).toBeVisible()
await overrideRow.getByTestId('slot-code-btn').click()
await expect(page.getByTestId('embed-code-dialog')).toBeVisible()
const overrideSnippet = page.getByTestId('embed-code-snippet')
await expect(overrideSnippet).toContainText('data-slot=')
await expect(overrideSnippet).toContainText('data-position="left"')
await expect(overrideSnippet).toContainText('data-size="sm"')
await expect(overrideSnippet).toContainText('data-interval="8000"')
await expect(overrideSnippet).toContainText('data-image-fit="cover"')
await expect(overrideSnippet).toContainText('data-offset-x="40"')
await expect(overrideSnippet).toContainText('data-offset-bottom="80"')
await expect(overrideSnippet).toContainText('data-z-index="2000"')
await expect(overrideSnippet).not.toContainText('data-lang=')
// 覆盖说明可见(缺省字段让后台设置生效)
await expect(page.getByTestId('embed-code-override-hint')).toBeVisible()
await page.getByTestId('embed-code-close').click()
await expect(page.getByTestId('embed-code-dialog')).toBeHidden()
// 全默认 slot:仅 data-slot(缺省字段全部省略 → 后台配置生效)
const defaultRow = slotRow(page, DEFAULT_SLOT_NAME)
await expect(defaultRow).toBeVisible()
await defaultRow.getByTestId('slot-code-btn').click()
await expect(page.getByTestId('embed-code-dialog')).toBeVisible()
const defaultSnippet = page.getByTestId('embed-code-snippet')
const defaultToken = ((await page.getByTestId('embed-code-token').textContent()) ?? '').trim()
await expect(defaultSnippet).toHaveText(
`<script src="http://127.0.0.1:5175/embed/v1/loader.js" data-slot="${defaultToken}" defer></script>`
)
await page.getByTestId('embed-code-close').click()
})
test('预览纯本地:表单交互窗口零 /v1/ 请求(不写展示位、不上报浏览)', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('/#/system/embed-slots')
await expect(page.getByTestId('embed-slots-table')).toBeVisible()
await page.getByTestId('slot-create-btn').click()
await expect(page.getByTestId('slot-form-dialog')).toBeVisible()
const desktop = page.getByTestId('embed-preview-desktop')
// 交互窗口内记录全部 /v1/ 请求(表单/预览均为纯本地)
const apiCalls: string[] = []
const onRequest = (req: Request): void => {
const path = new URL(req.url()).pathname
if (path.startsWith('/v1/')) apiCalls.push(path)
}
page.on('request', onRequest)
try {
await page.getByTestId('slot-form-name').fill('零接口预览')
await page.getByTestId('slot-form-position').click()
await page.getByTestId('slot-form-position-left').click()
await page.getByTestId('slot-form-size').click()
await page.getByTestId('slot-form-size-sm').click()
await page.getByTestId('slot-form-lang').click()
await page.getByTestId('slot-form-lang-en').click()
await page.getByTestId('slot-form-image-fit-cover').click()
await expect(desktop.getByTestId('embed-preview-stage')).toHaveClass(/is-cover/)
// collapsedDefault=true → 桌面预览初始收起为把手(把手点击可本地展开)
await page.getByTestId('slot-form-collapsed').click()
await expect(desktop.getByTestId('embed-preview-handle')).toBeVisible()
await desktop.getByTestId('embed-preview-handle').click()
await expect(desktop.getByTestId('embed-preview-stage')).toBeVisible()
await page.getByTestId('slot-form-offset-x').locator('input').fill('20')
await page.keyboard.press('Tab')
await page.getByTestId('slot-form-offset-bottom').locator('input').fill('48')
await page.keyboard.press('Tab')
await page.getByTestId('slot-form-z-index').locator('input').fill('1500')
await page.keyboard.press('Tab')
await page.getByTestId('slot-form-interval').locator('input').fill('6000')
await page.keyboard.press('Tab')
// 静置收尾:预览随动后无任何后续/防抖请求
await page.waitForTimeout(600)
expect(apiCalls).toEqual([])
} finally {
page.off('request', onRequest)
}
// 取消退出:不保存(零写入旅程闭环)
await page.getByTestId('slot-form-cancel').click()
await expect(page.getByTestId('slot-form-dialog')).toBeHidden()
})
})