Files
photo-live/apps/admin/e2e/embed-slots.spec.ts
T
浅念 0238f505c4 feat(admin): embed slot management with code snippet
P4b Task 5:/system/embed-slots 管理页(Task 8/9 页面模式:useCursorList
游标累积 + 对话框治理)。列表(名称/绑定相册/启停/configVersion/更新时间,
启停开关行内 PATCH → configVersion+1 toast 提示)+ 新建/编辑对话框
(设置表单位置/尺寸/间隔/语言/默认收起,intervalMs 契约区间夹取;编辑按
整体替换语义提交完整 settings)+ 绑定相册对话框(相册标题/ID 搜索选中 →
确认框携带 A22 文案「configVersion 将递增,官网无需改动」;清空即解绑)+
嵌入代码卡({EMBED_ORIGIN}/embed/v1/loader.js + data-slot 令牌,一键复制;
EMBED_ORIGIN=VITE_EMBED_ORIGIN 缺省本机 viewer dev,.env.example 登记)。

- API 层 src/api/embedSlots.ts + types/api/embedSlot.d.ts(契约镜像)
- vitest 9 例:表单→载荷(缺省+夹取)、编辑载荷收敛、snippet 拼接、
  环境基址兜底、绑定变更分类、zh/en 键一致 + A22 短语断言
- Playwright e2e(真实 cn+ov 后端):新建→绑定(gov-photos 直种 +
  seed-fixture 新增 sync-flush 模式经 bootstrap/真实发送管线投影 photos_ov)
  →嵌入代码卡含 token→ov 公开 feed 返回相册照片→换绑 configVersion+1 +
  feed 切换(A22)→停用 feed 404;admin playwright 配置补 ov webServer
- i18n zh/en(embedSlotsManage.* + menus.system.embedSlots)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 23:25:59 +08:00

202 lines
8.9 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 ALBUM_A_TITLE = `E2E嵌入相册A ${SUFFIX}`
const ALBUM_B_TITLE = `E2E嵌入相册B ${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 形状(断言关心的子集) */
interface FeedBody {
album: { publicToken: string; title: string }
photos: Array<{ photoId: string }>
}
/** 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)
// 设置表单:位置/间隔(其余取缺省值;编辑载荷映射由 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)
})
})