import { describe, expect, it, beforeEach } from 'vitest'; import { CrashReporter, MockSentryBridge, buildErrorRecoveryData, crashReporter } from '../src/services/crash'; describe('CrashReporter', () => { let reporter: CrashReporter; beforeEach(() => { reporter = new CrashReporter(); }); it('recordCrash 记录崩溃', () => { reporter.recordCrash({ timestamp: new Date().toISOString(), message: '测试崩溃', source: 'uncaught', }); expect(reporter.getRecords()).toHaveLength(1); expect(reporter.getLatestCrash()?.message).toBe('测试崩溃'); }); it('captureException 记录已处理异常', () => { reporter.captureException(new Error('已处理错误')); const records = reporter.getRecords(); expect(records).toHaveLength(1); expect(records[0].source).toBe('handled'); expect(records[0].stack).toContain('已处理错误'); }); it('getLatestCrash 返回最近一条', () => { reporter.recordCrash({ timestamp: '2026-07-13T10:00:00', message: 'first', source: 'uncaught' }); reporter.recordCrash({ timestamp: '2026-07-13T11:00:00', message: 'second', source: 'uncaught' }); expect(reporter.getLatestCrash()?.message).toBe('second'); }); it('超过上限丢弃最旧', () => { const small = new CrashReporter(); (small as unknown as { maxRecords: number }).maxRecords = 3; for (let i = 0; i < 5; i++) { small.recordCrash({ timestamp: `2026-07-13T1${i}:00:00`, message: `crash${i}`, source: 'uncaught' }); } expect(small.getRecords()).toHaveLength(3); expect(small.getRecords()[0].message).toBe('crash2'); // 最旧的2条被丢弃 }); it('exportAsText 导出文本', () => { reporter.recordCrash({ timestamp: '2026-07-13T10:00:00', message: 'msg1', source: 'uncaught' }); const text = reporter.exportAsText(); expect(text).toContain('msg1'); expect(text).toContain('uncaught'); }); it('clear 清空', () => { reporter.recordCrash({ timestamp: 'now', message: 'x', source: 'uncaught' }); reporter.clear(); expect(reporter.getRecords()).toHaveLength(0); }); it('setSentry 注入后 captureException 转发', () => { const sentry = new MockSentryBridge(); reporter.setSentry(sentry, 'fake-dsn', 'test'); expect(sentry.initialized).toBe(true); reporter.captureException(new Error('sentry error')); expect(sentry.exceptions).toHaveLength(1); }); it('captureMessage 转发到 sentry', () => { const sentry = new MockSentryBridge(); reporter.setSentry(sentry, 'dsn', 'test'); reporter.captureMessage('info msg'); reporter.captureMessage('warn msg', 'warning'); expect(sentry.messages).toHaveLength(2); }); it('install 不重复安装', () => { reporter.install(); reporter.install(); // 第二次无副作用 // 无直接断言,验证不抛错 }); }); describe('buildErrorRecoveryData', () => { it('构造错误恢复 UI 数据', () => { const data = buildErrorRecoveryData(new Error('UI 崩溃')); expect(data.title).toBe('应用遇到问题'); expect(data.message).toBe('UI 崩溃'); expect(data.actions.length).toBeGreaterThan(0); expect(data.actions.some(a => a.type === 'retry')).toBe(true); expect(data.actions.some(a => a.type === 'export-logs')).toBe(true); }); it('非 Error 对象也能构造', () => { const data = buildErrorRecoveryData('字符串错误'); expect(data.message).toBe('字符串错误'); expect(data.stack).toBeUndefined(); }); }); describe('crashReporter 单例', () => { it('是 CrashReporter 实例', () => { expect(crashReporter).toBeInstanceOf(CrashReporter); }); });