From 21161e4afead7e684a008f4c3f1a76f818ddddbc Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:01:56 +0300 Subject: [PATCH] test(export): add unit tests for canvas image export functionality (#85) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- src/ui/export.test.ts | 171 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/ui/export.test.ts diff --git a/src/ui/export.test.ts b/src/ui/export.test.ts new file mode 100644 index 0000000..d3009ef --- /dev/null +++ b/src/ui/export.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { exportSheetImage } from './export'; +import { writeImageFile } from '../storage'; + +vi.mock('../storage', () => ({ + writeImageFile: vi.fn(), +})); + +describe('exportSheetImage', () => { + let mockContext2D: any; + let mockCanvas: any; + let toastMock: any; + let tMock: any; + + beforeEach(() => { + toastMock = vi.fn(); + tMock = vi.fn((key: string) => `trans_${key}`); + + mockContext2D = { + measureText: vi.fn().mockReturnValue({ width: 10 }), + scale: vi.fn(), + fillRect: vi.fn(), + fillText: vi.fn(), + font: '', + fillStyle: '', + }; + + mockCanvas = { + getContext: vi.fn().mockReturnValue(mockContext2D), + toBlob: vi.fn((cb) => cb(new Blob(['fake-image-data'], { type: 'image/png' }))), + width: 0, + height: 0, + }; + + vi.stubGlobal('document', { + documentElement: {}, + createElement: vi.fn((tag) => { + if (tag === 'canvas') return mockCanvas; + return {}; + }) + }); + + vi.stubGlobal('window', { + devicePixelRatio: 0 + }); + + vi.stubGlobal('getComputedStyle', vi.fn().mockReturnValue({ + getPropertyValue: vi.fn((prop) => { + if (prop === '--bg') return ''; + if (prop === '--fg') return ''; + if (prop === '--result') return ''; + if (prop === '--mono') return ''; + return ''; + }) + })); + + vi.stubGlobal('Blob', class Blob { + content: any[]; + options: any; + constructor(content: any[], options: any) { + this.content = content; + this.options = options; + } + async arrayBuffer() { + return new Uint8Array([1, 2, 3]).buffer; + } + }); + + vi.stubGlobal('ClipboardItem', class ClipboardItem { + constructor(data: any) { + Object.assign(this, data); + } + }); + + vi.stubGlobal('navigator', { + clipboard: { + write: vi.fn().mockResolvedValue(undefined) + } + }); + + vi.stubGlobal('btoa', (str: string) => Buffer.from(str, 'binary').toString('base64')); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + it('should create a canvas and copy it to clipboard successfully', async () => { + await exportSheetImage({ + lines: ['1 + 1'], + results: [{ text: '2' }], + fontSize: 14, + toast: toastMock, + t: tMock + }); + + expect(document.createElement).toHaveBeenCalledWith('canvas'); + expect(mockCanvas.getContext).toHaveBeenCalledWith('2d'); + expect(mockContext2D.fillText).toHaveBeenCalledWith('1 + 1', expect.any(Number), expect.any(Number)); + expect(mockContext2D.fillText).toHaveBeenCalledWith('2', expect.any(Number), expect.any(Number)); + + expect(navigator.clipboard.write).toHaveBeenCalled(); + expect(toastMock).toHaveBeenCalledWith('trans_imageCopied'); + }); + + it('should handle blob failure', async () => { + mockCanvas.toBlob = vi.fn((cb) => cb(null)); + + await exportSheetImage({ + lines: ['test'], + results: [{ text: null }], + fontSize: 14, + toast: toastMock, + t: tMock + }); + + expect(toastMock).toHaveBeenCalledWith('trans_imageFailed'); + expect(navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + it('should fallback to writeImageFile when clipboard write fails', async () => { + navigator.clipboard.write = vi.fn().mockRejectedValue(new Error('clipboard fail')); + vi.mocked(writeImageFile).mockResolvedValue(true); + + await exportSheetImage({ + lines: ['fallback'], + results: [{ text: 'res' }], + fontSize: 14, + toast: toastMock, + t: tMock + }); + + expect(navigator.clipboard.write).toHaveBeenCalled(); + expect(writeImageFile).toHaveBeenCalled(); + expect(writeImageFile).toHaveBeenCalledWith('AQID'); + expect(toastMock).toHaveBeenCalledWith('trans_saved'); + }); + + it('should handle writeImageFile failure in fallback', async () => { + navigator.clipboard.write = vi.fn().mockRejectedValue(new Error('clipboard fail')); + vi.mocked(writeImageFile).mockRejectedValue(new Error('save fail')); + + await exportSheetImage({ + lines: ['fail'], + results: [{ text: 'res' }], + fontSize: 14, + toast: toastMock, + t: tMock + }); + + expect(writeImageFile).toHaveBeenCalled(); + expect(toastMock).toHaveBeenCalledWith('trans_imageFailed'); + }); + + it('should handle writeImageFile returning false', async () => { + navigator.clipboard.write = vi.fn().mockRejectedValue(new Error('clipboard fail')); + vi.mocked(writeImageFile).mockResolvedValue(false); + + await exportSheetImage({ + lines: ['fail'], + results: [{ text: 'res' }], + fontSize: 14, + toast: toastMock, + t: tMock + }); + + expect(writeImageFile).toHaveBeenCalled(); + expect(toastMock).not.toHaveBeenCalledWith('trans_saved'); + }); +});