feat(embed): versioned loader with postMessage whitelist
packages/embed-loader:手写零依赖加载器(esbuild bundle+minify→dist/v1/loader.js, IIFE target es2018 4.7KB)。自发现 EMBED_ORIGIN(currentScript 兜底扫描)、 data-slot 必填缺失静默 no-op、__photoliveEmbedV1 标记去重+同槽去重、 窄屏 <480px 强制 sm+默认收起、postMessage 双向白名单(source+origin 严格 校验,resize 夹取 60-640)、sessionStorage 收起记忆、零全局零 console 全静默。sync 脚本携 sha256 校验拷贝至 viewer publicDir(两份产物入库); src/params.js 为 loader/单测共用参数源(11 vitest 用例)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// packages/embed-loader/src/params.test.js
|
||||
// 纯函数单测:data-* 解析/缺省回退/夹取、窄屏强制、URL 拼接、高度夹取。
|
||||
// DOM/宿主交互行为由 Task 4 Playwright 宿主页覆盖(计划注明 node 模拟 DOM 太薄)。
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildEmbedUrl,
|
||||
clampHeight,
|
||||
normalizeInterval,
|
||||
parseEmbedConfig,
|
||||
} from './params.js';
|
||||
|
||||
describe('parseEmbedConfig', () => {
|
||||
it('空 dataset 全缺省(slot null → 调用方 no-op)', () => {
|
||||
const c = parseEmbedConfig({});
|
||||
expect(c).toEqual({
|
||||
slot: null,
|
||||
lang: 'auto',
|
||||
position: 'right',
|
||||
intervalMs: 5000,
|
||||
size: 'md',
|
||||
collapsedDefault: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('data-slot 保留并去首尾空白;非字符串(缺失)为 null', () => {
|
||||
expect(parseEmbedConfig({ slot: ' tok ' }).slot).toBe('tok');
|
||||
expect(parseEmbedConfig({}).slot).toBeNull();
|
||||
expect(parseEmbedConfig({ slot: ' ' }).slot).toBeNull();
|
||||
});
|
||||
|
||||
it('lang/position 白名单外静默回退缺省', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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('窄屏(<480px):强制 sm 尺寸 + 默认收起,覆盖 dataset 尺寸语义', () => {
|
||||
const c = parseEmbedConfig({ slot: 'a', size: 'md' }, { narrow: true });
|
||||
expect(c.size).toBe('sm');
|
||||
expect(c.collapsedDefault).toBe(true);
|
||||
});
|
||||
|
||||
it('非窄屏时 collapsedDefault 恒 false(收起默认由 iframe 侧按服务端设置下发)', () => {
|
||||
expect(parseEmbedConfig({ slot: 'a' }, { narrow: false }).collapsedDefault).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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('slot token 编码(防路径注入)', () => {
|
||||
const url = buildEmbedUrl('https://ov.example.com', 'a/b?c=1', {
|
||||
lang: 'auto',
|
||||
position: 'right',
|
||||
intervalMs: 5000,
|
||||
});
|
||||
expect(url.startsWith('https://ov.example.com/embed/a%2Fb%3Fc%3D1?')).toBe(true);
|
||||
expect(url).toContain('lang=auto&pos=right&interval=5000');
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user