Files
photo-live/apps/admin/e2e/embed-slots.spec.ts
T
2026-09-22 18:10:53 +08:00

268 lines
12 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(P4b Task 5,真实后端:cn 8080 + ov 8081 + admin dev 5176
// 由 playwright.config webServer 拉起或复用)。旅程:新建(设置表单)→ 行
// 回显(未绑定/v1)→ 绑定相册(搜索选中 → A22 确认文案 → v2 + 绑定回显)→
// 嵌入代码卡(token + /embed/v1/loader.js 引用)→ ov 公开 feed 返回相册照片
// (seedFixture gov-photos 直种 cn + sync-flush 经 bootstrap/真实发送管线投影
// photos_ov)→ 换绑另一相册(configVersion+1,feed 切换新相册照片——A22 换绑
// 不改官网)→ 停用(feed 404)。选择器一律 data-test;文案断言仅用稳定短语。
import { expect, test, type Page } from '@playwright/test'
import { adminCredentials, loginAsAdmin, ovBaseUrl, seedFixture } 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 V2_SLOT_NAME = `E2E展示位v2 ${SUFFIX}`
const ALBUM_A_TITLE = `E2E嵌入相册A ${SUFFIX}`
const ALBUM_B_TITLE = `E2E嵌入相册B ${SUFFIX}`
const ALBUM_C_TITLE = `E2E嵌入相册C ${SUFFIX}`
interface SeededAlbum {
id: string
title: string
photoCount: number
}
/** API 建相册(zh 标题)+ gov-photos 直种已发布照片(含 thumb 变体行——
* ov 公开 feed photos 的必要形状)+ bootstrap 投影到 ov 副本。 */
async function seedOvVisibleAlbum(
page: Page,
title: string,
photoCount: number
): Promise<SeededAlbum> {
const { account, password } = adminCredentials()
const login = await page.request.post('/v1/auth/password', { data: { account, password } })
expect(login.ok()).toBeTruthy()
const sessionToken = ((await login.json()) as { sessionToken: string }).sessionToken
const created = await page.request.post('/v1/albums', {
headers: { Authorization: `Bearer ${sessionToken}` },
data: {
defaultLocale: 'zh-CN',
startAt: '2026-10-01T10:00:00.000Z',
translations: [{ locale: 'zh-CN', title }]
}
})
expect(created.ok()).toBeTruthy()
const { id } = (await created.json()) as { id: string }
seedFixture('gov-photos', id, account, String(photoCount))
// bootstrap(当前态快照入 outbox)+ 真实发送管线排空 → albums_ov/photos_ov 就位
seedFixture('sync-flush', id)
return { id, title, photoCount }
}
/** 列表内定位展示位行 */
function slotRow(page: Page, name: string) {
return page.getByTestId('embed-slots-table').locator('tr', { hasText: name })
}
/** ov 公开嵌入 feed 形状(断言关心的子集;settings 为 UI-04 v2 投影断言) */
interface FeedBody {
album: { publicToken: string; title: string }
photos: Array<{ photoId: string }>
settings: { displayMode: string; scrollSpeed: number; reactionsEnabled: boolean }
}
/** ov 公开嵌入 feed(观众侧权威断言;断言侧直接读 ov 副本端点) */
async function ovFeed(
page: Page,
slotToken: string
): Promise<{ status: number; body: FeedBody | null }> {
const res = await page.request.get(`${ovBaseUrl()}/v1/public/embed-slots/${slotToken}`)
return { status: res.status(), body: res.ok() ? ((await res.json()) as FeedBody) : null }
}
test.describe.serial('embed slot management (P4b Task 5)', () => {
let slotToken = ''
let configVersion = 1
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()
await page.getByTestId('slot-form-name').fill(SLOT_NAME)
// UI-04:新建默认「照片上滚」(v2)——本用例回归旧版轮播路径,先显式切换
await page.getByTestId('slot-form-mode-carousel').click()
// 设置表单:位置/间隔(其余取缺省值;编辑载荷映射由 vitest 覆盖)
await page.getByTestId('slot-form-position').click()
await page.getByTestId('slot-form-position-left').click()
await page.getByTestId('slot-form-interval').locator('input').fill('8000')
await page.getByTestId('slot-form-submit').click()
await expect(page.getByTestId('slot-form-dialog')).toBeHidden()
const row = slotRow(page, SLOT_NAME)
await expect(row).toBeVisible()
await expect(row.getByTestId('slot-album-unbound')).toBeVisible()
await expect(row.getByTestId('slot-config-version')).toHaveText('v1')
// 嵌入代码卡:token + loader 引用(EMBED_ORIGIN 缺省本机 viewer dev)
await row.getByTestId('slot-code-btn').click()
await expect(page.getByTestId('embed-code-dialog')).toBeVisible()
slotToken = ((await page.getByTestId('embed-code-token').textContent()) ?? '').trim()
expect(slotToken).toMatch(/^[0-9a-z]{20,32}$/)
await expect(page.getByTestId('embed-code-snippet')).toContainText(
`http://127.0.0.1:5175/embed/v1/loader.js`
)
await expect(page.getByTestId('embed-code-snippet')).toContainText(`data-slot="${slotToken}"`)
await page.getByTestId('embed-code-close').click()
await expect(page.getByTestId('embed-code-dialog')).toBeHidden()
})
test('绑定相册:搜索选中 → A22 确认文案 → v2 + ov 公开 feed 返回相册照片', async ({ page }) => {
const albumA = await seedOvVisibleAlbum(page, ALBUM_A_TITLE, 3)
await loginAsAdmin(page)
await page.goto('/#/system/embed-slots')
const row = slotRow(page, SLOT_NAME)
await expect(row).toBeVisible()
await row.getByTestId('slot-bind-btn').click()
await expect(page.getByTestId('slot-bind-dialog')).toBeVisible()
await expect(page.getByTestId('slot-bind-current')).toHaveText('未绑定')
const select = page.getByTestId('slot-bind-select')
await select.click()
await select.locator('input').fill(SUFFIX)
await page.getByTestId(`slot-bind-option-${albumA.id}`).click()
// 提交 → 确认框携带 A22 文案(configVersion 将递增,官网无需改动)
await page.getByTestId('slot-bind-submit').click()
const confirmBox = page.locator('.el-message-box')
await expect(confirmBox).toContainText('configVersion 将递增')
await expect(confirmBox).toContainText('官网无需改动')
await page.getByRole('button', { name: '确定' }).click()
await expect(page.getByTestId('slot-bind-dialog')).toBeHidden()
await expect(
page.locator('.el-message').filter({ hasText: 'configVersion 已递增至 2' })
).toBeVisible()
await expect(row.getByTestId('slot-album')).toContainText(ALBUM_A_TITLE)
await expect(row.getByTestId('slot-config-version')).toHaveText('v2')
configVersion = 2
// slot.updated 事件不随 bootstrap 走:单独排空 → ov 副本就位
seedFixture('sync-flush')
const feed = await ovFeed(page, slotToken)
expect(feed.status).toBe(200)
expect(feed.body.album.publicToken).toBeTruthy()
expect(feed.body.photos).toHaveLength(albumA.photoCount)
})
test('换绑相册:configVersion+1 → ov feed 切换新相册照片(A22 换绑不改官网)', async ({
page
}) => {
const albumB = await seedOvVisibleAlbum(page, ALBUM_B_TITLE, 2)
await loginAsAdmin(page)
await page.goto('/#/system/embed-slots')
const row = slotRow(page, SLOT_NAME)
await expect(row.getByTestId('slot-config-version')).toHaveText(`v${configVersion}`)
await row.getByTestId('slot-bind-btn').click()
await expect(page.getByTestId('slot-bind-dialog')).toBeVisible()
await expect(page.getByTestId('slot-bind-current')).toContainText(ALBUM_A_TITLE)
const select = page.getByTestId('slot-bind-select')
await select.click()
await select.locator('input').fill(SUFFIX)
await page.getByTestId(`slot-bind-option-${albumB.id}`).click()
await page.getByTestId('slot-bind-submit').click()
await expect(page.locator('.el-message-box')).toContainText('configVersion 将递增')
await page.getByRole('button', { name: '确定' }).click()
await expect(page.getByTestId('slot-bind-dialog')).toBeHidden()
configVersion += 1
await expect(row.getByTestId('slot-config-version')).toHaveText(`v${configVersion}`)
await expect(row.getByTestId('slot-album')).toContainText(ALBUM_B_TITLE)
seedFixture('sync-flush')
// 同一 token(嵌入代码不变):feed 反映新相册照片集
const feed = await ovFeed(page, slotToken)
expect(feed.status).toBe(200)
expect(feed.body.photos).toHaveLength(albumB.photoCount)
})
test('停用:开关 → configVersion 再增 → ov feed 404(软下线)', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('/#/system/embed-slots')
const row = slotRow(page, SLOT_NAME)
await expect(row.getByTestId('slot-enabled-switch')).toHaveClass(/is-checked/)
await row.getByTestId('slot-enabled-switch').click()
await expect(
page.locator('.el-message').filter({ hasText: 'configVersion 已递增至' })
).toBeVisible()
configVersion += 1
await expect(row.getByTestId('slot-config-version')).toHaveText(`v${configVersion}`)
await expect(row.getByTestId('slot-enabled-switch')).not.toHaveClass(/is-checked/)
seedFixture('sync-flush')
expect((await ovFeed(page, slotToken)).status).toBe(404)
})
test('UI-04 v2 新建:照片上滚表单/真实组件预览/模拟按钮 → v2 代码卡 → ov settings 投影', async ({
page
}) => {
const albumC = await seedOvVisibleAlbum(page, ALBUM_C_TITLE, 2)
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()
await page.getByTestId('slot-form-name').fill(V2_SLOT_NAME)
// 默认即「照片上滚」:v2 字段组可见、v1 布局字段隐藏(§7.2 不偷偷生效)。
await expect(page.getByTestId('slot-form-mode-stream')).toBeVisible()
await expect(page.getByTestId('slot-form-fixed-size')).toContainText('195')
await expect(page.getByTestId('slot-form-speed')).toBeVisible()
await expect(page.getByTestId('slot-form-reactions')).toBeVisible()
await expect(page.getByTestId('slot-form-interval')).toHaveCount(0)
await expect(page.getByTestId('slot-form-size')).toHaveCount(0)
// 真实组件预览(§8.2):iframe 挂载 + 模拟点赞按钮在工具区。
await expect(page.getByTestId('embed-preview-v2-iframe')).toBeVisible()
await expect(page.getByTestId('embed-preview-sim-like')).toBeVisible()
// 速度调整(24 px/s):滑轨点击定位((24-8)/(32-8)*180px ≈ 120)后断言值。
await page.getByTestId('slot-form-speed').click({ position: { x: 120, y: 10 } })
await expect(page.getByTestId('slot-form-speed-value')).toContainText('24')
await page.getByTestId('slot-form-submit').click()
await expect(page.getByTestId('slot-form-dialog')).toBeHidden()
const row = slotRow(page, V2_SLOT_NAME)
await expect(row).toBeVisible()
// 嵌入代码卡:v2 入口(§8.2 生成的接入代码使用 v2)。
await row.getByTestId('slot-code-btn').click()
await expect(page.getByTestId('embed-code-dialog')).toBeVisible()
const v2Token = ((await page.getByTestId('embed-code-token').textContent()) ?? '').trim()
expect(v2Token).toMatch(/^[0-9a-z]{20,32}$/)
await expect(page.getByTestId('embed-code-snippet')).toContainText(
`http://127.0.0.1:5175/embed/v2/loader.js`
)
await expect(page.getByTestId('embed-code-snippet')).toContainText(`data-slot="${v2Token}"`)
await page.getByTestId('embed-code-close').click()
// 绑定相册 + 排空 → ov 公开 feed settings 携带 v2 投影(保存与真实效果一致)。
await row.getByTestId('slot-bind-btn').click()
await page.getByTestId('slot-bind-select').click()
await page.getByTestId('slot-bind-select').locator('input').fill(SUFFIX)
await page.getByTestId(`slot-bind-option-${albumC.id}`).click()
await page.getByTestId('slot-bind-submit').click()
await page.locator('.el-message-box').getByRole('button', { name: '确定' }).click()
await expect(page.getByTestId('slot-bind-dialog')).toBeHidden()
seedFixture('sync-flush')
const feed = await ovFeed(page, v2Token)
expect(feed.status).toBe(200)
expect(feed.body.settings.displayMode).toBe('vertical-stream')
expect(feed.body.settings.scrollSpeed).toBe(24)
expect(feed.body.settings.reactionsEnabled).toBe(true)
})
})