315 lines
8.4 KiB
TypeScript
315 lines
8.4 KiB
TypeScript
/**
|
|
* Test utilities and mock factories
|
|
* Following TDD best practices with reusable test data generators
|
|
*/
|
|
|
|
import { vi } from 'vitest';
|
|
import type { PostData } from '../../src/main/engine/PostEngine';
|
|
import type { MediaData } from '../../src/main/engine/MediaEngine';
|
|
import type { Task, TaskProgress } from '../../src/main/engine/TaskManager';
|
|
|
|
// ============================================
|
|
// Post Mock Factory
|
|
// ============================================
|
|
|
|
let postIdCounter = 1;
|
|
|
|
export function createMockPost(overrides?: Partial<PostData>): PostData {
|
|
const id = `post-${postIdCounter++}`;
|
|
const now = new Date();
|
|
|
|
return {
|
|
id,
|
|
projectId: 'default',
|
|
title: `Test Post ${id}`,
|
|
slug: `test-post-${id}`,
|
|
excerpt: 'This is a test excerpt',
|
|
content: '# Test Content\n\nThis is test content.',
|
|
status: 'draft',
|
|
author: 'Test Author',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
publishedAt: undefined,
|
|
tags: ['test', 'mock'],
|
|
categories: ['testing'],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function createMockPublishedPost(overrides?: Partial<PostData>): PostData {
|
|
const now = new Date();
|
|
return createMockPost({
|
|
status: 'published',
|
|
publishedAt: now,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Media Mock Factory
|
|
// ============================================
|
|
|
|
let mediaIdCounter = 1;
|
|
|
|
export function createMockMedia(overrides?: Partial<MediaData>): MediaData {
|
|
const id = `media-${mediaIdCounter++}`;
|
|
const now = new Date();
|
|
|
|
return {
|
|
id,
|
|
filename: `${id}.jpg`,
|
|
originalName: `original-${id}.jpg`,
|
|
mimeType: 'image/jpeg',
|
|
size: 1024 * 100, // 100KB
|
|
width: 800,
|
|
height: 600,
|
|
alt: 'Test image',
|
|
caption: 'A test image caption',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
tags: ['test', 'image'],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function createMockPdfMedia(overrides?: Partial<MediaData>): MediaData {
|
|
return createMockMedia({
|
|
filename: 'document.pdf',
|
|
originalName: 'document.pdf',
|
|
mimeType: 'application/pdf',
|
|
width: undefined,
|
|
height: undefined,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Task Mock Factory
|
|
// ============================================
|
|
|
|
let taskIdCounter = 1;
|
|
|
|
export function createMockTask<T = void>(
|
|
executor?: (onProgress: (progress: number, message: string) => void) => Promise<T>,
|
|
overrides?: Partial<Task<T>>
|
|
): Task<T> {
|
|
const id = `task-${taskIdCounter++}`;
|
|
|
|
return {
|
|
id,
|
|
name: `Test Task ${id}`,
|
|
execute: executor || (async (onProgress) => {
|
|
onProgress(50, 'Processing...');
|
|
onProgress(100, 'Done');
|
|
return undefined as T;
|
|
}),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function createMockSlowTask(durationMs: number): Task<void> {
|
|
return createMockTask(async (onProgress) => {
|
|
const steps = 10;
|
|
const stepDuration = durationMs / steps;
|
|
|
|
for (let i = 1; i <= steps; i++) {
|
|
await new Promise(resolve => setTimeout(resolve, stepDuration));
|
|
onProgress(i * 10, `Step ${i}/${steps}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function createMockFailingTask(errorMessage: string = 'Task failed'): Task<void> {
|
|
return createMockTask(async () => {
|
|
throw new Error(errorMessage);
|
|
});
|
|
}
|
|
|
|
export function createMockTaskProgress(overrides?: Partial<TaskProgress>): TaskProgress {
|
|
return {
|
|
taskId: `task-${taskIdCounter++}`,
|
|
status: 'pending',
|
|
progress: 0,
|
|
message: 'Waiting...',
|
|
startTime: new Date(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// Database Mock Utilities
|
|
// ============================================
|
|
|
|
export function createMockDatabase() {
|
|
const data = {
|
|
posts: new Map<string, PostData>(),
|
|
media: new Map<string, MediaData>(),
|
|
};
|
|
|
|
return {
|
|
data,
|
|
getLocal: vi.fn(() => ({
|
|
select: vi.fn(() => ({
|
|
from: vi.fn(() => ({
|
|
where: vi.fn(() => Promise.resolve([])),
|
|
orderBy: vi.fn(() => Promise.resolve([])),
|
|
})),
|
|
})),
|
|
insert: vi.fn(() => ({
|
|
values: vi.fn(() => Promise.resolve()),
|
|
})),
|
|
update: vi.fn(() => ({
|
|
set: vi.fn(() => ({
|
|
where: vi.fn(() => Promise.resolve()),
|
|
})),
|
|
})),
|
|
delete: vi.fn(() => ({
|
|
where: vi.fn(() => Promise.resolve()),
|
|
})),
|
|
})),
|
|
getRemote: vi.fn(() => null),
|
|
getDataPaths: vi.fn(() => ({
|
|
database: '/mock/userData/bds.db',
|
|
posts: '/mock/userData/posts',
|
|
media: '/mock/userData/media',
|
|
})),
|
|
initializeLocal: vi.fn(),
|
|
initializeRemote: vi.fn(),
|
|
close: vi.fn(),
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// File System Mock Utilities
|
|
// ============================================
|
|
|
|
export function createMockFileSystem() {
|
|
const files = new Map<string, string | Buffer>();
|
|
const directories = new Set<string>(['/mock', '/mock/userData', '/mock/userData/posts', '/mock/userData/media']);
|
|
|
|
return {
|
|
files,
|
|
directories,
|
|
|
|
readFile: vi.fn(async (path: string) => {
|
|
const content = files.get(path);
|
|
if (content === undefined) {
|
|
const error = new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
|
throw error;
|
|
}
|
|
return content;
|
|
}),
|
|
|
|
writeFile: vi.fn(async (path: string, content: string | Buffer) => {
|
|
files.set(path, content);
|
|
}),
|
|
|
|
unlink: vi.fn(async (path: string) => {
|
|
if (!files.has(path)) {
|
|
const error = new Error(`ENOENT: no such file or directory, unlink '${path}'`);
|
|
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
|
throw error;
|
|
}
|
|
files.delete(path);
|
|
}),
|
|
|
|
mkdir: vi.fn(async (path: string) => {
|
|
directories.add(path);
|
|
}),
|
|
|
|
readdir: vi.fn(async (path: string) => {
|
|
const entries: string[] = [];
|
|
for (const filePath of files.keys()) {
|
|
if (filePath.startsWith(path + '/')) {
|
|
const relativePath = filePath.slice(path.length + 1);
|
|
const firstSegment = relativePath.split('/')[0];
|
|
if (!entries.includes(firstSegment)) {
|
|
entries.push(firstSegment);
|
|
}
|
|
}
|
|
}
|
|
return entries;
|
|
}),
|
|
|
|
stat: vi.fn(async (path: string) => {
|
|
if (files.has(path)) {
|
|
const content = files.get(path)!;
|
|
return {
|
|
isFile: () => true,
|
|
isDirectory: () => false,
|
|
size: typeof content === 'string' ? content.length : content.length,
|
|
};
|
|
}
|
|
if (directories.has(path)) {
|
|
return {
|
|
isFile: () => false,
|
|
isDirectory: () => true,
|
|
size: 0,
|
|
};
|
|
}
|
|
const error = new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
|
throw error;
|
|
}),
|
|
|
|
copyFile: vi.fn(async (src: string, dest: string) => {
|
|
const content = files.get(src);
|
|
if (content === undefined) {
|
|
const error = new Error(`ENOENT: no such file or directory, copyfile '${src}'`);
|
|
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
|
throw error;
|
|
}
|
|
files.set(dest, content);
|
|
}),
|
|
|
|
access: vi.fn(async (path: string) => {
|
|
if (!files.has(path) && !directories.has(path)) {
|
|
const error = new Error(`ENOENT: no such file or directory, access '${path}'`);
|
|
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
|
throw error;
|
|
}
|
|
}),
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// Reset Utilities
|
|
// ============================================
|
|
|
|
export function resetMockCounters(): void {
|
|
postIdCounter = 1;
|
|
mediaIdCounter = 1;
|
|
taskIdCounter = 1;
|
|
}
|
|
|
|
// ============================================
|
|
// Assertion Helpers
|
|
// ============================================
|
|
|
|
export function expectPostToMatchPartial(
|
|
actual: PostData,
|
|
expected: Partial<PostData>
|
|
): void {
|
|
for (const [key, value] of Object.entries(expected)) {
|
|
if (value instanceof Date) {
|
|
expect((actual as Record<string, unknown>)[key]).toBeInstanceOf(Date);
|
|
} else {
|
|
expect((actual as Record<string, unknown>)[key]).toEqual(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function expectMediaToMatchPartial(
|
|
actual: MediaData,
|
|
expected: Partial<MediaData>
|
|
): void {
|
|
for (const [key, value] of Object.entries(expected)) {
|
|
if (value instanceof Date) {
|
|
expect((actual as Record<string, unknown>)[key]).toBeInstanceOf(Date);
|
|
} else {
|
|
expect((actual as Record<string, unknown>)[key]).toEqual(value);
|
|
}
|
|
}
|
|
}
|