initial commit

This commit is contained in:
2026-02-10 11:04:44 +01:00
commit 5979fa3374
57 changed files with 19344 additions and 0 deletions

313
tests/utils/factories.ts Normal file
View File

@@ -0,0 +1,313 @@
/**
* 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,
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);
}
}
}

6
tests/utils/index.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* Test utilities index
* Re-exports all test utilities for convenient importing
*/
export * from './factories';