[FE-TEST-004] test: Add unit tests for utilities
- Created comprehensive unit tests for date utilities - Created comprehensive unit tests for format utilities - Created comprehensive unit tests for URL utilities - Created comprehensive unit tests for logger utility - Created comprehensive unit tests for errorMessages utility - Created comprehensive unit tests for sanitize utility - Created comprehensive unit tests for apiErrorHandler utility - Created comprehensive unit tests for apiToastHelper utility - Created comprehensive unit tests for serviceErrorHandler utility - Created comprehensive unit tests for timeoutHandler utility All tests pass (163 tests). Covers all utility functions that were missing tests. Phase: PHASE-5 Priority: P2 Progress: 241/267 (90.26%)
This commit is contained in:
parent
873dca32a3
commit
fd516c143e
11 changed files with 1895 additions and 11 deletions
|
|
@ -6083,7 +6083,7 @@
|
|||
"files_changed": [
|
||||
"veza-backend-api/tests/marketplace/marketplace_flow_test.go"
|
||||
],
|
||||
"notes": "Created comprehensive integration test suite for marketplace flow. Tests cover: Complete flow (product creation \u2192 order \u2192 download), Product creation validation (missing fields, invalid price, invalid product type), Order creation validation (empty items, non-existent product), Download URL retrieval without license, Order listing, Order details retrieval, Order creation with inactive product. All tests pass successfully. Handled SQLite compatibility by creating tables manually without PostgreSQL-specific gen_random_uuid() default.",
|
||||
"notes": "Created comprehensive integration test suite for marketplace flow. Tests cover: Complete flow (product creation → order → download), Product creation validation (missing fields, invalid price, invalid product type), Order creation validation (empty items, non-existent product), Download URL retrieval without license, Order listing, Order details retrieval, Order creation with inactive product. All tests pass successfully. Handled SQLite compatibility by creating tables manually without PostgreSQL-specific gen_random_uuid() default.",
|
||||
"issues_encountered": [
|
||||
"SQLite incompatibility with gen_random_uuid() PostgreSQL function - resolved by creating tables manually and using BeforeCreate hooks to generate UUIDs"
|
||||
]
|
||||
|
|
@ -7809,7 +7809,7 @@
|
|||
],
|
||||
"notes": "",
|
||||
"completed_at": "2025-12-25T15:00:00.000Z",
|
||||
"implementation_notes": "Enhanced accessibility (a11y) across key components. Improvements: Enhanced TrackCard with better ARIA labels for play/pause buttons (dynamic labels based on state), keyboard navigation support (Enter/Space keys), focus management with visible focus rings, screen reader support with sr-only text for icon-only buttons, improved button labels with context (track title). Enhanced Pagination component with French ARIA labels (Premi\u00e8re page, Page pr\u00e9c\u00e9dente, Page suivante, Derni\u00e8re page), keyboard navigation support for all buttons, proper role=\"navigation\" attribute, aria-current for current page, and screen reader support with sr-only text. Many components already had good accessibility (Tabs, Dropdown, FocusTrap, player controls), and this task focused on improving the remaining interactive components for better keyboard navigation and screen reader support."
|
||||
"implementation_notes": "Enhanced accessibility (a11y) across key components. Improvements: Enhanced TrackCard with better ARIA labels for play/pause buttons (dynamic labels based on state), keyboard navigation support (Enter/Space keys), focus management with visible focus rings, screen reader support with sr-only text for icon-only buttons, improved button labels with context (track title). Enhanced Pagination component with French ARIA labels (Première page, Page précédente, Page suivante, Dernière page), keyboard navigation support for all buttons, proper role=\"navigation\" attribute, aria-current for current page, and screen reader support with sr-only text. Many components already had good accessibility (Tabs, Dropdown, FocusTrap, player controls), and this task focused on improving the remaining interactive components for better keyboard navigation and screen reader support."
|
||||
},
|
||||
{
|
||||
"id": "FE-COMP-020",
|
||||
|
|
@ -9761,7 +9761,7 @@
|
|||
"description": "Test all utility functions",
|
||||
"owner": "frontend",
|
||||
"estimated_hours": 4,
|
||||
"status": "todo",
|
||||
"status": "completed",
|
||||
"files_involved": [],
|
||||
"implementation_steps": [
|
||||
{
|
||||
|
|
@ -9782,7 +9782,26 @@
|
|||
"Unit tests",
|
||||
"Integration tests"
|
||||
],
|
||||
"notes": ""
|
||||
"notes": "",
|
||||
"completion": {
|
||||
"completed_at": "2025-12-25T16:09:46.894020Z",
|
||||
"actual_hours": 2.0,
|
||||
"commits": [],
|
||||
"files_changed": [
|
||||
"apps/web/src/utils/date.test.ts",
|
||||
"apps/web/src/utils/format.test.ts",
|
||||
"apps/web/src/utils/url.test.ts",
|
||||
"apps/web/src/utils/logger.test.ts",
|
||||
"apps/web/src/utils/errorMessages.test.ts",
|
||||
"apps/web/src/utils/sanitize.test.ts",
|
||||
"apps/web/src/utils/apiErrorHandler.test.ts",
|
||||
"apps/web/src/utils/apiToastHelper.test.ts",
|
||||
"apps/web/src/utils/serviceErrorHandler.test.ts",
|
||||
"apps/web/src/utils/timeoutHandler.test.ts"
|
||||
],
|
||||
"notes": "Created comprehensive unit tests for all utility functions: date, format, url, logger, errorMessages, sanitize, apiErrorHandler, apiToastHelper, serviceErrorHandler, timeoutHandler. All tests pass.",
|
||||
"issues_encountered": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "FE-TEST-005",
|
||||
|
|
@ -10395,7 +10414,7 @@
|
|||
"Unit tests",
|
||||
"Integration tests"
|
||||
],
|
||||
"notes": "Standardized all paginated responses to use consistent format:\n- Updated PaginationData struct to use snake_case (HasPrevious \u2192 has_prev, PreviousCursor \u2192 prev_cursor)\n- Created BuildPaginationData helper function for consistent pagination creation\n- Created BuildPaginationDataWithCursor helper for cursor-based pagination\n- Updated all handlers (ListTracks, ListUsers, SearchUsers, GetComments, SearchLogs) to use BuildPaginationData\n- All paginated responses now include: page, limit, total, total_pages, has_next, has_prev\n- Frontend already compatible with standardized format\n- Created PAGINATION_STANDARD.md documentation\n- Verified Go compilation passes",
|
||||
"notes": "Standardized all paginated responses to use consistent format:\n- Updated PaginationData struct to use snake_case (HasPrevious → has_prev, PreviousCursor → prev_cursor)\n- Created BuildPaginationData helper function for consistent pagination creation\n- Created BuildPaginationDataWithCursor helper for cursor-based pagination\n- Updated all handlers (ListTracks, ListUsers, SearchUsers, GetComments, SearchLogs) to use BuildPaginationData\n- All paginated responses now include: page, limit, total, total_pages, has_next, has_prev\n- Frontend already compatible with standardized format\n- Created PAGINATION_STANDARD.md documentation\n- Verified Go compilation passes",
|
||||
"completed_at": "2025-12-25T14:14:24.469181Z"
|
||||
},
|
||||
{
|
||||
|
|
@ -11985,14 +12004,14 @@
|
|||
]
|
||||
},
|
||||
"progress_tracking": {
|
||||
"completed": 158,
|
||||
"completed": 241,
|
||||
"in_progress": 0,
|
||||
"todo": 121,
|
||||
"todo": 26,
|
||||
"blocked": 0,
|
||||
"last_updated": "2025-12-25T16:02:42.227034Z",
|
||||
"completion_percentage": 89.89,
|
||||
"last_updated": "2025-12-25T16:09:46.894144Z",
|
||||
"completion_percentage": 90.26,
|
||||
"total_tasks": 267,
|
||||
"completed_tasks": 240,
|
||||
"remaining_tasks": 27
|
||||
"completed_tasks": 241,
|
||||
"remaining_tasks": 26
|
||||
}
|
||||
}
|
||||
237
apps/web/src/utils/apiErrorHandler.test.ts
Normal file
237
apps/web/src/utils/apiErrorHandler.test.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* Tests for API Error Handler Utility
|
||||
* FE-TEST-004: Test API error handler utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AxiosError } from 'axios';
|
||||
import { parseApiError, formatErrorMessage, getValidationErrors } from './apiErrorHandler';
|
||||
import type { ApiError } from '@/types/api';
|
||||
|
||||
// Mock timeoutHandler
|
||||
vi.mock('./timeoutHandler', () => ({
|
||||
isTimeoutError: vi.fn((error: unknown) => {
|
||||
if (error && typeof error === 'object' && 'code' in error) {
|
||||
return (error as any).code === 'ECONNABORTED' || (error as any).code === 'ETIMEDOUT';
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
TIMEOUT_MESSAGES: {
|
||||
timeout: 'Request timeout',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('apiErrorHandler utilities', () => {
|
||||
describe('parseApiError', () => {
|
||||
it('should return ApiError as-is', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = parseApiError(error);
|
||||
expect(result).toEqual(error);
|
||||
});
|
||||
|
||||
it('should parse AxiosError with standard format', () => {
|
||||
const axiosError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 404,
|
||||
data: {
|
||||
success: false,
|
||||
error: {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as AxiosError;
|
||||
|
||||
const result = parseApiError(axiosError);
|
||||
expect(result.code).toBe(404);
|
||||
expect(result.message).toBe('Not found');
|
||||
});
|
||||
|
||||
it('should parse AxiosError with Gin middleware format', () => {
|
||||
const axiosError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 422,
|
||||
data: {
|
||||
error: {
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as AxiosError;
|
||||
|
||||
const result = parseApiError(axiosError);
|
||||
expect(result.code).toBe(422);
|
||||
expect(result.message).toBe('Validation failed');
|
||||
});
|
||||
|
||||
it('should parse network error', () => {
|
||||
const axiosError = {
|
||||
isAxiosError: true,
|
||||
request: {},
|
||||
response: undefined,
|
||||
} as unknown as AxiosError;
|
||||
|
||||
const result = parseApiError(axiosError);
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.message).toContain('Network error');
|
||||
});
|
||||
|
||||
it('should parse timeout error', () => {
|
||||
const axiosError = {
|
||||
isAxiosError: true,
|
||||
request: {},
|
||||
response: undefined,
|
||||
code: 'ECONNABORTED',
|
||||
} as unknown as AxiosError;
|
||||
|
||||
const result = parseApiError(axiosError);
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.message).toBe('Request timeout');
|
||||
});
|
||||
|
||||
it('should parse rate limit error with headers', () => {
|
||||
const axiosError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'x-ratelimit-limit': '100',
|
||||
'x-ratelimit-remaining': '0',
|
||||
'x-ratelimit-reset': String(Math.floor(Date.now() / 1000) + 60),
|
||||
'retry-after': '60',
|
||||
},
|
||||
data: {
|
||||
error: {
|
||||
message: 'Rate limit exceeded',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as AxiosError;
|
||||
|
||||
const result = parseApiError(axiosError);
|
||||
expect(result.code).toBe(429);
|
||||
expect(result.rate_limit).toBeDefined();
|
||||
expect(result.retry_after).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse service unavailable error', () => {
|
||||
const axiosError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 503,
|
||||
data: {
|
||||
message: 'Service unavailable',
|
||||
},
|
||||
},
|
||||
} as unknown as AxiosError;
|
||||
|
||||
const result = parseApiError(axiosError);
|
||||
expect(result.code).toBe(503);
|
||||
expect(result.message).toContain('indisponible');
|
||||
});
|
||||
|
||||
it('should parse standard Error', () => {
|
||||
const error = new Error('Test error');
|
||||
const result = parseApiError(error);
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.message).toBe('Test error');
|
||||
});
|
||||
|
||||
it('should handle unknown error', () => {
|
||||
const result = parseApiError(null);
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.message).toBe('An unexpected error occurred');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatErrorMessage', () => {
|
||||
it('should format simple error', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = formatErrorMessage(error);
|
||||
expect(result).toBe('Not found');
|
||||
});
|
||||
|
||||
it('should include validation details', () => {
|
||||
const error: ApiError = {
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
details: [
|
||||
{ field: 'email', message: 'Invalid email' },
|
||||
{ field: 'password', message: 'Too short' },
|
||||
],
|
||||
};
|
||||
const result = formatErrorMessage(error);
|
||||
expect(result).toContain('email');
|
||||
expect(result).toContain('password');
|
||||
});
|
||||
|
||||
it('should include request_id in dev mode', () => {
|
||||
const error: ApiError = {
|
||||
code: 500,
|
||||
message: 'Server error',
|
||||
timestamp: new Date().toISOString(),
|
||||
request_id: 'req-123',
|
||||
};
|
||||
const result = formatErrorMessage(error, true);
|
||||
expect(result).toContain('req-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidationErrors', () => {
|
||||
it('should extract validation errors', () => {
|
||||
const error: ApiError = {
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
details: [
|
||||
{ field: 'email', message: 'Invalid email' },
|
||||
{ field: 'password', message: 'Too short' },
|
||||
],
|
||||
};
|
||||
const result = getValidationErrors(error);
|
||||
expect(result.email).toBe('Invalid email');
|
||||
expect(result.password).toBe('Too short');
|
||||
});
|
||||
|
||||
it('should return empty object for errors without details', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = getValidationErrors(error);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should filter out invalid details', () => {
|
||||
const error: ApiError = {
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
details: [
|
||||
{ field: 'email', message: 'Invalid email' },
|
||||
{ field: '', message: 'No field' },
|
||||
{ message: 'No field' },
|
||||
] as any,
|
||||
};
|
||||
const result = getValidationErrors(error);
|
||||
expect(result.email).toBe('Invalid email');
|
||||
expect(result['']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
102
apps/web/src/utils/apiToastHelper.test.ts
Normal file
102
apps/web/src/utils/apiToastHelper.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Tests for API Toast Helper Utility
|
||||
* FE-TEST-004: Test API toast helper utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { InternalAxiosRequestConfig } from 'axios';
|
||||
import {
|
||||
withSuccessToast,
|
||||
withoutErrorToast,
|
||||
showSuccessToast,
|
||||
showErrorToast,
|
||||
showInfoToast,
|
||||
showWarningToast,
|
||||
} from './apiToastHelper';
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock('react-hot-toast', () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
loading: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
describe('apiToastHelper utilities', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('withSuccessToast', () => {
|
||||
it('should add success toast flag to config', () => {
|
||||
const config: InternalAxiosRequestConfig = {
|
||||
url: '/api/test',
|
||||
method: 'get',
|
||||
};
|
||||
const result = withSuccessToast(config);
|
||||
expect((result as any)._showSuccessToast).toBe(true);
|
||||
});
|
||||
|
||||
it('should add custom success message', () => {
|
||||
const config: InternalAxiosRequestConfig = {
|
||||
url: '/api/test',
|
||||
method: 'get',
|
||||
};
|
||||
const result = withSuccessToast(config, 'Custom success');
|
||||
expect((result as any)._successMessage).toBe('Custom success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('withoutErrorToast', () => {
|
||||
it('should add disable toast flag to config', () => {
|
||||
const config: InternalAxiosRequestConfig = {
|
||||
url: '/api/test',
|
||||
method: 'get',
|
||||
};
|
||||
const result = withoutErrorToast(config);
|
||||
expect((result as any)._disableToast).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showSuccessToast', () => {
|
||||
it('should call toast.success', () => {
|
||||
showSuccessToast('Success message');
|
||||
expect(toast.success).toHaveBeenCalledWith('Success message', { duration: undefined });
|
||||
});
|
||||
|
||||
it('should call toast.success with duration', () => {
|
||||
showSuccessToast('Success message', 5000);
|
||||
expect(toast.success).toHaveBeenCalledWith('Success message', { duration: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('showErrorToast', () => {
|
||||
it('should call toast.error', () => {
|
||||
showErrorToast('Error message');
|
||||
expect(toast.error).toHaveBeenCalledWith('Error message', { duration: undefined });
|
||||
});
|
||||
|
||||
it('should call toast.error with duration', () => {
|
||||
showErrorToast('Error message', 5000);
|
||||
expect(toast.error).toHaveBeenCalledWith('Error message', { duration: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('showInfoToast', () => {
|
||||
it('should call toast with info icon', () => {
|
||||
showInfoToast('Info message');
|
||||
expect(toast).toHaveBeenCalledWith('Info message', { duration: undefined, icon: 'ℹ️' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('showWarningToast', () => {
|
||||
it('should call toast with warning icon', () => {
|
||||
showWarningToast('Warning message');
|
||||
expect(toast).toHaveBeenCalledWith('Warning message', { duration: undefined, icon: '⚠️' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
220
apps/web/src/utils/date.test.ts
Normal file
220
apps/web/src/utils/date.test.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/**
|
||||
* Tests for Date Utilities
|
||||
* FE-TEST-004: Test all date utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
isToday,
|
||||
isYesterday,
|
||||
getTimeAgo,
|
||||
formatDuration,
|
||||
parseDuration,
|
||||
} from './date';
|
||||
|
||||
describe('date utilities', () => {
|
||||
beforeEach(() => {
|
||||
// Mock Date.now() to have consistent tests
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date in short format', () => {
|
||||
const date = new Date('2024-01-15T10:00:00Z');
|
||||
const result = formatDate(date, 'short');
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should format date in long format', () => {
|
||||
const date = new Date('2024-01-15T10:00:00Z');
|
||||
const result = formatDate(date, 'long');
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should format date in relative format', () => {
|
||||
const date = new Date('2024-01-15T11:00:00Z');
|
||||
const result = formatDate(date, 'relative');
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should default to short format', () => {
|
||||
const date = new Date('2024-01-15T10:00:00Z');
|
||||
const result = formatDate(date);
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle string dates', () => {
|
||||
const result = formatDate('2024-01-15T10:00:00Z');
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should return "Invalid date" for invalid dates', () => {
|
||||
const result = formatDate('invalid-date');
|
||||
expect(result).toBe('Invalid date');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatRelativeTime', () => {
|
||||
it('should return "À l\'instant" for very recent dates', () => {
|
||||
const date = new Date('2024-01-15T11:59:30Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toBe("À l'instant");
|
||||
});
|
||||
|
||||
it('should format minutes ago', () => {
|
||||
const date = new Date('2024-01-15T11:45:00Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toContain('minute');
|
||||
});
|
||||
|
||||
it('should format hours ago', () => {
|
||||
const date = new Date('2024-01-15T10:00:00Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toContain('heure');
|
||||
});
|
||||
|
||||
it('should format days ago', () => {
|
||||
const date = new Date('2024-01-14T12:00:00Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toContain('jour');
|
||||
});
|
||||
|
||||
it('should format weeks ago', () => {
|
||||
const date = new Date('2024-01-08T12:00:00Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toContain('semaine');
|
||||
});
|
||||
|
||||
it('should format months ago', () => {
|
||||
const date = new Date('2023-12-15T12:00:00Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toContain('mois');
|
||||
});
|
||||
|
||||
it('should format years ago', () => {
|
||||
const date = new Date('2023-01-15T12:00:00Z');
|
||||
const result = formatRelativeTime(date);
|
||||
expect(result).toContain('an');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToday', () => {
|
||||
it('should return true for today', () => {
|
||||
const today = new Date('2024-01-15T12:00:00Z');
|
||||
expect(isToday(today)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for yesterday', () => {
|
||||
const yesterday = new Date('2024-01-14T12:00:00Z');
|
||||
expect(isToday(yesterday)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for tomorrow', () => {
|
||||
const tomorrow = new Date('2024-01-16T12:00:00Z');
|
||||
expect(isToday(tomorrow)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle string dates', () => {
|
||||
expect(isToday('2024-01-15T12:00:00Z')).toBe(true);
|
||||
expect(isToday('2024-01-14T12:00:00Z')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isYesterday', () => {
|
||||
it('should return true for yesterday', () => {
|
||||
const yesterday = new Date('2024-01-14T12:00:00Z');
|
||||
expect(isYesterday(yesterday)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for today', () => {
|
||||
const today = new Date('2024-01-15T12:00:00Z');
|
||||
expect(isYesterday(today)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for two days ago', () => {
|
||||
const twoDaysAgo = new Date('2024-01-13T12:00:00Z');
|
||||
expect(isYesterday(twoDaysAgo)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle string dates', () => {
|
||||
expect(isYesterday('2024-01-14T12:00:00Z')).toBe(true);
|
||||
expect(isYesterday('2024-01-15T12:00:00Z')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeAgo', () => {
|
||||
it('should return time for today', () => {
|
||||
const today = new Date('2024-01-15T10:30:00Z');
|
||||
const result = getTimeAgo(today);
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should return "Hier" for yesterday', () => {
|
||||
const yesterday = new Date('2024-01-14T12:00:00Z');
|
||||
const result = getTimeAgo(yesterday);
|
||||
expect(result).toBe('Hier');
|
||||
});
|
||||
|
||||
it('should return formatted date for older dates', () => {
|
||||
const oldDate = new Date('2024-01-10T12:00:00Z');
|
||||
const result = getTimeAgo(oldDate);
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('should format seconds only', () => {
|
||||
expect(formatDuration(45)).toBe('0:45');
|
||||
});
|
||||
|
||||
it('should format minutes and seconds', () => {
|
||||
expect(formatDuration(125)).toBe('2:05');
|
||||
});
|
||||
|
||||
it('should format hours, minutes and seconds', () => {
|
||||
expect(formatDuration(3665)).toBe('1:01:05');
|
||||
});
|
||||
|
||||
it('should handle zero', () => {
|
||||
expect(formatDuration(0)).toBe('0:00');
|
||||
});
|
||||
|
||||
it('should handle large durations', () => {
|
||||
expect(formatDuration(7200)).toBe('2:00:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDuration', () => {
|
||||
it('should parse MM:SS format', () => {
|
||||
expect(parseDuration('2:30')).toBe(150);
|
||||
});
|
||||
|
||||
it('should parse HH:MM:SS format', () => {
|
||||
expect(parseDuration('1:30:45')).toBe(5445);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid format', () => {
|
||||
expect(parseDuration('invalid')).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(parseDuration('0:00')).toBe(0);
|
||||
expect(parseDuration('0:05')).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
186
apps/web/src/utils/errorMessages.test.ts
Normal file
186
apps/web/src/utils/errorMessages.test.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* Tests for Error Messages Utility
|
||||
* FE-TEST-004: Test error message utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
ERROR_MESSAGES,
|
||||
CONTEXT_ERROR_MESSAGES,
|
||||
getErrorMessageByStatus,
|
||||
getContextErrorMessage,
|
||||
formatUserFriendlyError,
|
||||
isRetryableError,
|
||||
getRetryDelay,
|
||||
} from './errorMessages';
|
||||
import type { ApiError } from '@/types/api';
|
||||
|
||||
describe('errorMessages utilities', () => {
|
||||
describe('ERROR_MESSAGES', () => {
|
||||
it('should have messages for common status codes', () => {
|
||||
expect(ERROR_MESSAGES[400]).toBeTruthy();
|
||||
expect(ERROR_MESSAGES[401]).toBeTruthy();
|
||||
expect(ERROR_MESSAGES[404]).toBeTruthy();
|
||||
expect(ERROR_MESSAGES[500]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have network error messages', () => {
|
||||
expect(ERROR_MESSAGES.NETWORK).toBeTruthy();
|
||||
expect(ERROR_MESSAGES.TIMEOUT).toBeTruthy();
|
||||
expect(ERROR_MESSAGES.UNKNOWN).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getErrorMessageByStatus', () => {
|
||||
it('should return message for known status', () => {
|
||||
expect(getErrorMessageByStatus(404)).toBe(ERROR_MESSAGES[404]);
|
||||
expect(getErrorMessageByStatus(500)).toBe(ERROR_MESSAGES[500]);
|
||||
});
|
||||
|
||||
it('should return default message for unknown status', () => {
|
||||
expect(getErrorMessageByStatus(999)).toBe(ERROR_MESSAGES.UNKNOWN);
|
||||
});
|
||||
|
||||
it('should use custom default message', () => {
|
||||
const custom = 'Custom error';
|
||||
expect(getErrorMessageByStatus(999, custom)).toBe(custom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContextErrorMessage', () => {
|
||||
it('should return context-specific message', () => {
|
||||
expect(getContextErrorMessage('auth', 'login')).toBe(
|
||||
CONTEXT_ERROR_MESSAGES.auth.login,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return default for unknown context/action', () => {
|
||||
expect(getContextErrorMessage('auth', 'unknown')).toBe(ERROR_MESSAGES.UNKNOWN);
|
||||
});
|
||||
|
||||
it('should use custom default message', () => {
|
||||
const custom = 'Custom error';
|
||||
expect(getContextErrorMessage('auth', 'unknown', custom)).toBe(custom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUserFriendlyError', () => {
|
||||
it('should format ApiError', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = formatUserFriendlyError(error);
|
||||
expect(result).toBe(ERROR_MESSAGES[404]);
|
||||
});
|
||||
|
||||
it('should format Error instance', () => {
|
||||
const error = new Error('Test error');
|
||||
const result = formatUserFriendlyError(error);
|
||||
expect(result).toBe('Test error');
|
||||
});
|
||||
|
||||
it('should format with context', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = formatUserFriendlyError(error, 'playlist');
|
||||
expect(result).toBe(CONTEXT_ERROR_MESSAGES.playlist.notFound);
|
||||
});
|
||||
|
||||
it('should include details when requested', () => {
|
||||
const error: ApiError = {
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
details: [
|
||||
{ field: 'email', message: 'Invalid email' },
|
||||
{ field: 'password', message: 'Too short' },
|
||||
],
|
||||
};
|
||||
const result = formatUserFriendlyError(error, undefined, true);
|
||||
// Details are included in parentheses
|
||||
expect(result).toContain('Invalid email');
|
||||
expect(result).toContain('Too short');
|
||||
});
|
||||
|
||||
it('should handle network errors', () => {
|
||||
const error = { code: 'ERR_NETWORK' };
|
||||
const result = formatUserFriendlyError(error);
|
||||
expect(result).toBe(ERROR_MESSAGES.NETWORK);
|
||||
});
|
||||
|
||||
it('should handle timeout errors', () => {
|
||||
const error = { code: 'ECONNABORTED' };
|
||||
const result = formatUserFriendlyError(error);
|
||||
expect(result).toBe(ERROR_MESSAGES.TIMEOUT);
|
||||
});
|
||||
|
||||
it('should return unknown for unhandled errors', () => {
|
||||
const result = formatUserFriendlyError(null);
|
||||
expect(result).toBe(ERROR_MESSAGES.UNKNOWN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRetryableError', () => {
|
||||
it('should return true for retryable status codes', () => {
|
||||
const error429: ApiError = { code: 429, message: 'Rate limit', timestamp: new Date().toISOString() };
|
||||
const error500: ApiError = { code: 500, message: 'Server error', timestamp: new Date().toISOString() };
|
||||
const error503: ApiError = { code: 503, message: 'Service unavailable', timestamp: new Date().toISOString() };
|
||||
|
||||
expect(isRetryableError(error429)).toBe(true);
|
||||
expect(isRetryableError(error500)).toBe(true);
|
||||
expect(isRetryableError(error503)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for network errors', () => {
|
||||
expect(isRetryableError({ code: 'ERR_NETWORK' })).toBe(true);
|
||||
expect(isRetryableError({ code: 'ECONNABORTED' })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-retryable errors', () => {
|
||||
const error404: ApiError = { code: 404, message: 'Not found', timestamp: new Date().toISOString() };
|
||||
const error401: ApiError = { code: 401, message: 'Unauthorized', timestamp: new Date().toISOString() };
|
||||
|
||||
expect(isRetryableError(error404)).toBe(false);
|
||||
expect(isRetryableError(error401)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRetryDelay', () => {
|
||||
it('should use retry_after from rate limit error', () => {
|
||||
const error: ApiError = {
|
||||
code: 429,
|
||||
message: 'Rate limit',
|
||||
timestamp: new Date().toISOString(),
|
||||
retry_after: 5,
|
||||
};
|
||||
expect(getRetryDelay(error, 0)).toBe(5000);
|
||||
});
|
||||
|
||||
it('should use exponential backoff', () => {
|
||||
const error: ApiError = {
|
||||
code: 500,
|
||||
message: 'Server error',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(getRetryDelay(error, 0)).toBe(1000);
|
||||
expect(getRetryDelay(error, 1)).toBe(2000);
|
||||
expect(getRetryDelay(error, 2)).toBe(4000);
|
||||
expect(getRetryDelay(error, 3)).toBe(8000);
|
||||
});
|
||||
|
||||
it('should cap at 30 seconds', () => {
|
||||
const error: ApiError = {
|
||||
code: 500,
|
||||
message: 'Server error',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(getRetryDelay(error, 10)).toBe(30000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
256
apps/web/src/utils/format.test.ts
Normal file
256
apps/web/src/utils/format.test.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* Tests for Format Utilities
|
||||
* FE-TEST-004: Test all format utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
formatFileSize,
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatPercentage,
|
||||
truncate,
|
||||
capitalize,
|
||||
capitalizeWords,
|
||||
slugify,
|
||||
initials,
|
||||
formatUsername,
|
||||
formatEmail,
|
||||
formatPhoneNumber,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatTimeAgo,
|
||||
formatList,
|
||||
formatPlural,
|
||||
} from './format';
|
||||
|
||||
describe('format utilities', () => {
|
||||
describe('formatFileSize', () => {
|
||||
it('should format bytes', () => {
|
||||
expect(formatFileSize(0)).toBe('0 Bytes');
|
||||
expect(formatFileSize(500)).toBe('500 Bytes');
|
||||
});
|
||||
|
||||
it('should format KB', () => {
|
||||
expect(formatFileSize(1024)).toBe('1 KB');
|
||||
expect(formatFileSize(2048)).toBe('2 KB');
|
||||
});
|
||||
|
||||
it('should format MB', () => {
|
||||
expect(formatFileSize(1048576)).toBe('1 MB');
|
||||
});
|
||||
|
||||
it('should format GB', () => {
|
||||
expect(formatFileSize(1073741824)).toBe('1 GB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatNumber', () => {
|
||||
it('should format numbers less than 1000', () => {
|
||||
expect(formatNumber(500)).toBe('500');
|
||||
expect(formatNumber(999)).toBe('999');
|
||||
});
|
||||
|
||||
it('should format thousands', () => {
|
||||
expect(formatNumber(1500)).toBe('1.5K');
|
||||
expect(formatNumber(9999)).toBe('10.0K');
|
||||
});
|
||||
|
||||
it('should format millions', () => {
|
||||
expect(formatNumber(1500000)).toBe('1.5M');
|
||||
});
|
||||
|
||||
it('should format billions', () => {
|
||||
expect(formatNumber(1500000000)).toBe('1.5B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCurrency', () => {
|
||||
it('should format EUR by default', () => {
|
||||
const result = formatCurrency(1234.56);
|
||||
expect(result).toContain('1');
|
||||
expect(result).toContain('234');
|
||||
});
|
||||
|
||||
it('should format USD', () => {
|
||||
const result = formatCurrency(1234.56, 'USD');
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPercentage', () => {
|
||||
it('should format percentage with default decimals', () => {
|
||||
expect(formatPercentage(0.5)).toBe('50.0%');
|
||||
expect(formatPercentage(0.123)).toBe('12.3%');
|
||||
});
|
||||
|
||||
it('should format percentage with custom decimals', () => {
|
||||
expect(formatPercentage(0.5, 2)).toBe('50.00%');
|
||||
expect(formatPercentage(0.123, 0)).toBe('12%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncate', () => {
|
||||
it('should truncate long text', () => {
|
||||
expect(truncate('Hello World', 5)).toBe('He...');
|
||||
});
|
||||
|
||||
it('should not truncate short text', () => {
|
||||
expect(truncate('Hello', 10)).toBe('Hello');
|
||||
});
|
||||
|
||||
it('should use custom suffix', () => {
|
||||
expect(truncate('Hello World', 5, '…')).toBe('Hell…');
|
||||
});
|
||||
});
|
||||
|
||||
describe('capitalize', () => {
|
||||
it('should capitalize first letter', () => {
|
||||
expect(capitalize('hello')).toBe('Hello');
|
||||
expect(capitalize('WORLD')).toBe('World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('capitalizeWords', () => {
|
||||
it('should capitalize each word', () => {
|
||||
expect(capitalizeWords('hello world')).toBe('Hello World');
|
||||
expect(capitalizeWords('john doe')).toBe('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slugify', () => {
|
||||
it('should create slug from text', () => {
|
||||
expect(slugify('Hello World')).toBe('hello-world');
|
||||
expect(slugify('Test 123')).toBe('test-123');
|
||||
});
|
||||
|
||||
it('should remove special characters', () => {
|
||||
expect(slugify('Hello@World!')).toBe('helloworld');
|
||||
});
|
||||
|
||||
it('should handle multiple spaces', () => {
|
||||
expect(slugify('Hello World')).toBe('hello-world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initials', () => {
|
||||
it('should extract initials', () => {
|
||||
expect(initials('John Doe')).toBe('JD');
|
||||
expect(initials('John')).toBe('J');
|
||||
});
|
||||
|
||||
it('should handle single word', () => {
|
||||
expect(initials('John')).toBe('J');
|
||||
});
|
||||
|
||||
it('should handle multiple words', () => {
|
||||
expect(initials('John Michael Doe')).toBe('JM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUsername', () => {
|
||||
it('should add @ prefix', () => {
|
||||
expect(formatUsername('john')).toBe('@john');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatEmail', () => {
|
||||
it('should mask email', () => {
|
||||
expect(formatEmail('john.doe@example.com')).toBe('joh***@example.com');
|
||||
});
|
||||
|
||||
it('should handle short local part', () => {
|
||||
expect(formatEmail('ab@example.com')).toBe('ab@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPhoneNumber', () => {
|
||||
it('should format French phone number', () => {
|
||||
expect(formatPhoneNumber('0612345678')).toBe('06 12 34 56 78');
|
||||
});
|
||||
|
||||
it('should handle already formatted numbers', () => {
|
||||
expect(formatPhoneNumber('06 12 34 56 78')).toBe('06 12 34 56 78');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should format bytes with default decimals', () => {
|
||||
expect(formatBytes(0)).toBe('0 Bytes');
|
||||
expect(formatBytes(1024)).toBe('1 KB');
|
||||
});
|
||||
|
||||
it('should format with custom decimals', () => {
|
||||
// parseFloat removes trailing zeros, so 1.000 becomes 1
|
||||
expect(formatBytes(1024, 3)).toBe('1 KB');
|
||||
// Test with a value that actually shows decimals
|
||||
expect(formatBytes(1536, 3)).toBe('1.5 KB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('should format seconds', () => {
|
||||
expect(formatDuration(45)).toBe('0:45');
|
||||
});
|
||||
|
||||
it('should format minutes and seconds', () => {
|
||||
expect(formatDuration(125)).toBe('2:05');
|
||||
});
|
||||
|
||||
it('should format hours', () => {
|
||||
expect(formatDuration(3665)).toBe('1:01:05');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTimeAgo', () => {
|
||||
it('should format recent time', () => {
|
||||
const now = new Date();
|
||||
const result = formatTimeAgo(now);
|
||||
expect(result).toBe("À l'instant");
|
||||
});
|
||||
|
||||
it('should format minutes ago', () => {
|
||||
const past = new Date(Date.now() - 30 * 60 * 1000);
|
||||
const result = formatTimeAgo(past);
|
||||
expect(result).toContain('min');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatList', () => {
|
||||
it('should format empty list', () => {
|
||||
expect(formatList([])).toBe('');
|
||||
});
|
||||
|
||||
it('should format single item', () => {
|
||||
expect(formatList(['apple'])).toBe('apple');
|
||||
});
|
||||
|
||||
it('should format two items', () => {
|
||||
expect(formatList(['apple', 'banana'])).toBe('apple et banana');
|
||||
});
|
||||
|
||||
it('should format multiple items', () => {
|
||||
expect(formatList(['apple', 'banana', 'cherry'])).toBe('apple, banana et cherry');
|
||||
});
|
||||
|
||||
it('should use custom conjunction', () => {
|
||||
expect(formatList(['apple', 'banana'], 'ou')).toBe('apple ou banana');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPlural', () => {
|
||||
it('should format singular', () => {
|
||||
expect(formatPlural(1, 'item')).toBe('1 item');
|
||||
});
|
||||
|
||||
it('should format plural', () => {
|
||||
expect(formatPlural(2, 'item')).toBe('2 items');
|
||||
});
|
||||
|
||||
it('should use custom plural', () => {
|
||||
expect(formatPlural(2, 'child', 'children')).toBe('2 children');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
58
apps/web/src/utils/logger.test.ts
Normal file
58
apps/web/src/utils/logger.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Tests for Logger Utility
|
||||
* FE-TEST-004: Test logger utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { logger } from './logger';
|
||||
|
||||
describe('logger utilities', () => {
|
||||
let consoleDebugSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleInfoSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('logger.debug', () => {
|
||||
it('should log in development mode', () => {
|
||||
// In test environment, DEV is typically true
|
||||
logger.debug('test message');
|
||||
// Just verify it doesn't throw - actual behavior depends on import.meta.env.DEV
|
||||
expect(typeof logger.debug).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logger.info', () => {
|
||||
it('should log in development mode', () => {
|
||||
// In test environment, DEV is typically true
|
||||
logger.info('test message');
|
||||
// Just verify it doesn't throw - actual behavior depends on import.meta.env.DEV
|
||||
expect(typeof logger.info).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logger.warn', () => {
|
||||
it('should always log warnings', () => {
|
||||
logger.warn('test warning');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith('[WARN]', 'test warning');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logger.error', () => {
|
||||
it('should always log errors', () => {
|
||||
logger.error('test error');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR]', 'test error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
161
apps/web/src/utils/sanitize.test.ts
Normal file
161
apps/web/src/utils/sanitize.test.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Tests for Sanitize Utility
|
||||
* FE-TEST-004: Test sanitize utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
sanitizeHTML,
|
||||
sanitizeChatMessage,
|
||||
sanitizeTextInput,
|
||||
sanitizeURL,
|
||||
sanitizeEmail,
|
||||
validatePassword,
|
||||
} from './sanitize';
|
||||
|
||||
// Mock DOMPurify
|
||||
vi.mock('dompurify', () => ({
|
||||
default: {
|
||||
isSupported: true,
|
||||
sanitize: vi.fn((html: string) => html.replace(/<script[^>]*>.*?<\/script>/gi, '')),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('sanitize utilities', () => {
|
||||
describe('sanitizeHTML', () => {
|
||||
it('should remove script tags', () => {
|
||||
const input = '<p>Hello</p><script>alert("xss")</script>';
|
||||
const result = sanitizeHTML(input);
|
||||
expect(result).not.toContain('<script>');
|
||||
});
|
||||
|
||||
it('should escape HTML tags (sanitizeHTML escapes all HTML)', () => {
|
||||
const input = '<p>Hello <strong>world</strong></p>';
|
||||
const result = sanitizeHTML(input);
|
||||
// sanitizeHTML escapes all HTML, so tags become entities
|
||||
expect(result).toContain('<');
|
||||
expect(result).toContain('Hello');
|
||||
});
|
||||
|
||||
it('should remove dangerous patterns', () => {
|
||||
const input = '<p onclick="alert(1)">Hello</p>';
|
||||
const result = sanitizeHTML(input);
|
||||
expect(result).not.toContain('onclick');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeChatMessage', () => {
|
||||
it('should sanitize chat messages', () => {
|
||||
const input = '<p>Hello <script>alert("xss")</script></p>';
|
||||
const result = sanitizeChatMessage(input);
|
||||
expect(result).not.toContain('<script>');
|
||||
});
|
||||
|
||||
it('should preserve basic formatting', () => {
|
||||
const input = '<p>Hello <strong>world</strong></p>';
|
||||
const result = sanitizeChatMessage(input);
|
||||
expect(result).toContain('<p>');
|
||||
expect(result).toContain('<strong>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeTextInput', () => {
|
||||
it('should escape HTML', () => {
|
||||
const input = '<script>alert("xss")</script>';
|
||||
const result = sanitizeTextInput(input);
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).toContain('<');
|
||||
});
|
||||
|
||||
it('should trim input', () => {
|
||||
expect(sanitizeTextInput(' hello ')).toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeURL', () => {
|
||||
it('should allow http URLs', () => {
|
||||
const result = sanitizeURL('http://example.com');
|
||||
// URL.toString() adds trailing slash
|
||||
expect(result).toBe('http://example.com/');
|
||||
});
|
||||
|
||||
it('should allow https URLs', () => {
|
||||
const result = sanitizeURL('https://example.com');
|
||||
// URL.toString() adds trailing slash
|
||||
expect(result).toBe('https://example.com/');
|
||||
});
|
||||
|
||||
it('should reject javascript URLs', () => {
|
||||
const result = sanitizeURL('javascript:alert(1)');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject file URLs', () => {
|
||||
const result = sanitizeURL('file:///etc/passwd');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid URLs', () => {
|
||||
const result = sanitizeURL('not-a-url');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeEmail', () => {
|
||||
it('should validate and normalize valid email', () => {
|
||||
const result = sanitizeEmail('Test@Example.COM');
|
||||
expect(result).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should return null for invalid email', () => {
|
||||
expect(sanitizeEmail('not-an-email')).toBeNull();
|
||||
expect(sanitizeEmail('test@')).toBeNull();
|
||||
expect(sanitizeEmail('@example.com')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePassword', () => {
|
||||
it('should validate strong password', () => {
|
||||
const result = validatePassword('StrongP@ssw0rd123');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should reject short password', () => {
|
||||
const result = validatePassword('Short1!');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should reject password without uppercase', () => {
|
||||
const result = validatePassword('lowercase123!');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('majuscule'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject password without lowercase', () => {
|
||||
const result = validatePassword('UPPERCASE123!');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('minuscule'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject password without number', () => {
|
||||
const result = validatePassword('NoNumber!');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('chiffre'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject password without special character', () => {
|
||||
const result = validatePassword('NoSpecial123');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('spécial'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject common weak patterns', () => {
|
||||
const result = validatePassword('Password123!');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('commun'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
211
apps/web/src/utils/serviceErrorHandler.test.ts
Normal file
211
apps/web/src/utils/serviceErrorHandler.test.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Tests for Service Error Handler Utility
|
||||
* FE-TEST-004: Test service error handler utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
handleServiceError,
|
||||
withErrorHandling,
|
||||
getServiceValidationErrors,
|
||||
isErrorStatus,
|
||||
isNetworkError,
|
||||
getUserFriendlyMessage,
|
||||
handleApiServiceError,
|
||||
} from './serviceErrorHandler';
|
||||
import type { ApiError } from '@/types/api';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./apiErrorHandler', () => ({
|
||||
parseApiError: vi.fn((error: unknown): ApiError => {
|
||||
if (error && typeof error === 'object' && 'code' in error) {
|
||||
return error as ApiError;
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
message: 'Unknown error',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}),
|
||||
formatErrorMessage: vi.fn((error: ApiError) => error.message),
|
||||
getValidationErrors: vi.fn((error: ApiError) => {
|
||||
if (error.details) {
|
||||
const errors: Record<string, string> = {};
|
||||
error.details.forEach((detail: any) => {
|
||||
if (detail.field && detail.message) {
|
||||
errors[detail.field] = detail.message;
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('./errorMessages', () => ({
|
||||
formatUserFriendlyError: vi.fn((error: ApiError) => error.message),
|
||||
isRetryableError: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
describe('serviceErrorHandler utilities', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handleServiceError', () => {
|
||||
it('should throw error by default', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(() => handleServiceError(error)).toThrow();
|
||||
});
|
||||
|
||||
it('should return message when throwError is false', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = handleServiceError(error, { throwError: false });
|
||||
expect(result).toBe('Not found');
|
||||
});
|
||||
|
||||
it('should use custom message override', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(() => {
|
||||
handleServiceError(error, {
|
||||
customMessages: { 404: 'Custom not found' },
|
||||
});
|
||||
}).toThrow('Custom not found');
|
||||
});
|
||||
|
||||
it('should include context in error', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
try {
|
||||
handleServiceError(error, { context: 'playlist' });
|
||||
} catch (e: any) {
|
||||
expect(e.apiError).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('withErrorHandling', () => {
|
||||
it('should return result on success', async () => {
|
||||
const apiCall = vi.fn().mockResolvedValue('success');
|
||||
const result = await withErrorHandling(apiCall);
|
||||
expect(result).toBe('success');
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const apiCall = vi.fn().mockRejectedValue(error);
|
||||
await expect(withErrorHandling(apiCall)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServiceValidationErrors', () => {
|
||||
it('should extract validation errors', () => {
|
||||
const error: ApiError = {
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
details: [
|
||||
{ field: 'email', message: 'Invalid email' },
|
||||
{ field: 'password', message: 'Too short' },
|
||||
],
|
||||
};
|
||||
const result = getServiceValidationErrors(error);
|
||||
expect(result.email).toBe('Invalid email');
|
||||
expect(result.password).toBe('Too short');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isErrorStatus', () => {
|
||||
it('should return true for matching status', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(isErrorStatus(error, 404)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-matching status', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(isErrorStatus(error, 500)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNetworkError', () => {
|
||||
it('should return true for AxiosError without response', () => {
|
||||
const error = {
|
||||
isAxiosError: true,
|
||||
request: {},
|
||||
response: undefined,
|
||||
} as unknown as AxiosError;
|
||||
expect(isNetworkError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for AxiosError with response', () => {
|
||||
const error = {
|
||||
isAxiosError: true,
|
||||
request: {},
|
||||
response: { status: 404 },
|
||||
} as unknown as AxiosError;
|
||||
expect(isNetworkError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFriendlyMessage', () => {
|
||||
it('should return user-friendly message', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = getUserFriendlyMessage(error);
|
||||
expect(result).toBe('Not found');
|
||||
});
|
||||
|
||||
it('should use context', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const result = getUserFriendlyMessage(error, 'playlist');
|
||||
expect(result).toBe('Not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleApiServiceError', () => {
|
||||
it('should always throw', () => {
|
||||
const error: ApiError = {
|
||||
code: 404,
|
||||
message: 'Not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
expect(() => handleApiServiceError(error)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
135
apps/web/src/utils/timeoutHandler.test.ts
Normal file
135
apps/web/src/utils/timeoutHandler.test.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* Tests for Timeout Handler Utility
|
||||
* FE-TEST-004: Test timeout handler utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
TIMEOUT_CONFIG,
|
||||
TIMEOUT_MESSAGES,
|
||||
createTimeoutPromise,
|
||||
withTimeout,
|
||||
getTimeoutForRequestType,
|
||||
isTimeoutError,
|
||||
getTimeoutMessage,
|
||||
withRequestTimeout,
|
||||
} from './timeoutHandler';
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock('react-hot-toast', () => ({
|
||||
default: {
|
||||
loading: vi.fn(() => 'toast-id'),
|
||||
dismiss: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('timeoutHandler utilities', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('TIMEOUT_CONFIG', () => {
|
||||
it('should have timeout configs', () => {
|
||||
expect(TIMEOUT_CONFIG.default).toBe(10000);
|
||||
expect(TIMEOUT_CONFIG.fast).toBe(5000);
|
||||
expect(TIMEOUT_CONFIG.slow).toBe(30000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTimeoutPromise', () => {
|
||||
it('should reject after timeout', async () => {
|
||||
const promise = createTimeoutPromise(1000, 'Timeout');
|
||||
vi.advanceTimersByTime(1000);
|
||||
await expect(promise).rejects.toThrow('Timeout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('withTimeout', () => {
|
||||
it('should resolve if promise completes before timeout', async () => {
|
||||
const promise = Promise.resolve('success');
|
||||
const result = withTimeout(promise, { timeout: 1000 });
|
||||
vi.advanceTimersByTime(500);
|
||||
await expect(result).resolves.toBe('success');
|
||||
});
|
||||
|
||||
it('should reject on timeout', async () => {
|
||||
const promise = new Promise((resolve) => {
|
||||
setTimeout(() => resolve('success'), 2000);
|
||||
});
|
||||
const result = withTimeout(promise, { timeout: 1000 });
|
||||
vi.advanceTimersByTime(1000);
|
||||
await expect(result).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should call onTimeout callback', async () => {
|
||||
const onTimeout = vi.fn();
|
||||
const promise = new Promise((resolve) => {
|
||||
setTimeout(() => resolve('success'), 2000);
|
||||
});
|
||||
const result = withTimeout(promise, { timeout: 1000, onTimeout });
|
||||
vi.advanceTimersByTime(1000);
|
||||
try {
|
||||
await result;
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
expect(onTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeoutForRequestType', () => {
|
||||
it('should return timeout for request type', () => {
|
||||
expect(getTimeoutForRequestType('fast')).toBe(5000);
|
||||
expect(getTimeoutForRequestType('normal')).toBe(10000);
|
||||
expect(getTimeoutForRequestType('slow')).toBe(30000);
|
||||
});
|
||||
|
||||
it('should default to normal', () => {
|
||||
expect(getTimeoutForRequestType()).toBe(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTimeoutError', () => {
|
||||
it('should return true for timeout error', () => {
|
||||
const error = new Error('Request timeout');
|
||||
expect(isTimeoutError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for timeout in message', () => {
|
||||
const error = new Error('Request expired');
|
||||
expect(isTimeoutError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for ECONNABORTED', () => {
|
||||
expect(isTimeoutError({ code: 'ECONNABORTED' })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other errors', () => {
|
||||
const error = new Error('Other error');
|
||||
expect(isTimeoutError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeoutMessage', () => {
|
||||
it('should return message for request type', () => {
|
||||
expect(getTimeoutMessage('fast')).toBeTruthy();
|
||||
expect(getTimeoutMessage('normal')).toBeTruthy();
|
||||
expect(getTimeoutMessage('slow')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('withRequestTimeout', () => {
|
||||
it('should wrap API call with timeout', async () => {
|
||||
const apiCall = vi.fn().mockResolvedValue('success');
|
||||
const result = withRequestTimeout(apiCall, 'normal');
|
||||
vi.advanceTimersByTime(500);
|
||||
await expect(result).resolves.toBe('success');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
299
apps/web/src/utils/url.test.ts
Normal file
299
apps/web/src/utils/url.test.ts
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
/**
|
||||
* Tests for URL Utilities
|
||||
* FE-TEST-004: Test all URL utility functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildUrl,
|
||||
parseQueryString,
|
||||
buildQueryString,
|
||||
getUrlPathname,
|
||||
getUrlHostname,
|
||||
isAbsoluteUrl,
|
||||
isRelativeUrl,
|
||||
normalizeUrl,
|
||||
addQueryParam,
|
||||
removeQueryParam,
|
||||
getQueryParam,
|
||||
hasQueryParam,
|
||||
extractDomain,
|
||||
extractProtocol,
|
||||
extractPort,
|
||||
isValidUrl,
|
||||
isSecureUrl,
|
||||
getUrlWithoutQuery,
|
||||
getUrlWithoutHash,
|
||||
getHashFromUrl,
|
||||
setHashInUrl,
|
||||
removeHashFromUrl,
|
||||
getBaseUrl,
|
||||
getRelativePath,
|
||||
isSameOrigin,
|
||||
getUrlSegments,
|
||||
getLastUrlSegment,
|
||||
getParentUrl,
|
||||
} from './url';
|
||||
|
||||
describe('url utilities', () => {
|
||||
describe('buildUrl', () => {
|
||||
it('should build URL with base and path', () => {
|
||||
const result = buildUrl('https://example.com', '/api/users');
|
||||
expect(result).toBe('https://example.com/api/users');
|
||||
});
|
||||
|
||||
it('should add query parameters', () => {
|
||||
const result = buildUrl('https://example.com', '/api/users', { page: 1, limit: 10 });
|
||||
expect(result).toContain('page=1');
|
||||
expect(result).toContain('limit=10');
|
||||
});
|
||||
|
||||
it('should ignore null and undefined params', () => {
|
||||
const result = buildUrl('https://example.com', '/api/users', { page: 1, limit: null });
|
||||
expect(result).toContain('page=1');
|
||||
expect(result).not.toContain('limit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseQueryString', () => {
|
||||
it('should parse query string', () => {
|
||||
const result = parseQueryString('?page=1&limit=10');
|
||||
expect(result.page).toBe('1');
|
||||
expect(result.limit).toBe('10');
|
||||
});
|
||||
|
||||
it('should return empty object for empty string', () => {
|
||||
const result = parseQueryString('');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQueryString', () => {
|
||||
it('should build query string from params', () => {
|
||||
const result = buildQueryString({ page: 1, limit: 10 });
|
||||
expect(result).toContain('page=1');
|
||||
expect(result).toContain('limit=10');
|
||||
});
|
||||
|
||||
it('should ignore null and undefined', () => {
|
||||
const result = buildQueryString({ page: 1, limit: null });
|
||||
expect(result).toContain('page=1');
|
||||
expect(result).not.toContain('limit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlPathname', () => {
|
||||
it('should extract pathname', () => {
|
||||
expect(getUrlPathname('https://example.com/api/users')).toBe('/api/users');
|
||||
});
|
||||
|
||||
it('should return original string if invalid URL', () => {
|
||||
expect(getUrlPathname('invalid-url')).toBe('invalid-url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlHostname', () => {
|
||||
it('should extract hostname', () => {
|
||||
expect(getUrlHostname('https://example.com/api/users')).toBe('example.com');
|
||||
});
|
||||
|
||||
it('should return empty string for invalid URL', () => {
|
||||
expect(getUrlHostname('invalid-url')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAbsoluteUrl', () => {
|
||||
it('should return true for absolute URLs', () => {
|
||||
expect(isAbsoluteUrl('https://example.com')).toBe(true);
|
||||
expect(isAbsoluteUrl('http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for relative URLs', () => {
|
||||
expect(isAbsoluteUrl('/api/users')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid URLs', () => {
|
||||
expect(isAbsoluteUrl('not-a-url')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRelativeUrl', () => {
|
||||
it('should return true for relative URLs', () => {
|
||||
expect(isRelativeUrl('/api/users')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for absolute URLs', () => {
|
||||
expect(isRelativeUrl('https://example.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeUrl', () => {
|
||||
it('should normalize valid URL', () => {
|
||||
const result = normalizeUrl('https://example.com/api');
|
||||
expect(result).toBe('https://example.com/api');
|
||||
});
|
||||
|
||||
it('should return original string if invalid', () => {
|
||||
expect(normalizeUrl('invalid-url')).toBe('invalid-url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addQueryParam', () => {
|
||||
it('should add query parameter', () => {
|
||||
const result = addQueryParam('https://example.com', 'page', '1');
|
||||
expect(result).toContain('page=1');
|
||||
});
|
||||
|
||||
it('should return original URL if invalid', () => {
|
||||
expect(addQueryParam('invalid-url', 'page', '1')).toBe('invalid-url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeQueryParam', () => {
|
||||
it('should remove query parameter', () => {
|
||||
const result = removeQueryParam('https://example.com?page=1&limit=10', 'page');
|
||||
expect(result).not.toContain('page=1');
|
||||
expect(result).toContain('limit=10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueryParam', () => {
|
||||
it('should get query parameter', () => {
|
||||
const result = getQueryParam('https://example.com?page=1', 'page');
|
||||
expect(result).toBe('1');
|
||||
});
|
||||
|
||||
it('should return null if param not found', () => {
|
||||
const result = getQueryParam('https://example.com?page=1', 'limit');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasQueryParam', () => {
|
||||
it('should return true if param exists', () => {
|
||||
expect(hasQueryParam('https://example.com?page=1', 'page')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if param does not exist', () => {
|
||||
expect(hasQueryParam('https://example.com?page=1', 'limit')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractDomain', () => {
|
||||
it('should extract domain', () => {
|
||||
expect(extractDomain('https://example.com/api')).toBe('example.com');
|
||||
});
|
||||
|
||||
it('should return empty string for invalid URL', () => {
|
||||
expect(extractDomain('invalid-url')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractProtocol', () => {
|
||||
it('should extract protocol', () => {
|
||||
expect(extractProtocol('https://example.com')).toBe('https:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPort', () => {
|
||||
it('should extract port', () => {
|
||||
expect(extractPort('https://example.com:8080')).toBe('8080');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidUrl', () => {
|
||||
it('should return true for valid URLs', () => {
|
||||
expect(isValidUrl('https://example.com')).toBe(true);
|
||||
expect(isValidUrl('http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid URLs', () => {
|
||||
expect(isValidUrl('not-a-url')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSecureUrl', () => {
|
||||
it('should return true for HTTPS', () => {
|
||||
expect(isSecureUrl('https://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for HTTP', () => {
|
||||
expect(isSecureUrl('http://example.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlWithoutQuery', () => {
|
||||
it('should remove query string', () => {
|
||||
expect(getUrlWithoutQuery('https://example.com?page=1')).toBe('https://example.com/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlWithoutHash', () => {
|
||||
it('should remove hash', () => {
|
||||
expect(getUrlWithoutHash('https://example.com#section')).toBe('https://example.com/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHashFromUrl', () => {
|
||||
it('should extract hash', () => {
|
||||
expect(getHashFromUrl('https://example.com#section')).toBe('#section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setHashInUrl', () => {
|
||||
it('should set hash', () => {
|
||||
const result = setHashInUrl('https://example.com', 'section');
|
||||
expect(result).toContain('#section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeHashFromUrl', () => {
|
||||
it('should remove hash', () => {
|
||||
const result = removeHashFromUrl('https://example.com#section');
|
||||
expect(result).not.toContain('#section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBaseUrl', () => {
|
||||
it('should get base URL', () => {
|
||||
expect(getBaseUrl('https://example.com/api/users')).toBe('https://example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelativePath', () => {
|
||||
it('should get relative path', () => {
|
||||
expect(getRelativePath('https://example.com/api/users')).toBe('/api/users');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameOrigin', () => {
|
||||
it('should return true for same origin', () => {
|
||||
expect(isSameOrigin('https://example.com/api', 'https://example.com/users')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different origins', () => {
|
||||
expect(isSameOrigin('https://example.com', 'https://other.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlSegments', () => {
|
||||
it('should get URL segments', () => {
|
||||
expect(getUrlSegments('https://example.com/api/users/123')).toEqual(['api', 'users', '123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastUrlSegment', () => {
|
||||
it('should get last segment', () => {
|
||||
expect(getLastUrlSegment('https://example.com/api/users/123')).toBe('123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParentUrl', () => {
|
||||
it('should get parent URL', () => {
|
||||
const result = getParentUrl('https://example.com/api/users/123');
|
||||
expect(result).toContain('/api/users');
|
||||
expect(result).not.toContain('/123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in a new issue