mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Implement utils tests
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { Parent } from 'unist';
|
||||
|
||||
/**
|
||||
* User model from the API
|
||||
*/
|
||||
@@ -285,3 +287,66 @@ export interface SettingsAction<T> {
|
||||
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;
|
||||
}
|
||||
|
||||
169
app/src/utils/fileHelpers.test.ts
Normal file
169
app/src/utils/fileHelpers.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
124
app/src/utils/formatBytes.test.ts
Normal file
124
app/src/utils/formatBytes.test.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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++;
|
||||
|
||||
165
app/src/utils/themeStyle.test.ts
Normal file
165
app/src/utils/themeStyle.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user