From 49cac03db89837685b1b6bdf923a665f054216c3 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 26 May 2025 20:35:02 +0200 Subject: [PATCH] Implement utils tests --- app/src/types/models.ts | 65 ++++++++++++ app/src/utils/fileHelpers.test.ts | 169 ++++++++++++++++++++++++++++++ app/src/utils/formatBytes.test.ts | 124 ++++++++++++++++++++++ app/src/utils/formatBytes.ts | 3 + app/src/utils/themeStyle.test.ts | 165 +++++++++++++++++++++++++++++ 5 files changed, 526 insertions(+) create mode 100644 app/src/utils/fileHelpers.test.ts create mode 100644 app/src/utils/formatBytes.test.ts create mode 100644 app/src/utils/themeStyle.test.ts diff --git a/app/src/types/models.ts b/app/src/types/models.ts index 3c17535..8e2f905 100644 --- a/app/src/types/models.ts +++ b/app/src/types/models.ts @@ -1,3 +1,5 @@ +import type { Parent } from 'unist'; + /** * User model from the API */ @@ -285,3 +287,66 @@ export interface SettingsAction { type: SettingsActionType; payload?: T; } + +// WikiLinks + +/** + * Represents a wiki link match from the regex + */ +export interface WikiLinkMatch { + fullMatch: string; + isImage: boolean; // Changed from string to boolean + fileName: string; + displayText: string; + heading?: string | undefined; + index: number; +} + +/** + * Node replacement information for processing + */ +export interface ReplacementInfo { + matches: WikiLinkMatch[]; + parent: Parent; + index: number; +} + +/** + * Properties for link nodes + */ +export interface LinkNodeProps { + style?: { + color?: string; + textDecoration?: string; + }; +} + +/** + * Link node with data properties + */ +export interface LinkNode extends Node { + type: 'link'; + url: string; + children: Node[]; + data?: { + hProperties?: LinkNodeProps; + }; +} + +/** + * Image node + */ +export interface ImageNode extends Node { + type: 'image'; + url: string; + alt?: string; + title?: string; +} + +/** + * Text node + */ +export interface TextNode extends Node { + type: 'text'; + value: string; +} diff --git a/app/src/utils/fileHelpers.test.ts b/app/src/utils/fileHelpers.test.ts new file mode 100644 index 0000000..f370d64 --- /dev/null +++ b/app/src/utils/fileHelpers.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { isImageFile, getFileUrl } from './fileHelpers'; + +describe('fileHelpers', () => { + beforeEach(() => { + // Ensure API_BASE_URL is set for tests + window.API_BASE_URL = 'http://localhost:8080/api/v1'; + }); + + describe('isImageFile', () => { + it('returns true for supported image file extensions', () => { + expect(isImageFile('image.jpg')).toBe(true); + expect(isImageFile('image.jpeg')).toBe(true); + expect(isImageFile('image.png')).toBe(true); + expect(isImageFile('image.gif')).toBe(true); + expect(isImageFile('image.webp')).toBe(true); + expect(isImageFile('image.svg')).toBe(true); + }); + + it('returns true for uppercase image file extensions', () => { + expect(isImageFile('image.JPG')).toBe(true); + expect(isImageFile('image.JPEG')).toBe(true); + expect(isImageFile('image.PNG')).toBe(true); + expect(isImageFile('image.GIF')).toBe(true); + expect(isImageFile('image.WEBP')).toBe(true); + expect(isImageFile('image.SVG')).toBe(true); + }); + + it('returns true for mixed case image file extensions', () => { + expect(isImageFile('image.JpG')).toBe(true); + expect(isImageFile('image.JpEg')).toBe(true); + expect(isImageFile('image.PnG')).toBe(true); + expect(isImageFile('screenshot.WeBp')).toBe(true); + }); + + it('returns false for non-image file extensions', () => { + expect(isImageFile('document.md')).toBe(false); + expect(isImageFile('document.txt')).toBe(false); + expect(isImageFile('document.pdf')).toBe(false); + expect(isImageFile('document.docx')).toBe(false); + expect(isImageFile('script.js')).toBe(false); + expect(isImageFile('style.css')).toBe(false); + expect(isImageFile('data.json')).toBe(false); + expect(isImageFile('archive.zip')).toBe(false); + }); + + it('returns false for files without extensions', () => { + expect(isImageFile('README')).toBe(false); + expect(isImageFile('Dockerfile')).toBe(false); + expect(isImageFile('LICENSE')).toBe(false); + expect(isImageFile('Makefile')).toBe(false); + }); + + it('handles complex file paths correctly', () => { + expect(isImageFile('path/to/image.jpg')).toBe(true); + expect(isImageFile('./relative/path/image.png')).toBe(true); + expect(isImageFile('/absolute/path/image.gif')).toBe(true); + expect(isImageFile('../../parent/image.svg')).toBe(true); + expect(isImageFile('path/to/document.md')).toBe(false); + expect(isImageFile('./config/settings.json')).toBe(false); + }); + + it('handles files with multiple dots in filename', () => { + expect(isImageFile('my.image.file.jpg')).toBe(true); + expect(isImageFile('config.backup.json')).toBe(false); + expect(isImageFile('version.1.2.png')).toBe(true); + expect(isImageFile('app.config.local.js')).toBe(false); + expect(isImageFile('test.component.spec.ts')).toBe(false); + }); + + it('handles edge cases', () => { + expect(isImageFile('')).toBe(false); + expect(isImageFile('.')).toBe(false); + expect(isImageFile('.jpg')).toBe(true); + expect(isImageFile('.hidden.png')).toBe(true); + expect(isImageFile('file.')).toBe(false); + }); + }); + + describe('getFileUrl', () => { + it('constructs correct file URL with simple parameters', () => { + const workspaceName = 'my-workspace'; + const filePath = 'folder/file.md'; + + const expectedUrl = + 'http://localhost:8080/api/v1/workspaces/my-workspace/files/folder%2Ffile.md'; + const actualUrl = getFileUrl(workspaceName, filePath); + + expect(actualUrl).toBe(expectedUrl); + }); + + it('properly encodes workspace name with special characters', () => { + const workspaceName = 'my workspace with spaces'; + const filePath = 'file.md'; + + const expectedUrl = + 'http://localhost:8080/api/v1/workspaces/my%20workspace%20with%20spaces/files/file.md'; + const actualUrl = getFileUrl(workspaceName, filePath); + + expect(actualUrl).toBe(expectedUrl); + }); + + it('properly encodes file path with special characters', () => { + const workspaceName = 'workspace'; + const filePath = 'folder with spaces/file with spaces.md'; + + const expectedUrl = + 'http://localhost:8080/api/v1/workspaces/workspace/files/folder%20with%20spaces%2Ffile%20with%20spaces.md'; + const actualUrl = getFileUrl(workspaceName, filePath); + + expect(actualUrl).toBe(expectedUrl); + }); + + it('handles special URL characters that need encoding', () => { + const workspaceName = 'test&workspace'; + const filePath = 'file?name=test.md'; + + const expectedUrl = + 'http://localhost:8080/api/v1/workspaces/test%26workspace/files/file%3Fname%3Dtest.md'; + const actualUrl = getFileUrl(workspaceName, filePath); + + expect(actualUrl).toBe(expectedUrl); + }); + + it('handles Unicode characters', () => { + const workspaceName = 'プロジェクト'; + const filePath = 'ファイル.md'; + + const expectedUrl = + 'http://localhost:8080/api/v1/workspaces/%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/files/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md'; + const actualUrl = getFileUrl(workspaceName, filePath); + + expect(actualUrl).toBe(expectedUrl); + }); + + it('handles nested folder structures', () => { + const workspaceName = 'docs'; + const filePath = 'projects/2024/q1/report.md'; + + const expectedUrl = + 'http://localhost:8080/api/v1/workspaces/docs/files/projects%2F2024%2Fq1%2Freport.md'; + const actualUrl = getFileUrl(workspaceName, filePath); + + expect(actualUrl).toBe(expectedUrl); + }); + + it('handles edge cases with empty strings', () => { + expect(getFileUrl('', '')).toBe( + 'http://localhost:8080/api/v1/workspaces//files/' + ); + expect(getFileUrl('workspace', '')).toBe( + 'http://localhost:8080/api/v1/workspaces/workspace/files/' + ); + expect(getFileUrl('', 'file.md')).toBe( + 'http://localhost:8080/api/v1/workspaces//files/file.md' + ); + }); + + it('uses the API base URL correctly', () => { + // Test that the function uses the expected API base URL + // Note: The API_BASE_URL is imported at module load time, so we test the expected behavior + const url = getFileUrl('test', 'file.md'); + expect(url).toBe( + 'http://localhost:8080/api/v1/workspaces/test/files/file.md' + ); + expect(url).toContain(window.API_BASE_URL); + }); + }); +}); diff --git a/app/src/utils/formatBytes.test.ts b/app/src/utils/formatBytes.test.ts new file mode 100644 index 0000000..8affcd7 --- /dev/null +++ b/app/src/utils/formatBytes.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from 'vitest'; +import { formatBytes } from './formatBytes'; + +describe('formatBytes', () => { + describe('bytes formatting', () => { + it('formats small byte values correctly', () => { + expect(formatBytes(0)).toBe('0.0 B'); + expect(formatBytes(1)).toBe('1.0 B'); + expect(formatBytes(512)).toBe('512.0 B'); + expect(formatBytes(1023)).toBe('1023.0 B'); + }); + }); + + describe('kilobytes formatting', () => { + it('formats kilobyte values correctly', () => { + expect(formatBytes(1024)).toBe('1.0 KB'); + expect(formatBytes(1536)).toBe('1.5 KB'); + expect(formatBytes(2048)).toBe('2.0 KB'); + expect(formatBytes(5120)).toBe('5.0 KB'); + expect(formatBytes(1048575)).toBe('1024.0 KB'); // Just under 1MB + }); + + it('handles fractional kilobytes', () => { + expect(formatBytes(1433)).toBe('1.4 KB'); + expect(formatBytes(1587)).toBe('1.5 KB'); + expect(formatBytes(1741)).toBe('1.7 KB'); + }); + }); + + describe('megabytes formatting', () => { + it('formats megabyte values correctly', () => { + expect(formatBytes(1048576)).toBe('1.0 MB'); // 1024^2 + expect(formatBytes(1572864)).toBe('1.5 MB'); + expect(formatBytes(2097152)).toBe('2.0 MB'); + expect(formatBytes(5242880)).toBe('5.0 MB'); + expect(formatBytes(1073741823)).toBe('1024.0 MB'); // Just under 1GB + }); + + it('handles fractional megabytes', () => { + expect(formatBytes(1638400)).toBe('1.6 MB'); + expect(formatBytes(2621440)).toBe('2.5 MB'); + expect(formatBytes(10485760)).toBe('10.0 MB'); + }); + }); + + describe('gigabytes formatting', () => { + it('formats gigabyte values correctly', () => { + expect(formatBytes(1073741824)).toBe('1.0 GB'); // 1024^3 + expect(formatBytes(1610612736)).toBe('1.5 GB'); + expect(formatBytes(2147483648)).toBe('2.0 GB'); + expect(formatBytes(5368709120)).toBe('5.0 GB'); + }); + + it('handles fractional gigabytes', () => { + expect(formatBytes(1288490188.8)).toBe('1.2 GB'); + expect(formatBytes(3221225472)).toBe('3.0 GB'); + expect(formatBytes(10737418240)).toBe('10.0 GB'); + }); + + it('handles very large gigabyte values', () => { + expect(formatBytes(1099511627776)).toBe('1024.0 GB'); // 1TB but capped at GB + expect(formatBytes(2199023255552)).toBe('2048.0 GB'); // 2TB but capped at GB + }); + }); + + describe('decimal precision', () => { + it('always shows one decimal place', () => { + expect(formatBytes(1024)).toBe('1.0 KB'); + expect(formatBytes(1536)).toBe('1.5 KB'); + expect(formatBytes(1048576)).toBe('1.0 MB'); + expect(formatBytes(1073741824)).toBe('1.0 GB'); + }); + + it('rounds to one decimal place correctly', () => { + expect(formatBytes(1126)).toBe('1.1 KB'); // 1126 / 1024 = 1.099... + expect(formatBytes(1177)).toBe('1.1 KB'); // 1177 / 1024 = 1.149... + expect(formatBytes(1229)).toBe('1.2 KB'); // 1229 / 1024 = 1.200... + }); + }); + + describe('edge cases', () => { + it('handles exact unit boundaries', () => { + expect(formatBytes(1024)).toBe('1.0 KB'); + expect(formatBytes(1048576)).toBe('1.0 MB'); + expect(formatBytes(1073741824)).toBe('1.0 GB'); + }); + + it('handles very small decimal values', () => { + expect(formatBytes(0.1)).toBe('0.1 B'); + expect(formatBytes(0.9)).toBe('0.9 B'); + }); + + it('handles negative values (edge case)', () => { + expect(() => formatBytes(-1024)).toThrowError( + 'Byte size cannot be negative' + ); + expect(() => formatBytes(-1048576)).toThrowError( + 'Byte size cannot be negative' + ); + }); + + it('handles extremely large values', () => { + const largeValue = Number.MAX_SAFE_INTEGER; + const result = formatBytes(largeValue); + expect(result).toContain('GB'); + expect(result).toMatch(/^\d+\.\d GB$/); + }); + }); + + describe('unit progression', () => { + it('uses the correct unit for each range', () => { + expect(formatBytes(500)).toContain('B'); + expect(formatBytes(5000)).toContain('KB'); + expect(formatBytes(5000000)).toContain('MB'); + expect(formatBytes(5000000000)).toContain('GB'); + }); + + it('stops at GB unit (does not go to TB)', () => { + const oneTerabyte = 1024 * 1024 * 1024 * 1024; + expect(formatBytes(oneTerabyte)).toContain('GB'); + expect(formatBytes(oneTerabyte)).not.toContain('TB'); + }); + }); +}); diff --git a/app/src/utils/formatBytes.ts b/app/src/utils/formatBytes.ts index 595c61a..3c4cf6a 100644 --- a/app/src/utils/formatBytes.ts +++ b/app/src/utils/formatBytes.ts @@ -16,6 +16,9 @@ const UNITS: readonly ByteUnit[] = ['B', 'KB', 'MB', 'GB'] as const; export const formatBytes = (bytes: number): string => { let size: number = bytes; let unitIndex: number = 0; + if (size < 0) { + throw new Error('Byte size cannot be negative'); + } while (size >= 1024 && unitIndex < UNITS.length - 1) { size /= 1024; unitIndex++; diff --git a/app/src/utils/themeStyle.test.ts b/app/src/utils/themeStyle.test.ts new file mode 100644 index 0000000..b3a2352 --- /dev/null +++ b/app/src/utils/themeStyle.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect } from 'vitest'; +import type { MantineTheme } from '@mantine/core'; +import { + getHoverStyle, + getConditionalColor, + getAccordionStyles, + getWorkspacePaperStyle, + getTextColor, +} from './themeStyles'; + +// Create partial mock themes with only the properties we need +const createMockTheme = (colorScheme: 'light' | 'dark') => ({ + radius: { sm: '4px' }, + spacing: { md: '16px' }, + colors: { + dark: [ + '#fff', + '#f8f9fa', + '#e9ecef', + '#dee2e6', + '#ced4da', + '#adb5bd', + '#6c757d', + '#495057', + '#343a40', + '#212529', + ], + gray: [ + '#f8f9fa', + '#e9ecef', + '#dee2e6', + '#ced4da', + '#adb5bd', + '#6c757d', + '#495057', + '#343a40', + '#212529', + '#000', + ], + blue: [ + '#e7f5ff', + '#d0ebff', + '#a5d8ff', + '#74c0fc', + '#339af0', + '#228be6', + '#1971c2', + '#1864ab', + '#0b4fa8', + '#073e78', + ], + }, + colorScheme, +}); + +const mockLightTheme = createMockTheme('light') as unknown as MantineTheme; +const mockDarkTheme = createMockTheme('dark') as unknown as MantineTheme; + +describe('themeStyles utilities', () => { + describe('getHoverStyle', () => { + it('returns correct hover styles for light theme', () => { + const result = getHoverStyle(mockLightTheme); + + expect(result).toEqual({ + borderRadius: '4px', + '&:hover': { + backgroundColor: '#f8f9fa', // gray[0] for light theme + }, + }); + }); + + it('returns correct hover styles for dark theme', () => { + const result = getHoverStyle(mockDarkTheme); + + expect(result).toEqual({ + borderRadius: '4px', + '&:hover': { + backgroundColor: '#adb5bd', // dark[5] for dark theme + }, + }); + }); + }); + + describe('getConditionalColor', () => { + it('returns blue color when selected in light theme', () => { + const result = getConditionalColor(mockLightTheme, true); + expect(result).toBe('#1864ab'); // blue[7] for light theme + }); + + it('returns blue color when selected in dark theme', () => { + const result = getConditionalColor(mockDarkTheme, true); + expect(result).toBe('#a5d8ff'); // blue[2] for dark theme + }); + + it('returns dimmed when not selected', () => { + expect(getConditionalColor(mockLightTheme, false)).toBe('dimmed'); + expect(getConditionalColor(mockDarkTheme, false)).toBe('dimmed'); + }); + + it('defaults to dimmed when no selection parameter provided', () => { + expect(getConditionalColor(mockLightTheme)).toBe('dimmed'); + expect(getConditionalColor(mockDarkTheme)).toBe('dimmed'); + }); + }); + + describe('getAccordionStyles', () => { + it('returns correct accordion styles for light theme', () => { + const result = getAccordionStyles(mockLightTheme); + + expect(result.control.paddingTop).toBe('16px'); + expect(result.control.paddingBottom).toBe('16px'); + expect(result.item.borderBottom).toBe('1px solid #ced4da'); // gray[3] + expect(result.item['&[data-active]'].backgroundColor).toBe('#f8f9fa'); // gray[0] + }); + + it('returns correct accordion styles for dark theme', () => { + const result = getAccordionStyles(mockDarkTheme); + + expect(result.control.paddingTop).toBe('16px'); + expect(result.control.paddingBottom).toBe('16px'); + expect(result.item.borderBottom).toBe('1px solid #ced4da'); // dark[4] + expect(result.item['&[data-active]'].backgroundColor).toBe('#495057'); // dark[7] + }); + }); + + describe('getWorkspacePaperStyle', () => { + it('returns selected styles for light theme when selected', () => { + const result = getWorkspacePaperStyle(mockLightTheme, true); + + expect(result.backgroundColor).toBe('#d0ebff'); // blue[1] + expect(result.borderColor).toBe('#228be6'); // blue[5] + }); + + it('returns selected styles for dark theme when selected', () => { + const result = getWorkspacePaperStyle(mockDarkTheme, true); + + expect(result.backgroundColor).toBe('#0b4fa8'); // blue[8] + expect(result.borderColor).toBe('#1864ab'); // blue[7] + }); + + it('returns undefined styles when not selected', () => { + const result = getWorkspacePaperStyle(mockLightTheme, false); + + expect(result.backgroundColor).toBeUndefined(); + expect(result.borderColor).toBeUndefined(); + }); + }); + + describe('getTextColor', () => { + it('returns blue text color when selected in light theme', () => { + const result = getTextColor(mockLightTheme, true); + expect(result).toBe('#073e78'); // blue[9] + }); + + it('returns blue text color when selected in dark theme', () => { + const result = getTextColor(mockDarkTheme, true); + expect(result).toBe('#e7f5ff'); // blue[0] + }); + + it('returns null when not selected', () => { + expect(getTextColor(mockLightTheme, false)).toBeNull(); + expect(getTextColor(mockDarkTheme, false)).toBeNull(); + }); + }); +});