Files
photo-live/apps/admin/e2e/embed-slots.spec.ts
T

275 lines
13 KiB
TypeScript
Raw Normal View History

// 展示位管理 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 挂载 + 模拟点赞按钮在工具区;预览帧内为
// 真实 EmbedStream(.stream-viewport)且模拟点赞出心形(WYSIWYG 证据)。
await expect(page.getByTestId('embed-preview-v2-iframe')).toBeVisible()
await expect(page.getByTestId('embed-preview-sim-like')).toBeVisible()
const previewFrame = page.frames().find((f) => f.url().includes('/embed-preview'))
expect(previewFrame).toBeDefined()
await expect(previewFrame!.locator('.stream-viewport')).toHaveCount(1)
await page.getByTestId('embed-preview-sim-like').click()
await expect(previewFrame!.locator('.heart').first()).toBeVisible({ timeout: 5_000 })
// 速度调整(24 px/s):滑轨点击定位((24-8)/(32-8)*180px ≈ 120)后断言值。
const runway = page.getByTestId('slot-form-speed').locator('.el-slider__runway')
await runway.click({ position: { x: 120, y: 4 } })
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)
})
})