ac23a4c40e
Co-Authored-By: Claude Code <noreply@anthropic.com>
234 lines
11 KiB
TypeScript
234 lines
11 KiB
TypeScript
// 展示位配置表单 + 本地实时预览 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()
|
||
})
|
||
})
|