Migrate tests from Jest to Vitest, updating setup and test files accordingly.

- Replace Jest imports and mocks with Vitest equivalents in setup and unit tests.
- Adjust test cases to use async imports and reset modules with Vitest.
- Add Vitest configuration file for test environment setup and coverage reporting.
This commit is contained in:
Max Kowalski
2025-06-27 19:14:05 +02:00
parent 5c1ced5900
commit 49a75f5ede
7 changed files with 1094 additions and 2230 deletions

View File

@ -1,22 +0,0 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'lib/**/*.ts',
'!lib/**/*.d.ts',
'!lib/**/*.test.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/$1',
},
testTimeout: 10000,
};

View File

@ -18,9 +18,9 @@
"prisma:push:force": "prisma db push --force-reset", "prisma:push:force": "prisma db push --force-reset",
"prisma:studio": "prisma studio", "prisma:studio": "prisma studio",
"start": "node server.mjs", "start": "node server.mjs",
"test": "jest", "test": "vitest run",
"test:watch": "jest --watch", "test:watch": "vitest",
"test:coverage": "jest --coverage", "test:coverage": "vitest run --coverage",
"lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"", "lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"" "lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\""
}, },
@ -57,31 +57,33 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0", "@eslint/js": "^9.27.0",
"@jest/globals": "^30.0.3",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@tailwindcss/postcss": "^4.1.7", "@tailwindcss/postcss": "^4.1.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/jest": "^30.0.0",
"@types/node": "^22.15.21", "@types/node": "^22.15.21",
"@types/node-cron": "^3.0.8", "@types/node-cron": "^3.0.8",
"@types/react": "^19.1.5", "@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.27.0", "eslint": "^9.27.0",
"eslint-config-next": "^15.3.2", "eslint-config-next": "^15.3.2",
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.4.0",
"jest": "^30.0.3", "jsdom": "^26.1.0",
"markdownlint-cli2": "^0.18.1", "markdownlint-cli2": "^0.18.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-jinja-template": "^2.1.0", "prettier-plugin-jinja-template": "^2.1.0",
"prisma": "^6.10.1", "prisma": "^6.10.1",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5.0.0" "typescript": "^5.0.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
}, },
"prettier": { "prettier": {
"bracketSpacing": true, "bracketSpacing": true,

3131
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,19 @@
// Jest test setup // Vitest test setup
import { jest } from '@jest/globals'; import { vi } from 'vitest';
// Mock console methods to reduce noise in tests // Mock console methods to reduce noise in tests
global.console = { global.console = {
...console, ...console,
log: jest.fn(), log: vi.fn(),
warn: jest.fn(), warn: vi.fn(),
error: jest.fn(), error: vi.fn(),
}; };
// Set test environment variables // Set test environment variables
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'test',
writable: true,
configurable: true,
});
process.env.NEXTAUTH_SECRET = 'test-secret'; process.env.NEXTAUTH_SECRET = 'test-secret';
process.env.NEXTAUTH_URL = 'http://localhost:3000'; process.env.NEXTAUTH_URL = 'http://localhost:3000';
// Mock node-fetch for transcript fetcher tests // Mock node-fetch for transcript fetcher tests
jest.mock('node-fetch', () => ({ vi.mock('node-fetch', () => ({
__esModule: true, default: vi.fn(),
default: jest.fn(),
})); }));

View File

@ -1,6 +1,5 @@
// Unit tests for environment management // Unit tests for environment management
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { env, validateEnv, getSchedulerConfig } from '../../lib/env';
describe('Environment Management', () => { describe('Environment Management', () => {
let originalEnv: NodeJS.ProcessEnv; let originalEnv: NodeJS.ProcessEnv;
@ -13,54 +12,55 @@ describe('Environment Management', () => {
afterEach(() => { afterEach(() => {
// Restore original environment // Restore original environment
process.env = originalEnv; process.env = originalEnv;
vi.resetModules();
}); });
describe('env object', () => { describe('env object', () => {
it('should have default values when environment variables are not set', () => { it('should have default values when environment variables are not set', async () => {
// Clear relevant env vars // Clear relevant env vars
delete process.env.NEXTAUTH_URL; delete process.env.NEXTAUTH_URL;
delete process.env.SCHEDULER_ENABLED; delete process.env.SCHEDULER_ENABLED;
delete process.env.PORT; delete process.env.PORT;
// Re-import to get fresh env object // Re-import to get fresh env object
jest.resetModules(); vi.resetModules();
const { env: freshEnv } = require('../../lib/env'); const { env: freshEnv } = await import('../../lib/env');
expect(freshEnv.NEXTAUTH_URL).toBe('http://localhost:3000'); expect(freshEnv.NEXTAUTH_URL).toBe('http://localhost:3000');
expect(freshEnv.SCHEDULER_ENABLED).toBe(false); expect(freshEnv.SCHEDULER_ENABLED).toBe(false);
expect(freshEnv.PORT).toBe(3000); expect(freshEnv.PORT).toBe(3000);
}); });
it('should use environment variables when set', () => { it('should use environment variables when set', async () => {
process.env.NEXTAUTH_URL = 'https://example.com'; process.env.NEXTAUTH_URL = 'https://example.com';
process.env.SCHEDULER_ENABLED = 'true'; process.env.SCHEDULER_ENABLED = 'true';
process.env.PORT = '8080'; process.env.PORT = '8080';
jest.resetModules(); vi.resetModules();
const { env: freshEnv } = require('../../lib/env'); const { env: freshEnv } = await import('../../lib/env');
expect(freshEnv.NEXTAUTH_URL).toBe('https://example.com'); expect(freshEnv.NEXTAUTH_URL).toBe('https://example.com');
expect(freshEnv.SCHEDULER_ENABLED).toBe(true); expect(freshEnv.SCHEDULER_ENABLED).toBe(true);
expect(freshEnv.PORT).toBe(8080); expect(freshEnv.PORT).toBe(8080);
}); });
it('should parse numeric environment variables correctly', () => { it('should parse numeric environment variables correctly', async () => {
process.env.IMPORT_PROCESSING_BATCH_SIZE = '100'; process.env.IMPORT_PROCESSING_BATCH_SIZE = '100';
process.env.SESSION_PROCESSING_CONCURRENCY = '10'; process.env.SESSION_PROCESSING_CONCURRENCY = '10';
jest.resetModules(); vi.resetModules();
const { env: freshEnv } = require('../../lib/env'); const { env: freshEnv } = await import('../../lib/env');
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(100); expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(100);
expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(10); expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(10);
}); });
it('should handle invalid numeric values gracefully', () => { it('should handle invalid numeric values gracefully', async () => {
process.env.IMPORT_PROCESSING_BATCH_SIZE = 'invalid'; process.env.IMPORT_PROCESSING_BATCH_SIZE = 'invalid';
process.env.SESSION_PROCESSING_CONCURRENCY = ''; process.env.SESSION_PROCESSING_CONCURRENCY = '';
jest.resetModules(); vi.resetModules();
const { env: freshEnv } = require('../../lib/env'); const { env: freshEnv } = await import('../../lib/env');
expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(50); // default expect(freshEnv.IMPORT_PROCESSING_BATCH_SIZE).toBe(50); // default
expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(5); // default expect(freshEnv.SESSION_PROCESSING_CONCURRENCY).toBe(5); // default
@ -68,62 +68,50 @@ describe('Environment Management', () => {
}); });
describe('validateEnv', () => { describe('validateEnv', () => {
it('should return valid when all required variables are set', () => { it('should return valid when all required variables are set', async () => {
process.env.NEXTAUTH_SECRET = 'test-secret'; vi.stubEnv('NEXTAUTH_SECRET', 'test-secret');
process.env.OPENAI_API_KEY = 'test-key'; vi.stubEnv('OPENAI_API_KEY', 'test-key');
Object.defineProperty(process.env, 'NODE_ENV', { vi.stubEnv('NODE_ENV', 'production');
value: 'production',
writable: true,
configurable: true,
});
jest.resetModules(); vi.resetModules();
const { validateEnv: freshValidateEnv } = require('../../lib/env'); const { validateEnv: freshValidateEnv } = await import('../../lib/env');
const result = freshValidateEnv(); const result = freshValidateEnv();
expect(result.valid).toBe(true); expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0); expect(result.errors).toHaveLength(0);
}); });
it('should return invalid when NEXTAUTH_SECRET is missing', () => { it('should return invalid when NEXTAUTH_SECRET is missing', async () => {
delete process.env.NEXTAUTH_SECRET; vi.stubEnv('NEXTAUTH_SECRET', '');
jest.resetModules(); vi.resetModules();
const { validateEnv: freshValidateEnv } = require('../../lib/env'); const { validateEnv: freshValidateEnv } = await import('../../lib/env');
const result = freshValidateEnv(); const result = freshValidateEnv();
expect(result.valid).toBe(false); expect(result.valid).toBe(false);
expect(result.errors).toContain('NEXTAUTH_SECRET is required'); expect(result.errors).toContain('NEXTAUTH_SECRET is required');
}); });
it('should require OPENAI_API_KEY in production', () => { it('should require OPENAI_API_KEY in production', async () => {
process.env.NEXTAUTH_SECRET = 'test-secret'; vi.stubEnv('NEXTAUTH_SECRET', 'test-secret');
delete process.env.OPENAI_API_KEY; vi.stubEnv('OPENAI_API_KEY', '');
Object.defineProperty(process.env, 'NODE_ENV', { vi.stubEnv('NODE_ENV', 'production');
value: 'production',
writable: true,
configurable: true,
});
jest.resetModules(); vi.resetModules();
const { validateEnv: freshValidateEnv } = require('../../lib/env'); const { validateEnv: freshValidateEnv } = await import('../../lib/env');
const result = freshValidateEnv(); const result = freshValidateEnv();
expect(result.valid).toBe(false); expect(result.valid).toBe(false);
expect(result.errors).toContain('OPENAI_API_KEY is required in production'); expect(result.errors).toContain('OPENAI_API_KEY is required in production');
}); });
it('should not require OPENAI_API_KEY in development', () => { it('should not require OPENAI_API_KEY in development', async () => {
process.env.NEXTAUTH_SECRET = 'test-secret'; vi.stubEnv('NEXTAUTH_SECRET', 'test-secret');
delete process.env.OPENAI_API_KEY; vi.stubEnv('OPENAI_API_KEY', '');
Object.defineProperty(process.env, 'NODE_ENV', { vi.stubEnv('NODE_ENV', 'development');
value: 'development',
writable: true,
configurable: true,
});
jest.resetModules(); vi.resetModules();
const { validateEnv: freshValidateEnv } = require('../../lib/env'); const { validateEnv: freshValidateEnv } = await import('../../lib/env');
const result = freshValidateEnv(); const result = freshValidateEnv();
expect(result.valid).toBe(true); expect(result.valid).toBe(true);
@ -132,7 +120,7 @@ describe('Environment Management', () => {
}); });
describe('getSchedulerConfig', () => { describe('getSchedulerConfig', () => {
it('should return correct scheduler configuration', () => { it('should return correct scheduler configuration', async () => {
process.env.SCHEDULER_ENABLED = 'true'; process.env.SCHEDULER_ENABLED = 'true';
process.env.CSV_IMPORT_INTERVAL = '*/10 * * * *'; process.env.CSV_IMPORT_INTERVAL = '*/10 * * * *';
process.env.IMPORT_PROCESSING_INTERVAL = '*/3 * * * *'; process.env.IMPORT_PROCESSING_INTERVAL = '*/3 * * * *';
@ -141,8 +129,8 @@ describe('Environment Management', () => {
process.env.SESSION_PROCESSING_BATCH_SIZE = '100'; process.env.SESSION_PROCESSING_BATCH_SIZE = '100';
process.env.SESSION_PROCESSING_CONCURRENCY = '8'; process.env.SESSION_PROCESSING_CONCURRENCY = '8';
jest.resetModules(); vi.resetModules();
const { getSchedulerConfig: freshGetSchedulerConfig } = require('../../lib/env'); const { getSchedulerConfig: freshGetSchedulerConfig } = await import('../../lib/env');
const config = freshGetSchedulerConfig(); const config = freshGetSchedulerConfig();
@ -155,13 +143,13 @@ describe('Environment Management', () => {
expect(config.sessionProcessing.concurrency).toBe(8); expect(config.sessionProcessing.concurrency).toBe(8);
}); });
it('should use defaults when environment variables are not set', () => { it('should use defaults when environment variables are not set', async () => {
delete process.env.SCHEDULER_ENABLED; delete process.env.SCHEDULER_ENABLED;
delete process.env.CSV_IMPORT_INTERVAL; delete process.env.CSV_IMPORT_INTERVAL;
delete process.env.IMPORT_PROCESSING_INTERVAL; delete process.env.IMPORT_PROCESSING_INTERVAL;
jest.resetModules(); vi.resetModules();
const { getSchedulerConfig: freshGetSchedulerConfig } = require('../../lib/env'); const { getSchedulerConfig: freshGetSchedulerConfig } = await import('../../lib/env');
const config = freshGetSchedulerConfig(); const config = freshGetSchedulerConfig();

View File

@ -1,5 +1,5 @@
// Unit tests for transcript fetcher // Unit tests for transcript fetcher
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { describe, it, expect, beforeEach, vi } from 'vitest';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { import {
fetchTranscriptContent, fetchTranscriptContent,
@ -8,18 +8,18 @@ import {
} from '../../lib/transcriptFetcher'; } from '../../lib/transcriptFetcher';
// Mock node-fetch // Mock node-fetch
const mockFetch = fetch as jest.MockedFunction<typeof fetch>; const mockFetch = fetch as any;
describe('Transcript Fetcher', () => { describe('Transcript Fetcher', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
}); });
describe('fetchTranscriptContent', () => { describe('fetchTranscriptContent', () => {
it('should successfully fetch transcript content', async () => { it('should successfully fetch transcript content', async () => {
const mockResponse = { const mockResponse = {
ok: true, ok: true,
text: jest.fn().mockResolvedValue('Session transcript content'), text: vi.fn().mockResolvedValue('Session transcript content'),
}; };
mockFetch.mockResolvedValue(mockResponse as any); mockFetch.mockResolvedValue(mockResponse as any);
@ -40,7 +40,7 @@ describe('Transcript Fetcher', () => {
it('should handle authentication with username and password', async () => { it('should handle authentication with username and password', async () => {
const mockResponse = { const mockResponse = {
ok: true, ok: true,
text: jest.fn().mockResolvedValue('Authenticated transcript'), text: vi.fn().mockResolvedValue('Authenticated transcript'),
}; };
mockFetch.mockResolvedValue(mockResponse as any); mockFetch.mockResolvedValue(mockResponse as any);
@ -82,7 +82,7 @@ describe('Transcript Fetcher', () => {
it('should handle empty transcript content', async () => { it('should handle empty transcript content', async () => {
const mockResponse = { const mockResponse = {
ok: true, ok: true,
text: jest.fn().mockResolvedValue(' '), text: vi.fn().mockResolvedValue(' '),
}; };
mockFetch.mockResolvedValue(mockResponse as any); mockFetch.mockResolvedValue(mockResponse as any);
@ -135,7 +135,7 @@ describe('Transcript Fetcher', () => {
it('should trim whitespace from content', async () => { it('should trim whitespace from content', async () => {
const mockResponse = { const mockResponse = {
ok: true, ok: true,
text: jest.fn().mockResolvedValue(' \n Session content \n '), text: vi.fn().mockResolvedValue(' \n Session content \n '),
}; };
mockFetch.mockResolvedValue(mockResponse as any); mockFetch.mockResolvedValue(mockResponse as any);

23
vitest.config.mts Normal file
View File

@ -0,0 +1,23 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: 'node',
globals: true,
setupFiles: ['./tests/setup.ts'],
include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
env: {
NODE_ENV: 'test',
},
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
include: ['lib/**/*.ts'],
exclude: ['lib/**/*.d.ts', 'lib/**/*.test.ts'],
},
testTimeout: 10000,
},
})