Files
photo-live/apps/admin/src/utils/system/embedSlots.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

132 lines
4.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.
import { describe, expect, it } from 'vitest'
import zhMessages from '@/locales/langs/zh.json'
import enMessages from '@/locales/langs/en.json'
import {
bindChangeKind,
buildEmbedSnippet,
buildSettingsPayload,
buildSlotEditPayload,
clampIntervalMs,
DEFAULT_EMBED_SETTINGS,
EMBED_ORIGIN_FALLBACK,
embedOriginFromEnv,
type EmbedSettingsDraft
} from './embedSlots'
/** 契约形状的既有条目(编辑载荷断言基准) */
const CURRENT_ITEM: Api.EmbedSlot.Item = {
id: 'slot-1',
publicToken: 'tok0000000000000000000',
name: '官网首页悬浮位',
enabled: true,
albumId: null,
albumTitle: null,
configVersion: 1,
settings: { ...DEFAULT_EMBED_SETTINGS },
updatedAt: '2026-09-20T10:00:00.000Z'
}
describe('buildSettingsPayload 表单 → settings 载荷', () => {
it('缺省草稿 → 契约缺省值全集(整体提交:字段齐全)', () => {
expect(buildSettingsPayload({ ...DEFAULT_EMBED_SETTINGS })).toEqual({
position: 'right',
size: 'md',
intervalMs: 5000,
lang: 'auto',
collapsedDefault: false
})
})
it('intervalMs 越界/非有限值 → 夹取到契约区间(键盘输入兜底)', () => {
expect(clampIntervalMs(1)).toBe(3000)
expect(clampIntervalMs(2999.6)).toBe(3000)
expect(clampIntervalMs(100000)).toBe(30000)
expect(clampIntervalMs(Number.NaN)).toBe(5000)
expect(clampIntervalMs(Number.POSITIVE_INFINITY)).toBe(5000)
const draft: EmbedSettingsDraft = { ...DEFAULT_EMBED_SETTINGS, intervalMs: 2000 }
expect(buildSettingsPayload(draft).intervalMs).toBe(3000)
})
})
describe('buildSlotEditPayload 编辑载荷收敛', () => {
it('name 与 settings 均未变化 → 空载荷(不制造服务端 no-op)', () => {
expect(
buildSlotEditPayload(CURRENT_ITEM, CURRENT_ITEM.name, { ...DEFAULT_EMBED_SETTINGS })
).toEqual({})
})
it('仅 name 变化 → 只送 name;settings 变化 → 送完整 settings(整体替换)', () => {
expect(
buildSlotEditPayload(CURRENT_ITEM, ' 活动页左侧位 ', { ...DEFAULT_EMBED_SETTINGS })
).toEqual({
name: '活动页左侧位'
})
const changed: EmbedSettingsDraft = {
...DEFAULT_EMBED_SETTINGS,
position: 'left',
size: 'sm',
intervalMs: 8000,
lang: 'zh-CN',
collapsedDefault: true
}
expect(buildSlotEditPayload(CURRENT_ITEM, CURRENT_ITEM.name, changed)).toEqual({
settings: changed
})
})
})
describe('buildEmbedSnippet 嵌入代码拼接', () => {
it('EMBED_ORIGIN + token → /embed/v1/loader.js 引用(尾斜杠容忍)', () => {
expect(buildEmbedSnippet('https://view.example.com', 'abc123def456ghi789jklmno')).toBe(
'<script src="https://view.example.com/embed/v1/loader.js" data-slot="abc123def456ghi789jklmno" defer></script>'
)
expect(buildEmbedSnippet('http://127.0.0.1:5175//', 'tok')).toBe(
'<script src="http://127.0.0.1:5175/embed/v1/loader.js" data-slot="tok" defer></script>'
)
})
it('环境基址:VITE_EMBED_ORIGIN 缺省/空白兜底本机 dev', () => {
expect(embedOriginFromEnv({})).toBe(EMBED_ORIGIN_FALLBACK)
expect(embedOriginFromEnv({ VITE_EMBED_ORIGIN: ' ' })).toBe(EMBED_ORIGIN_FALLBACK)
expect(embedOriginFromEnv({ VITE_EMBED_ORIGIN: ' https://cdn.example.com/ ' })).toBe(
'https://cdn.example.com'
)
})
})
describe('bindChangeKind 绑定变更分类', () => {
it('null→id 绑定 / id→id 换绑 / id→null 解绑 / 值不变 none', () => {
expect(bindChangeKind(null, 'album-1')).toBe('bind')
expect(bindChangeKind('album-1', 'album-2')).toBe('rebind')
expect(bindChangeKind('album-1', null)).toBe('unbind')
expect(bindChangeKind('album-1', 'album-1')).toBe('none')
expect(bindChangeKind(null, null)).toBe('none')
})
})
describe('绑定变更确认文案(A22:configVersion 将递增,官网无需改动)', () => {
const zh = zhMessages.embedSlotsManage as Record<string, unknown>
const en = enMessages.embedSlotsManage as Record<string, unknown>
const zhBind = zh.bind as Record<string, string>
const enBind = en.bind as Record<string, string>
it('zh/en 键集合一致(embedSlotsManage 子树)', () => {
const keysOf = (obj: Record<string, unknown>, prefix = ''): string[] =>
Object.entries(obj).flatMap(([key, value]) =>
typeof value === 'object' && value !== null
? keysOf(value as Record<string, unknown>, `${prefix}${key}.`)
: [`${prefix}${key}`]
)
expect(keysOf(en)).toEqual(keysOf(zh))
})
it('三类确认文案均携带 A22 短语(换绑不改官网:loader 只认 token)', () => {
for (const key of ['confirmBind', 'confirmRebind', 'confirmUnbind']) {
expect(zhBind[key]).toContain('configVersion 将递增')
expect(zhBind[key]).toContain('官网无需改动')
expect(enBind[key]).toContain('configVersion will increase')
expect(enBind[key]).toContain('no change is needed on the host site')
}
})
})