- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
283 行
9.8 KiB
TypeScript
283 行
9.8 KiB
TypeScript
/**
|
|
* OmniCard AI (CRMmp) - 微官网发音语音条与多媒体播放器测试套件 (TDD Red -> Green)
|
|
*
|
|
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 PRD 3.1、5.1 设计编写
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const AUDIO_PLAYER_UTS = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/pages/portal/useAudioPlayer.uts'
|
|
);
|
|
const AUDIO_PRONOUNCE_COMPONENT = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/components/audio-pronounce/audio-pronounce.uvue'
|
|
);
|
|
const PORTAL_PAGE = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/pages/portal/index.uvue'
|
|
);
|
|
|
|
type MockAudioContext = {
|
|
src: string;
|
|
duration: number;
|
|
currentTime: number;
|
|
paused: boolean;
|
|
play: jest.Mock;
|
|
pause: jest.Mock;
|
|
stop: jest.Mock;
|
|
destroy: jest.Mock;
|
|
onPlay: jest.Mock;
|
|
onEnded: jest.Mock;
|
|
onError: jest.Mock;
|
|
onTimeUpdate: jest.Mock;
|
|
_onPlayCb: (() => void) | null;
|
|
_onEndedCb: (() => void) | null;
|
|
_onErrorCb: ((err: { errMsg: string; errCode?: number }) => void) | null;
|
|
_onTimeUpdateCb: (() => void) | null;
|
|
};
|
|
|
|
const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u;
|
|
|
|
describe('OmniCard AI 移动微官网双语发音语音播放器驱动层测试 (useAudioPlayer.uts)', () => {
|
|
let audioContextMock: MockAudioContext;
|
|
let mockFactoryCalls: number;
|
|
|
|
function createMockAudio(): MockAudioContext {
|
|
mockFactoryCalls++;
|
|
const ctx: MockAudioContext = {
|
|
src: '',
|
|
duration: 3.5,
|
|
currentTime: 0,
|
|
paused: true,
|
|
play: jest.fn(() => {
|
|
ctx.paused = false;
|
|
if (ctx._onPlayCb) {
|
|
ctx._onPlayCb();
|
|
}
|
|
}),
|
|
pause: jest.fn(() => {
|
|
ctx.paused = true;
|
|
}),
|
|
stop: jest.fn(() => {
|
|
ctx.paused = true;
|
|
}),
|
|
destroy: jest.fn(() => {
|
|
ctx.paused = true;
|
|
}),
|
|
onPlay: jest.fn((cb: () => void) => {
|
|
ctx._onPlayCb = cb;
|
|
}),
|
|
onEnded: jest.fn((cb: () => void) => {
|
|
ctx._onEndedCb = cb;
|
|
}),
|
|
onError: jest.fn((cb: (err: { errMsg: string; errCode?: number }) => void) => {
|
|
ctx._onErrorCb = cb;
|
|
}),
|
|
onTimeUpdate: jest.fn((cb: () => void) => {
|
|
ctx._onTimeUpdateCb = cb;
|
|
}),
|
|
_onPlayCb: null,
|
|
_onEndedCb: null,
|
|
_onErrorCb: null,
|
|
_onTimeUpdateCb: null,
|
|
};
|
|
return ctx;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
mockFactoryCalls = 0;
|
|
audioContextMock = createMockAudio();
|
|
|
|
(global as unknown as { uni: Record<string, unknown> }).uni = {
|
|
createInnerAudioContext: () => audioContextMock,
|
|
showToast: jest.fn(),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete (global as unknown as { uni?: Record<string, unknown> }).uni;
|
|
});
|
|
|
|
function loadAudioPlayerModule() {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('../../../mpuniapp/pages/portal/useAudioPlayer.uts');
|
|
}
|
|
|
|
describe('1. 初始状态与契约 (createAudioPlayerState)', () => {
|
|
it('1.1 初始状态所有指示均为静止与默认零值', () => {
|
|
const { createAudioPlayerState } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
|
|
expect(state).toBeDefined();
|
|
expect(state.isPlaying).toBe(false);
|
|
expect(state.currentDuration).toBe(0);
|
|
expect(state.currentTime).toBe(0);
|
|
expect(state.waveformActive).toBe(false);
|
|
expect(state.currentUrl).toBe('');
|
|
expect(state.errorMessage).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('2. 正常播放逻辑 (playAudio)', () => {
|
|
it('2.1 提供合法链接时设置 src 并播放,更新 isPlaying 和 waveformActive', () => {
|
|
const { createAudioPlayerState, playAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
const testUrl = 'https://cdn.omnicard.ai/audio/alex-name.mp3';
|
|
|
|
const success = playAudio(state, testUrl);
|
|
expect(success).toBe(true);
|
|
expect(audioContextMock.src).toBe(testUrl);
|
|
expect(audioContextMock.play).toHaveBeenCalled();
|
|
expect(state.isPlaying).toBe(true);
|
|
expect(state.waveformActive).toBe(true);
|
|
expect(state.currentUrl).toBe(testUrl);
|
|
expect(state.errorMessage).toBe('');
|
|
});
|
|
|
|
it('2.2 播放不同音频时,自动停止上一首并载入新音频播放', () => {
|
|
const { createAudioPlayerState, playAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
const url1 = 'https://cdn.omnicard.ai/audio/alex-1.mp3';
|
|
const url2 = 'https://cdn.omnicard.ai/audio/alex-2.mp3';
|
|
|
|
playAudio(state, url1);
|
|
expect(state.currentUrl).toBe(url1);
|
|
|
|
playAudio(state, url2);
|
|
expect(audioContextMock.stop).toHaveBeenCalled();
|
|
expect(audioContextMock.src).toBe(url2);
|
|
expect(state.currentUrl).toBe(url2);
|
|
expect(state.isPlaying).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('3. 停止播放 (stopAudio)', () => {
|
|
it('3.1 停止后复位 isPlaying 与 waveformActive', () => {
|
|
const { createAudioPlayerState, playAudio, stopAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
playAudio(state, 'https://cdn.omnicard.ai/audio/alex.mp3');
|
|
|
|
stopAudio(state);
|
|
expect(audioContextMock.stop).toHaveBeenCalled();
|
|
expect(state.isPlaying).toBe(false);
|
|
expect(state.waveformActive).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('4. 切换控制 (toggleAudio)', () => {
|
|
it('4.1 未播放时点击触发播放;正在播放同一音频时点击触发停止', () => {
|
|
const { createAudioPlayerState, toggleAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
const url = 'https://cdn.omnicard.ai/audio/alex.mp3';
|
|
|
|
// 第一次点击:播放
|
|
const firstResult = toggleAudio(state, url);
|
|
expect(firstResult).toBe(true);
|
|
expect(state.isPlaying).toBe(true);
|
|
expect(state.waveformActive).toBe(true);
|
|
|
|
// 第二次点击同一链接:暂停/停止
|
|
const secondResult = toggleAudio(state, url);
|
|
expect(secondResult).toBe(false);
|
|
expect(state.isPlaying).toBe(false);
|
|
expect(state.waveformActive).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('5. 播放结束回调 (onEnded)', () => {
|
|
it('5.1 底层播放完毕触发 onEnded 时,自动恢复未播放状态与停止波形动效', () => {
|
|
const { createAudioPlayerState, playAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
playAudio(state, 'https://cdn.omnicard.ai/audio/alex.mp3');
|
|
|
|
expect(state.isPlaying).toBe(true);
|
|
expect(state.waveformActive).toBe(true);
|
|
|
|
// 触发 onEnded 回调
|
|
audioContextMock._onEndedCb?.();
|
|
expect(state.isPlaying).toBe(false);
|
|
expect(state.waveformActive).toBe(false);
|
|
expect(state.currentTime).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('6. 异常音频链接与错误处理优雅降级 (onError)', () => {
|
|
it('6.1 空链接或纯空格链接安全防卫退出,不调用 play 并记录提示', () => {
|
|
const { createAudioPlayerState, playAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
|
|
const emptyRes = playAudio(state, '');
|
|
expect(emptyRes).toBe(false);
|
|
expect(audioContextMock.play).not.toHaveBeenCalled();
|
|
expect(state.isPlaying).toBe(false);
|
|
|
|
const blankRes = playAudio(state, ' ');
|
|
expect(blankRes).toBe(false);
|
|
expect(state.isPlaying).toBe(false);
|
|
});
|
|
|
|
it('6.2 底层触发 onError 时优雅降级,重置状态并记录错误信息,不抛崩溃', () => {
|
|
const { createAudioPlayerState, playAudio } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
playAudio(state, 'https://cdn.omnicard.ai/audio/broken.mp3');
|
|
|
|
// 触发 onError
|
|
audioContextMock._onErrorCb?.({
|
|
errMsg: 'MEDIA_ELEMENT_ERROR: Format not supported',
|
|
errCode: 10004,
|
|
});
|
|
|
|
expect(state.isPlaying).toBe(false);
|
|
expect(state.waveformActive).toBe(false);
|
|
expect(state.errorMessage).toContain('Format not supported');
|
|
});
|
|
});
|
|
|
|
describe('7. 单例音频释放与资源清理 (releaseAudioPlayer)', () => {
|
|
it('7.1 调用 releaseAudioPlayer 释放底层实例与重置状态', () => {
|
|
const { createAudioPlayerState, playAudio, releaseAudioPlayer } = loadAudioPlayerModule();
|
|
const state = createAudioPlayerState();
|
|
playAudio(state, 'https://cdn.omnicard.ai/audio/alex.mp3');
|
|
|
|
releaseAudioPlayer(state);
|
|
expect(audioContextMock.destroy).toHaveBeenCalled();
|
|
expect(state.isPlaying).toBe(false);
|
|
expect(state.waveformActive).toBe(false);
|
|
expect(state.currentUrl).toBe('');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('OmniCard AI 语音组件契约与设计规范校验', () => {
|
|
it('组件与 Composable 源码存在并遵循强类型与无 emoji 规范', () => {
|
|
expect(fs.existsSync(AUDIO_PLAYER_UTS)).toBe(true);
|
|
expect(fs.existsSync(AUDIO_PRONOUNCE_COMPONENT)).toBe(true);
|
|
expect(fs.existsSync(PORTAL_PAGE)).toBe(true);
|
|
|
|
const playerSource = fs.readFileSync(AUDIO_PLAYER_UTS, 'utf8');
|
|
const compSource = fs.readFileSync(AUDIO_PRONOUNCE_COMPONENT, 'utf8');
|
|
const portalSource = fs.readFileSync(PORTAL_PAGE, 'utf8');
|
|
const testSource = fs.readFileSync(__filename, 'utf8');
|
|
|
|
// 严禁 emoji
|
|
expect(EMOJI_RE.test(playerSource)).toBe(false);
|
|
expect(EMOJI_RE.test(compSource)).toBe(false);
|
|
expect(EMOJI_RE.test(testSource)).toBe(false);
|
|
|
|
// 严禁 any
|
|
expect(playerSource.includes(': any') || playerSource.includes(' as any')).toBe(false);
|
|
expect(compSource.includes(': any') || compSource.includes(' as any')).toBe(false);
|
|
|
|
// 设计 Token: 主色 #0B5CFF、脉冲动画、prefers-reduced-motion
|
|
expect(compSource.includes('#0B5CFF')).toBe(true);
|
|
expect(compSource.includes('prefers-reduced-motion')).toBe(true);
|
|
|
|
// 门户首页 Hero 区关联 audio-pronounce 组件
|
|
expect(portalSource.includes('audio-pronounce') || portalSource.includes('AudioPronounce')).toBe(true);
|
|
});
|
|
});
|