Files
photo-live/packages/embed-loader/src/params.test.js
T

424 lines
16 KiB
JavaScript
Raw Normal View History

// packages/embed-loader/src/params.test.js
// 纯函数单测:data-* 解析/缺省回退/夹取、显式覆盖表(缺省 ≠ 覆盖)、窄屏强制、
// URL 拼接、高度夹取、后台设置归一/合并优先级、config 消息裁决、消息类型白名单。
// DOM/宿主交互行为由 Playwright 宿主页覆盖(计划注明 node 模拟 DOM 太薄)。
import { describe, expect, it } from 'vitest';
import {
MESSAGE_TYPES,
buildEmbedUrl,
buildEmbedUrlV2,
clampHeight,
mergeBackendConfig,
mergeBackendConfigV2,
normalizeBackendSettings,
normalizeBackendSettingsV2,
normalizeInterval,
parseConfigMessage,
parseConfigMessageV2,
parseEmbedConfig,
parseEmbedConfigV2,
SIZE_WIDTH,
} from './params.js';
describe('parseEmbedConfig', () => {
it('空 dataset 全缺省(slot null → 调用方 no-op)+ UI-01 新字段默认', () => {
const c = parseEmbedConfig({});
expect(c).toEqual({
slot: null,
lang: 'auto',
position: 'right',
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,
},
});
});
it('data-slot 保留并去首尾空白;非字符串(缺失)为 null', () => {
expect(parseEmbedConfig({ slot: ' tok ' }).slot).toBe('tok');
expect(parseEmbedConfig({}).slot).toBeNull();
expect(parseEmbedConfig({ slot: ' ' }).slot).toBeNull();
});
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]', () => {
expect(normalizeInterval('8000')).toBe(8000);
expect(normalizeInterval('100')).toBe(3000);
expect(normalizeInterval('999999')).toBe(30000);
expect(normalizeInterval('abc')).toBe(5000);
expect(normalizeInterval(undefined)).toBe(5000);
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('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('显式字段拼接 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', 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');
});
});
describe('MESSAGE_TYPES(消息类型白名单)', () => {
it('resize/collapse/expand/config/destroy 全集', () => {
expect([...MESSAGE_TYPES].sort()).toEqual(['collapse', 'config', 'destroy', 'expand', 'resize'].sort());
});
});
describe('clampHeight', () => {
it('正常值原样(取整)返回', () => {
expect(clampHeight(200)).toBe(200);
expect(clampHeight(200.6)).toBe(201);
});
it('夹取到 [60, 640]', () => {
expect(clampHeight(10)).toBe(60);
expect(clampHeight(9999)).toBe(640);
});
it('缺失/非数值返回 null(忽略该消息)', () => {
expect(clampHeight(null)).toBeNull();
expect(clampHeight(undefined)).toBeNull();
expect(clampHeight('abc')).toBeNull();
expect(clampHeight(NaN)).toBeNull();
expect(clampHeight(Infinity)).toBeNull();
});
});
// ── UI-04 v2(vertical-stream)参数面 ──
describe('parseEmbedConfigV2', () => {
it('空 dataset 全缺省:v2 字段默认 + explicit 全 false', () => {
const c = parseEmbedConfigV2({ slot: 'tok' }, {});
expect(c.version).toBe(2);
expect(c.slot).toBe('tok');
expect(c.lang).toBe('auto');
expect(c.position).toBe('right');
expect(c.speed).toBe(16);
expect(c.reactions).toBe(true);
expect(c.offsetX).toBe(16);
expect(c.offsetBottom).toBe(24);
expect(c.zIndex).toBe(1000);
expect(c.pollMs).toBeNull();
expect(c.explicit).toEqual({
lang: false,
position: false,
speed: false,
reactions: false,
offsetX: false,
offsetBottom: false,
zIndex: false,
});
});
it('data-speed 夹取 8..32;非法回退 16', () => {
expect(parseEmbedConfigV2({ slot: 'a', speed: '1' }, {}).speed).toBe(8);
expect(parseEmbedConfigV2({ slot: 'a', speed: '24' }, {}).speed).toBe(24);
expect(parseEmbedConfigV2({ slot: 'a', speed: '99' }, {}).speed).toBe(32);
expect(parseEmbedConfigV2({ slot: 'a', speed: 'abc' }, {}).speed).toBe(16);
expect(parseEmbedConfigV2({ slot: 'a' }, {}).explicit.speed).toBe(false);
expect(parseEmbedConfigV2({ slot: 'a', speed: '24' }, {}).explicit.speed).toBe(true);
});
it('data-reactions 仅 on/off;其他回退 true', () => {
expect(parseEmbedConfigV2({ slot: 'a', reactions: 'off' }, {}).reactions).toBe(false);
expect(parseEmbedConfigV2({ slot: 'a', reactions: 'on' }, {}).reactions).toBe(true);
expect(parseEmbedConfigV2({ slot: 'a', reactions: 'yes' }, {}).reactions).toBe(true);
});
it('v1 布局字段(size/interval/imageFit)不在 v2 配置面(195×280 硬性)', () => {
const c = parseEmbedConfigV2({ slot: 'a', size: 'md', interval: '8000', imageFit: 'cover' }, {});
expect(c.size).toBeUndefined();
expect(c.intervalMs).toBeUndefined();
expect(c.imageFit).toBeUndefined();
});
});
describe('normalizeBackendSettingsV2 / mergeBackendConfigV2', () => {
it('v2 字段归一:displayMode 白名单 / speed 夹取 / reactions 布尔回退', () => {
const s = normalizeBackendSettingsV2({
position: 'right', size: 'md', intervalMs: 5000, lang: 'auto', collapsedDefault: false,
imageFit: 'contain', offsetX: 16, offsetBottom: 24, zIndex: 1000,
displayMode: 'vertical-stream', scrollSpeed: 20, reactionsEnabled: false,
});
expect(s.displayMode).toBe('vertical-stream');
expect(s.scrollSpeed).toBe(20);
expect(s.reactionsEnabled).toBe(false);
expect(normalizeBackendSettingsV2({ displayMode: 'weird', scrollSpeed: 999, reactionsEnabled: 'x' }).displayMode).toBe('carousel');
expect(normalizeBackendSettingsV2({ scrollSpeed: 999 }).scrollSpeed).toBe(32);
expect(normalizeBackendSettingsV2({ reactionsEnabled: 'x' }).reactionsEnabled).toBe(true);
expect(normalizeBackendSettingsV2(null)).toBeNull();
});
it('合并优先级:data-* 显式保持,缺省字段取后台值', () => {
const config = parseEmbedConfigV2({ slot: 'a', speed: '24' }, {});
const merged = mergeBackendConfigV2(config, {
position: 'left', size: 'md', intervalMs: 5000, lang: 'en', collapsedDefault: true,
imageFit: 'contain', offsetX: 40, offsetBottom: 80, zIndex: 2000,
displayMode: 'vertical-stream', scrollSpeed: 12, reactionsEnabled: false,
});
expect(merged.speed).toBe(24); // 显式 data-speed 胜出
expect(merged.reactions).toBe(false); // 后台(未显式)
expect(merged.position).toBe('left');
expect(merged.lang).toBe('en');
expect(merged.offsetX).toBe(40);
expect(merged.offsetBottom).toBe(80);
expect(merged.zIndex).toBe(2000);
});
});
describe('parseConfigMessageV2 / buildEmbedUrlV2', () => {
it('config 消息:版本闸(低于等于忽略)+ 非法忽略', () => {
const settings = { position: 'right', size: 'md', intervalMs: 5000, lang: 'auto', collapsedDefault: false };
expect(parseConfigMessageV2({ photoliveEmbed: true, type: 'config', configVersion: 2, settings }, 1)).not.toBeNull();
expect(parseConfigMessageV2({ photoliveEmbed: true, type: 'config', configVersion: 1, settings }, 1)).toBeNull();
expect(parseConfigMessageV2({ photoliveEmbed: true, type: 'config', configVersion: 'x', settings }, 0)).toBeNull();
expect(parseConfigMessageV2({ photoliveEmbed: true, type: 'config', configVersion: 2, settings: null }, 0)).toBeNull();
});
it('URL:v=2 恒在;speed/reactions 仅显式拼接;poll 透传', () => {
const base = parseEmbedConfigV2({ slot: 'tok' }, {});
expect(buildEmbedUrlV2('https://e.test', 'tok', base)).toBe(
'https://e.test/embed/tok?v=2&lang=auto&pos=right',
);
const explicit = parseEmbedConfigV2({ slot: 'tok', speed: '20', reactions: 'off', poll: '500' }, {});
expect(buildEmbedUrlV2('https://e.test', 'tok', explicit)).toBe(
'https://e.test/embed/tok?v=2&lang=auto&pos=right&speed=20&reactions=off&poll=500',
);
});
it('槽 token 编码防注入(路径段转义)', () => {
const c = parseEmbedConfigV2({ slot: 'a/b?c' }, {});
expect(buildEmbedUrlV2('https://e.test', 'a/b?c', c)).toContain('/embed/a%2Fb%3Fc?');
});
});