diff --git a/src/js/modules/README.md b/src/js/modules/README.md index 1cda00a..2403042 100644 --- a/src/js/modules/README.md +++ b/src/js/modules/README.md @@ -58,6 +58,15 @@ giffgaffAPI.clearCache(); #### `clipboard.js` Shared clipboard helper. Re-exported by Giffgaff/Simyo `utils.js` so existing import paths stay stable. +#### `diagnostics.js` +Issue 诊断导出。业务页初始化后可在控制台执行: + +```js +copyEsimDiagnostics() // 复制脱敏 JSON 到剪贴板,便于粘贴到 GitHub Issue +``` + +不包含 token / 密码 / cookie / LPA 等敏感字段。 + #### `resource-hints.js` > **状态:未挂载生产页面**。路径已对齐原生 ES 模块静态托管,但 HTML 入口尚未 `import` 本模块。 > 生产页面当前依赖浏览器默认加载与各页已写死的 script/link。 diff --git a/tests/modules/diagnostics.test.js b/tests/modules/diagnostics.test.js new file mode 100644 index 0000000..6f7a75e --- /dev/null +++ b/tests/modules/diagnostics.test.js @@ -0,0 +1,76 @@ +'use strict'; + +/** + * 诊断信息导出测试 + */ + +import { + collectDiagnostics, + formatDiagnostics, + copyDiagnostics, + installDiagnosticsGlobal +} from '../../src/js/modules/diagnostics.js'; + +describe('diagnostics', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete window.copyEsimDiagnostics; + delete window.__esimDiagnostics; + }); + + it('collectDiagnostics 应脱敏敏感字段', () => { + const snap = collectDiagnostics({ + app: 'simyo', + state: { + sessionToken: 'secret-token', + phoneNumber: '0612345678', + password: 'should-not-appear', + currentStep: 2 + } + }); + + expect(snap.app).toBe('simyo'); + expect(snap.state.sessionToken).toBe('[redacted]'); + expect(snap.state.password).toBe('[redacted]'); + expect(snap.state.phoneNumber).toBe('0612345678'); + expect(snap.state.currentStep).toBe(2); + expect(snap.page.hostname).toBeDefined(); + }); + + it('formatDiagnostics 应输出可粘贴 markdown 代码块', () => { + const text = formatDiagnostics({ app: 'giffgaff', state: { accessToken: 'tok' } }); + expect(text).toContain('```json'); + expect(text).toContain('[redacted]'); + expect(text).not.toContain('"accessToken": "tok"'); + }); + + it('copyDiagnostics 应调用剪贴板', async () => { + navigator.clipboard.writeText.mockResolvedValueOnce(undefined); + Object.defineProperty(window, 'isSecureContext', { + configurable: true, + value: true + }); + + const text = await copyDiagnostics({ app: 'home' }); + expect(navigator.clipboard.writeText).toHaveBeenCalled(); + expect(text).toContain('```json'); + }); + + it('installDiagnosticsGlobal 应挂载 copyEsimDiagnostics', async () => { + navigator.clipboard.writeText.mockResolvedValueOnce(undefined); + Object.defineProperty(window, 'isSecureContext', { + configurable: true, + value: true + }); + + installDiagnosticsGlobal({ + app: 'simyo', + getState: () => ({ sessionToken: 'abc', phoneNumber: '0611111111' }) + }); + + expect(typeof window.copyEsimDiagnostics).toBe('function'); + const text = await window.copyEsimDiagnostics(); + expect(text).toContain('[redacted]'); + expect(text).toContain('0611111111'); + }); +}); diff --git a/tests/simyo/auth-handler.test.js b/tests/simyo/auth-handler.test.js index 60957e4..dcb22a5 100644 --- a/tests/simyo/auth-handler.test.js +++ b/tests/simyo/auth-handler.test.js @@ -39,7 +39,8 @@ describe('Simyo AuthHandler', () => { expect(result.sessionToken).toBe('sess-abc'); expect(stateManager.get('sessionToken')).toBe('sess-abc'); expect(stateManager.get('phoneNumber')).toBe('0612345678'); - expect(stateManager.get('password')).toBe(''); + expect(stateManager.get('password')).toBeUndefined(); + expect(Object.prototype.hasOwnProperty.call(stateManager.getState(), 'password')).toBe(false); expect(authHandler.isLoggedIn()).toBe(true); }); diff --git a/tests/simyo/help.test.js b/tests/simyo/help.test.js index f3352f0..008755d 100644 --- a/tests/simyo/help.test.js +++ b/tests/simyo/help.test.js @@ -3,6 +3,7 @@ */ import { openHelp } from '../../src/simyo/js/modules/utils.js'; +import * as i18n from '../../src/js/modules/i18n.js'; describe('Simyo openHelp()', () => { beforeEach(() => { @@ -31,5 +32,25 @@ describe('Simyo openHelp()', () => { overlay.click(); expect(document.querySelector('[data-help-overlay="simyo-help"]')).toBeNull(); }); + + it('帮助文案应作为文本渲染,不执行 HTML', () => { + jest.spyOn(i18n, 't').mockImplementation((key) => { + if (key === 'simyo.help.title') return 'XSS'; + if (key.endsWith('.heading')) return 'Heading'; + if (key.endsWith('.content')) return ''; + if (key === 'simyo.help.close') return 'Close'; + return key; + }); + + openHelp(); + + const overlay = document.querySelector('[data-help-overlay="simyo-help"]'); + expect(overlay.querySelector('img[src="x"]')).toBeNull(); + expect(overlay.querySelector('script')).toBeNull(); + expect(overlay.textContent).toContain('XSS'); + expect(overlay.textContent).toContain(''); + + i18n.t.mockRestore(); + }); });