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:
浅念
2026-09-20 22:31:40 +08:00
parent aee4b7ac46
commit ddb4c8175f
10 changed files with 637 additions and 0 deletions
+98
View File
@@ -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();
});
});