diff --git a/web/jest.config.ts b/web/jest.config.ts index aa2f22bf82..2fbaa9a0a0 100644 --- a/web/jest.config.ts +++ b/web/jest.config.ts @@ -98,7 +98,7 @@ const config: Config = { // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module moduleNameMapper: { - '^@/components/(.*)$': '/components/$1', + '^@/(.*)$': '/$1', '^lodash-es$': 'lodash', }, diff --git a/web/package.json b/web/package.json index 74eead4eba..bcc39ab15b 100644 --- a/web/package.json +++ b/web/package.json @@ -184,6 +184,7 @@ "husky": "^9.1.6", "jest": "^29.7.0", "lint-staged": "^15.2.10", + "lodash": "^4.17.21", "magicast": "^0.3.4", "postcss": "^8.4.47", "sass": "^1.80.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f02da2209d..71f6fe0ea5 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -485,6 +485,9 @@ importers: lint-staged: specifier: ^15.2.10 version: 15.2.10 + lodash: + specifier: ^4.17.21 + version: 4.17.21 magicast: specifier: ^0.3.4 version: 0.3.5 diff --git a/web/utils/format.spec.ts b/web/utils/format.spec.ts index f349efa4e4..ab1c5c1610 100644 --- a/web/utils/format.spec.ts +++ b/web/utils/format.spec.ts @@ -1,4 +1,5 @@ -import { formatFileSize, formatNumber, formatTime } from './format' +import { downloadFile, formatFileSize, formatNumber, formatTime } from './format' + describe('formatNumber', () => { test('should correctly format integers', () => { expect(formatNumber(1234567)).toBe('1,234,567') @@ -59,3 +60,45 @@ describe('formatTime', () => { expect(formatTime(7200)).toBe('2.00 h') }) }) +describe('downloadFile', () => { + test('should create a link and trigger a download correctly', () => { + // Mock data + const blob = new Blob(['test content'], { type: 'text/plain' }) + const fileName = 'test-file.txt' + const mockUrl = 'blob:mockUrl' + + // Mock URL.createObjectURL + const createObjectURLMock = jest.fn().mockReturnValue(mockUrl) + const revokeObjectURLMock = jest.fn() + Object.defineProperty(window.URL, 'createObjectURL', { value: createObjectURLMock }) + Object.defineProperty(window.URL, 'revokeObjectURL', { value: revokeObjectURLMock }) + + // Mock createElement and appendChild + const mockLink = { + href: '', + download: '', + click: jest.fn(), + remove: jest.fn(), + } + const createElementMock = jest.spyOn(document, 'createElement').mockReturnValue(mockLink as any) + const appendChildMock = jest.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => { + return node + }) + + // Call the function + downloadFile({ data: blob, fileName }) + + // Assertions + expect(createObjectURLMock).toHaveBeenCalledWith(blob) + expect(createElementMock).toHaveBeenCalledWith('a') + expect(mockLink.href).toBe(mockUrl) + expect(mockLink.download).toBe(fileName) + expect(appendChildMock).toHaveBeenCalledWith(mockLink) + expect(mockLink.click).toHaveBeenCalled() + expect(mockLink.remove).toHaveBeenCalled() + expect(revokeObjectURLMock).toHaveBeenCalledWith(mockUrl) + + // Clean up mocks + jest.restoreAllMocks() + }) +}) diff --git a/web/utils/index.spec.ts b/web/utils/index.spec.ts new file mode 100644 index 0000000000..4785e55594 --- /dev/null +++ b/web/utils/index.spec.ts @@ -0,0 +1,295 @@ +import { + asyncRunSafe, + canFindTool, + correctModelProvider, + correctToolProvider, + fetchWithRetry, + getPurifyHref, + getTextWidthWithCanvas, + randomString, + removeSpecificQueryParam, + sleep, +} from './index' + +describe('sleep', () => { + it('should wait for the specified time', async () => { + const timeVariance = 10 + const sleepTime = 100 + const start = Date.now() + await sleep(sleepTime) + const elapsed = Date.now() - start + expect(elapsed).toBeGreaterThanOrEqual(sleepTime - timeVariance) + }) +}) + +describe('asyncRunSafe', () => { + it('should return [null, result] when promise resolves', async () => { + const result = await asyncRunSafe(Promise.resolve('success')) + expect(result).toEqual([null, 'success']) + }) + + it('should return [error] when promise rejects', async () => { + const error = new Error('test error') + const result = await asyncRunSafe(Promise.reject(error)) + expect(result).toEqual([error]) + }) + + it('should return [Error] when promise rejects with undefined', async () => { + // eslint-disable-next-line prefer-promise-reject-errors + const result = await asyncRunSafe(Promise.reject()) + expect(result[0]).toBeInstanceOf(Error) + expect(result[0]?.message).toBe('unknown error') + }) +}) + +describe('getTextWidthWithCanvas', () => { + let originalCreateElement: any + + beforeEach(() => { + // Store original implementation + originalCreateElement = document.createElement + + // Mock canvas and context + const measureTextMock = jest.fn().mockReturnValue({ width: 100 }) + const getContextMock = jest.fn().mockReturnValue({ + measureText: measureTextMock, + font: '', + }) + + document.createElement = jest.fn().mockReturnValue({ + getContext: getContextMock, + }) + }) + + afterEach(() => { + // Restore original implementation + document.createElement = originalCreateElement + }) + + it('should return the width of text', () => { + const width = getTextWidthWithCanvas('test text') + expect(width).toBe(100) + }) + + it('should return 0 if context is not available', () => { + // Override mock for this test + document.createElement = jest.fn().mockReturnValue({ + getContext: () => null, + }) + + const width = getTextWidthWithCanvas('test text') + expect(width).toBe(0) + }) +}) + +describe('randomString', () => { + it('should generate string of specified length', () => { + const result = randomString(10) + expect(result.length).toBe(10) + }) + + it('should only contain valid characters', () => { + const result = randomString(100) + const validChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_' + for (const char of result) + expect(validChars).toContain(char) + }) + + it('should generate different strings on consecutive calls', () => { + const result1 = randomString(20) + const result2 = randomString(20) + expect(result1).not.toEqual(result2) + }) +}) + +describe('getPurifyHref', () => { + it('should return empty string for falsy input', () => { + expect(getPurifyHref('')).toBe('') + expect(getPurifyHref(undefined as any)).toBe('') + }) + + it('should escape HTML characters', () => { + expect(getPurifyHref('')).not.toContain('