// 展示位配置表单 + 本地实时预览 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 { 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 { 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( `` ) 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() }) })