feat(embed,viewer): UI-01 complete carousel, loader config and accessibility
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,22 @@
|
||||
// packages/embed-loader/src/params.test.js
|
||||
// 纯函数单测:data-* 解析/缺省回退/夹取、窄屏强制、URL 拼接、高度夹取。
|
||||
// DOM/宿主交互行为由 Task 4 Playwright 宿主页覆盖(计划注明 node 模拟 DOM 太薄)。
|
||||
// 纯函数单测:data-* 解析/缺省回退/夹取、显式覆盖表(缺省 ≠ 覆盖)、窄屏强制、
|
||||
// URL 拼接、高度夹取、后台设置归一/合并优先级、config 消息裁决、消息类型白名单。
|
||||
// DOM/宿主交互行为由 Playwright 宿主页覆盖(计划注明 node 模拟 DOM 太薄)。
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
MESSAGE_TYPES,
|
||||
buildEmbedUrl,
|
||||
clampHeight,
|
||||
mergeBackendConfig,
|
||||
normalizeBackendSettings,
|
||||
normalizeInterval,
|
||||
parseConfigMessage,
|
||||
parseEmbedConfig,
|
||||
SIZE_WIDTH,
|
||||
} from './params.js';
|
||||
|
||||
describe('parseEmbedConfig', () => {
|
||||
it('空 dataset 全缺省(slot null → 调用方 no-op)', () => {
|
||||
it('空 dataset 全缺省(slot null → 调用方 no-op)+ UI-01 新字段默认', () => {
|
||||
const c = parseEmbedConfig({});
|
||||
expect(c).toEqual({
|
||||
slot: null,
|
||||
@@ -19,6 +25,22 @@ describe('parseEmbedConfig', () => {
|
||||
intervalMs: 5000,
|
||||
size: 'md',
|
||||
collapsedDefault: false,
|
||||
imageFit: 'contain',
|
||||
offsetX: 16,
|
||||
offsetBottom: 24,
|
||||
zIndex: 1000,
|
||||
pollMs: null,
|
||||
narrow: false,
|
||||
explicit: {
|
||||
lang: false,
|
||||
position: false,
|
||||
interval: false,
|
||||
size: false,
|
||||
imageFit: false,
|
||||
offsetX: false,
|
||||
offsetBottom: false,
|
||||
zIndex: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,12 +50,15 @@ describe('parseEmbedConfig', () => {
|
||||
expect(parseEmbedConfig({ slot: ' ' }).slot).toBeNull();
|
||||
});
|
||||
|
||||
it('lang/position 白名单外静默回退缺省', () => {
|
||||
it('lang/position/imageFit 白名单外静默回退缺省', () => {
|
||||
expect(parseEmbedConfig({ slot: 'a', lang: 'zh-CN' }).lang).toBe('zh-CN');
|
||||
expect(parseEmbedConfig({ slot: 'a', lang: 'en' }).lang).toBe('en');
|
||||
expect(parseEmbedConfig({ slot: 'a', lang: 'fr' }).lang).toBe('auto');
|
||||
expect(parseEmbedConfig({ slot: 'a', position: 'left' }).position).toBe('left');
|
||||
expect(parseEmbedConfig({ slot: 'a', position: 'top' }).position).toBe('right');
|
||||
expect(parseEmbedConfig({ slot: 'a', imageFit: 'cover' }).imageFit).toBe('cover');
|
||||
expect(parseEmbedConfig({ slot: 'a', imageFit: 'fill' }).imageFit).toBe('contain');
|
||||
expect(parseEmbedConfig({ slot: 'a', imageFit: 'cover' }).explicit.imageFit).toBe(true);
|
||||
});
|
||||
|
||||
it('interval 解析失败取缺省、越界夹取到 [3000, 30000]', () => {
|
||||
@@ -45,35 +70,222 @@ describe('parseEmbedConfig', () => {
|
||||
expect(parseEmbedConfig({ slot: 'a', interval: '2500' }).intervalMs).toBe(3000);
|
||||
});
|
||||
|
||||
it('offsetX/offsetBottom/zIndex 数值夹取到契约范围,非法回退缺省', () => {
|
||||
expect(parseEmbedConfig({ slot: 'a', offsetX: '200' }).offsetX).toBe(120);
|
||||
expect(parseEmbedConfig({ slot: 'a', offsetX: '-5' }).offsetX).toBe(0);
|
||||
expect(parseEmbedConfig({ slot: 'a', offsetX: '64' }).offsetX).toBe(64);
|
||||
expect(parseEmbedConfig({ slot: 'a', offsetBottom: '999' }).offsetBottom).toBe(240);
|
||||
expect(parseEmbedConfig({ slot: 'a', offsetBottom: 'abc' }).offsetBottom).toBe(24);
|
||||
expect(parseEmbedConfig({ slot: 'a', zIndex: '99999999999' }).zIndex).toBe(2147483000);
|
||||
expect(parseEmbedConfig({ slot: 'a', zIndex: '0' }).zIndex).toBe(1);
|
||||
expect(parseEmbedConfig({ slot: 'a', zIndex: '3000' }).zIndex).toBe(3000);
|
||||
});
|
||||
|
||||
it('窄屏(<480px):强制 sm 尺寸 + 默认收起,覆盖 dataset 尺寸语义', () => {
|
||||
const c = parseEmbedConfig({ slot: 'a', size: 'md' }, { narrow: true });
|
||||
expect(c.size).toBe('sm');
|
||||
expect(c.narrow).toBe(true);
|
||||
expect(c.explicit.size).toBe(false);
|
||||
});
|
||||
|
||||
it('md 尺寸宽 320(UI-01 §5.1 视觉变更),sm 280', () => {
|
||||
expect(SIZE_WIDTH.md).toBe(320);
|
||||
expect(SIZE_WIDTH.sm).toBe(280);
|
||||
});
|
||||
});
|
||||
|
||||
describe('explicit 覆盖表(缺省 ≠ 显式覆盖)', () => {
|
||||
it('仅真正提供的字段进 explicit(空串视为缺省)', () => {
|
||||
const c = parseEmbedConfig({ slot: 'a', offsetX: '40', lang: '', imageFit: 'cover' });
|
||||
expect(c.explicit.offsetX).toBe(true);
|
||||
expect(c.explicit.imageFit).toBe(true);
|
||||
expect(c.explicit.lang).toBe(false);
|
||||
expect(c.explicit.interval).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeBackendSettings(iframe config 消息 / 公开端 settings)', () => {
|
||||
it('合法输入原样通过', () => {
|
||||
expect(
|
||||
normalizeBackendSettings({
|
||||
position: 'left',
|
||||
size: 'sm',
|
||||
intervalMs: 8000,
|
||||
lang: 'en',
|
||||
collapsedDefault: true,
|
||||
imageFit: 'cover',
|
||||
offsetX: 40,
|
||||
offsetBottom: 80,
|
||||
zIndex: 2000,
|
||||
}),
|
||||
).toEqual({
|
||||
position: 'left',
|
||||
size: 'sm',
|
||||
intervalMs: 8000,
|
||||
lang: 'en',
|
||||
collapsedDefault: true,
|
||||
imageFit: 'cover',
|
||||
offsetX: 40,
|
||||
offsetBottom: 80,
|
||||
zIndex: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
it('非法值逐字段回退缺省(枚举/布尔/数值夹取)', () => {
|
||||
expect(
|
||||
normalizeBackendSettings({
|
||||
position: 'top',
|
||||
size: 'xl',
|
||||
intervalMs: 'x',
|
||||
lang: 1,
|
||||
collapsedDefault: 'yes',
|
||||
imageFit: null,
|
||||
offsetX: 999,
|
||||
offsetBottom: -1,
|
||||
zIndex: 0,
|
||||
}),
|
||||
).toEqual({
|
||||
position: 'right',
|
||||
size: 'md',
|
||||
intervalMs: 5000,
|
||||
lang: 'auto',
|
||||
collapsedDefault: false,
|
||||
imageFit: 'contain',
|
||||
offsetX: 120,
|
||||
offsetBottom: 0,
|
||||
zIndex: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('非对象输入返回 null(整体忽略)', () => {
|
||||
expect(normalizeBackendSettings(null)).toBeNull();
|
||||
expect(normalizeBackendSettings('x')).toBeNull();
|
||||
expect(normalizeBackendSettings(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeBackendConfig(优先级:data-* 显式 > 后台 > 默认)', () => {
|
||||
const backend = {
|
||||
position: 'left',
|
||||
size: 'sm',
|
||||
intervalMs: 9000,
|
||||
lang: 'en',
|
||||
collapsedDefault: true,
|
||||
imageFit: 'cover',
|
||||
offsetX: 48,
|
||||
offsetBottom: 96,
|
||||
zIndex: 2500,
|
||||
};
|
||||
|
||||
it('未显式覆盖的字段采用后台值', () => {
|
||||
const c = mergeBackendConfig(parseEmbedConfig({ slot: 'a' }), backend);
|
||||
expect(c.position).toBe('left');
|
||||
expect(c.intervalMs).toBe(9000);
|
||||
expect(c.imageFit).toBe('cover');
|
||||
expect(c.offsetX).toBe(48);
|
||||
expect(c.offsetBottom).toBe(96);
|
||||
expect(c.zIndex).toBe(2500);
|
||||
expect(c.collapsedDefault).toBe(true);
|
||||
});
|
||||
|
||||
it('非窄屏时 collapsedDefault 恒 false(收起默认由 iframe 侧按服务端设置下发)', () => {
|
||||
expect(parseEmbedConfig({ slot: 'a' }, { narrow: false }).collapsedDefault).toBe(false);
|
||||
it('data-* 显式覆盖的字段不被后台刷新冲掉', () => {
|
||||
const c = mergeBackendConfig(
|
||||
parseEmbedConfig({ slot: 'a', position: 'right', offsetX: '10', imageFit: 'contain' }),
|
||||
backend,
|
||||
);
|
||||
expect(c.position).toBe('right');
|
||||
expect(c.offsetX).toBe(10);
|
||||
expect(c.imageFit).toBe('contain');
|
||||
expect(c.intervalMs).toBe(9000); // 未显式 → 后台
|
||||
});
|
||||
|
||||
it('后台缺字段/非法 → 契约默认(旧投影行兼容)', () => {
|
||||
const c = mergeBackendConfig(parseEmbedConfig({ slot: 'a' }), { position: 'left' });
|
||||
expect(c.imageFit).toBe('contain');
|
||||
expect(c.offsetX).toBe(16);
|
||||
expect(c.zIndex).toBe(1000);
|
||||
expect(c.collapsedDefault).toBe(false);
|
||||
});
|
||||
|
||||
it('后台非法输入 → 返回原配置(忽略整条消息)', () => {
|
||||
const base = parseEmbedConfig({ slot: 'a', offsetX: '10' });
|
||||
expect(mergeBackendConfig(base, null)).toBe(base);
|
||||
});
|
||||
|
||||
it('窄屏强制优先于后台:size 恒 sm、collapsedDefault 恒 true', () => {
|
||||
const c = mergeBackendConfig(parseEmbedConfig({ slot: 'a' }, { narrow: true }), {
|
||||
...backend,
|
||||
size: 'md',
|
||||
collapsedDefault: false,
|
||||
});
|
||||
expect(c.size).toBe('sm');
|
||||
expect(c.collapsedDefault).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseConfigMessage(版本闸 + 校验)', () => {
|
||||
const settings = {
|
||||
position: 'right',
|
||||
size: 'md',
|
||||
intervalMs: 5000,
|
||||
lang: 'auto',
|
||||
collapsedDefault: false,
|
||||
imageFit: 'contain',
|
||||
offsetX: 16,
|
||||
offsetBottom: 24,
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
it('高版本 + 合法 settings → 归一结果', () => {
|
||||
const r = parseConfigMessage({ photoliveEmbed: true, type: 'config', configVersion: 3, settings }, 1);
|
||||
expect(r).toEqual({ configVersion: 3, settings });
|
||||
});
|
||||
|
||||
it('版本不高(旧请求/等值)→ null(旧配置不得覆盖新配置)', () => {
|
||||
expect(parseConfigMessage({ configVersion: 1, settings }, 1)).toBeNull();
|
||||
expect(parseConfigMessage({ configVersion: 2, settings }, 5)).toBeNull();
|
||||
});
|
||||
|
||||
it('首条消息(当前版本 0/null)接受', () => {
|
||||
expect(parseConfigMessage({ configVersion: 1, settings }, 0)).not.toBeNull();
|
||||
expect(parseConfigMessage({ configVersion: 1, settings }, null)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('形状非法(缺版本/缺 settings/非对象)→ null', () => {
|
||||
expect(parseConfigMessage({ settings }, 0)).toBeNull();
|
||||
expect(parseConfigMessage({ configVersion: 2 }, 0)).toBeNull();
|
||||
expect(parseConfigMessage(null, 0)).toBeNull();
|
||||
expect(parseConfigMessage('x', 0)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildEmbedUrl', () => {
|
||||
it('拼接 lang/pos/interval 查询参数', () => {
|
||||
const url = buildEmbedUrl('https://ov.example.com', 'tok', {
|
||||
lang: 'zh-CN',
|
||||
position: 'left',
|
||||
intervalMs: 7000,
|
||||
});
|
||||
expect(url).toBe('https://ov.example.com/embed/tok?lang=zh-CN&pos=left&interval=7000');
|
||||
it('显式字段拼接 interval/fit;缺省不拼(后台设置经 iframe 响应生效)', () => {
|
||||
const explicit = parseEmbedConfig({ slot: 'a', interval: '7000', imageFit: 'cover' });
|
||||
expect(buildEmbedUrl('https://ov.example.com', 'tok', explicit)).toBe(
|
||||
'https://ov.example.com/embed/tok?lang=auto&pos=right&interval=7000&fit=cover',
|
||||
);
|
||||
const bare = parseEmbedConfig({ slot: 'a' });
|
||||
expect(buildEmbedUrl('https://ov.example.com', 'tok', bare)).toBe(
|
||||
'https://ov.example.com/embed/tok?lang=auto&pos=right',
|
||||
);
|
||||
});
|
||||
|
||||
it('poll 显式时透传(dev/e2e 加速;生产构建忽略)', () => {
|
||||
const c = parseEmbedConfig({ slot: 'a', poll: '2000' });
|
||||
expect(buildEmbedUrl('https://ov.example.com', 'tok', c)).toContain('&poll=2000');
|
||||
});
|
||||
|
||||
it('slot token 编码(防路径注入)', () => {
|
||||
const url = buildEmbedUrl('https://ov.example.com', 'a/b?c=1', {
|
||||
lang: 'auto',
|
||||
position: 'right',
|
||||
intervalMs: 5000,
|
||||
});
|
||||
const url = buildEmbedUrl('https://ov.example.com', 'a/b?c=1', parseEmbedConfig({ slot: 'a/b?c=1' }));
|
||||
expect(url.startsWith('https://ov.example.com/embed/a%2Fb%3Fc%3D1?')).toBe(true);
|
||||
expect(url).toContain('lang=auto&pos=right&interval=5000');
|
||||
expect(url).toContain('lang=auto&pos=right');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MESSAGE_TYPES(消息类型白名单)', () => {
|
||||
it('resize/collapse/expand/config/destroy 全集', () => {
|
||||
expect([...MESSAGE_TYPES].sort()).toEqual(['collapse', 'config', 'destroy', 'expand', 'resize'].sort());
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user