Files
eSIM-Tools/tests/giffgaff/direct-fetch.test.js
Abner 75da15edff 🐛 fix: 修复 PR 评审发现的 11 项问题
- data.data 空值保护:data?.data?.eSims 防止 TypeError
- directFetchMode 移至成功路径设置,失败时重置为 false
- SMS 卡片切换时隐藏 directFetchSection,避免双内联区同时可见
- modeBadge 直取模式下隐藏"设备更换"徽标
- tl() 文案迁移至 LITERAL_TRANSLATIONS,修复英文环境回退中文
- "拉取中"状态类型从 success 改为 info
- SSN radio 查询限定到 picker 容器内
- jest.config 补全 .spec.js 排除规则
- innerHTML 改用 DOM API 构建(pullBtn + renderSsnPicker)
- 测试 fail() 替换为 expect().rejects 惯用写法
2026-05-28 20:12:19 +08:00

197 lines
5.9 KiB
JavaScript

/**
* 直取 eSIM 功能单元测试
* 覆盖 fetchExistingESims / directFetchFlow 的正常与异常路径
*/
// 模拟 secure-storage 模块(必须在 import 之前)
jest.mock('../../src/js/modules/secure-storage.js', () => ({
__esModule: true,
default: {
getItem: jest.fn(() => null),
setItem: jest.fn(),
removeItem: jest.fn()
}
}));
// 模拟 i18n 模块
jest.mock('../../src/js/modules/i18n.js', () => ({
__esModule: true,
t: jest.fn((key, vars) => {
const map = {
'giffgaff.directFetch.errors.missingToken': '缺少访问令牌',
'giffgaff.directFetch.errors.empty': '未发现可下载的 eSIM',
'giffgaff.directFetch.errors.multipleNeedPick': '检测到多个可下载的 eSIM',
'giffgaff.directFetch.errors.generic': '直取 eSIM 失败:' + (vars?.message || ''),
};
return map[key] || key;
}),
tl: jest.fn((key) => key)
}));
// 模拟 utils 模块
jest.mock('../../src/giffgaff/js/modules/utils.js', () => ({
__esModule: true,
isNetlifyEnvironment: jest.fn(() => false)
}));
import { stateManager } from '../../src/giffgaff/js/modules/state-manager.js';
import { esimService } from '../../src/giffgaff/js/modules/esim-service.js';
describe('ESimService - directFetchFlow', () => {
beforeEach(() => {
// 重置状态
stateManager.setState({
accessToken: '',
emailSignature: '',
memberId: '',
esimSSN: '',
esimActivationCode: '',
esimDeliveryStatus: '',
lpaString: '',
directFetchMode: false
});
// 重置 fetch mock
global.fetch = jest.fn();
});
describe('fetchExistingESims', () => {
test('成功返回 eSIM 列表', async () => {
stateManager.setState({ accessToken: 'valid-token' });
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSims: [{ ssn: 'SSN-001', __typename: 'ESim' }] }
})
});
const result = await esimService.fetchExistingESims();
expect(result).toEqual([{ ssn: 'SSN-001', __typename: 'ESim' }]);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
test('缺 accessToken 时抛出异常', async () => {
stateManager.setState({ accessToken: '' });
await expect(esimService.fetchExistingESims()).rejects.toThrow(/访问令牌/);
});
test('HTTP 错误时抛出异常', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => 'Internal Server Error'
});
await expect(esimService.fetchExistingESims()).rejects.toThrow(/500/);
});
test('GraphQL errors 时抛出对应 message', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
errors: [{ message: 'Unauthorized' }]
})
});
await expect(esimService.fetchExistingESims()).rejects.toThrow('Unauthorized');
});
test('返回空列表时返回空数组', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSims: [] }
})
});
const result = await esimService.fetchExistingESims();
expect(result).toEqual([]);
});
});
describe('directFetchFlow', () => {
test('单 eSIM 时自动完成并写入状态', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSims: [{ ssn: 'SSN-A', __typename: 'ESim' }] }
})
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSimDownloadToken: { lpaString: 'LPA:1$test', host: 'h', matchingId: 'm' } }
})
});
const result = await esimService.directFetchFlow();
expect(result.success).toBe(true);
expect(result.ssn).toBe('SSN-A');
expect(result.lpaString).toBe('LPA:1$test');
expect(stateManager.get('esimSSN')).toBe('SSN-A');
expect(stateManager.get('lpaString')).toBe('LPA:1$test');
expect(stateManager.get('directFetchMode')).toBe(true);
expect(stateManager.get('esimDeliveryStatus')).toBe('DOWNLOADABLE');
});
test('多 eSIM 未选定 preselectedSsn 时抛出 MULTIPLE_ESIMS', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSims: [{ ssn: 'A' }, { ssn: 'B' }] }
})
});
await expect(esimService.directFetchFlow()).rejects.toMatchObject({
code: 'MULTIPLE_ESIMS',
candidates: ['A', 'B']
});
});
test('多 eSIM 已选定 preselectedSsn 时直接使用该 ssn', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSims: [{ ssn: 'A' }, { ssn: 'B' }] }
})
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSimDownloadToken: { lpaString: 'LPA:1$B-token', host: 'h', matchingId: 'm' } }
})
});
const result = await esimService.directFetchFlow('B');
expect(result.success).toBe(true);
expect(result.ssn).toBe('B');
expect(result.lpaString).toBe('LPA:1$B-token');
expect(stateManager.get('esimSSN')).toBe('B');
});
test('空列表时抛出 EMPTY_LIST', async () => {
stateManager.setState({ accessToken: 'tok' });
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: { eSims: [] }
})
});
await expect(esimService.directFetchFlow()).rejects.toMatchObject({
code: 'EMPTY_LIST'
});
});
});
});