0238f505c4
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>
202 lines
8.9 KiB
TypeScript
202 lines
8.9 KiB
TypeScript
// 展示位管理 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)
|
||
})
|
||
})
|