feat(tests): integrate Vitest for testing framework and add test scripts

- Added Vitest as a dependency for improved testing capabilities.
- Updated package.json with new test scripts for running tests, watching, and coverage reporting.
- Configured ESLint to recognize test runner scripts and included them in the linting process.
- Modified tsconfig.json to include Vitest types for better TypeScript support in tests.
This commit is contained in:
Julien Froidefond
2025-11-21 10:40:30 +01:00
parent 31f9855a3c
commit 8bdd3a8253
10 changed files with 1216 additions and 2 deletions

View File

@@ -18,6 +18,8 @@ const eslintConfig = [
'out/**',
'build/**',
'next-env.d.ts',
'scripts/test-runner.js', // Script Node.js qui utilise require() légitimement
'scripts/generate-icons-from-jpg.ts', // Script utilitaire avec require()
],
},
];

View File

@@ -19,6 +19,10 @@
"cache:stats": "pnpm tsx scripts/cache-monitor.ts stats",
"cache:cleanup": "pnpm tsx scripts/cache-monitor.ts cleanup",
"cache:clear": "pnpm tsx scripts/cache-monitor.ts clear",
"test": "node scripts/test-runner.js",
"test:watch": "vitest --watch --reporter=verbose",
"test:coverage": "vitest --coverage --reporter=verbose",
"test:ui": "vitest --ui",
"test:story-points": "pnpm tsx scripts/test-story-points.ts",
"test:jira-fields": "pnpm tsx scripts/test-jira-fields.ts",
"prettier:format": "prettier --write .",
@@ -71,7 +75,8 @@
"sharp": "^0.34.5",
"tailwindcss": "^4.1.14",
"tsx": "^4.19.2",
"typescript": "^5"
"typescript": "^5",
"vitest": "^2.1.8"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,md}": [

766
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

62
scripts/test-runner.js Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Script pour gérer PostCSS pendant les tests
* Renomme temporairement postcss.config.mjs pour éviter les erreurs Vitest
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const postcssConfigPath = path.join(process.cwd(), 'postcss.config.mjs');
const postcssConfigBackupPath = path.join(
process.cwd(),
'postcss.config.mjs.testbak'
);
// Fonction pour restaurer le fichier
function restorePostCSS() {
if (fs.existsSync(postcssConfigBackupPath)) {
fs.renameSync(postcssConfigBackupPath, postcssConfigPath);
console.log('✓ PostCSS config restauré');
}
}
// Renommer le fichier PostCSS avant les tests
if (fs.existsSync(postcssConfigPath)) {
fs.renameSync(postcssConfigPath, postcssConfigBackupPath);
console.log('✓ PostCSS config temporairement désactivé pour les tests');
// Lancer Vitest avec reporter verbose pour plus de détails
const vitest = spawn('pnpm', ['vitest', '--run', '--reporter=verbose'], {
stdio: 'inherit',
});
// Restaurer le fichier après que Vitest ait terminé
vitest.on('close', (code) => {
restorePostCSS();
process.exit(code || 0);
});
// Gérer les signaux d'interruption
process.on('SIGINT', () => {
vitest.kill('SIGINT');
restorePostCSS();
process.exit(0);
});
process.on('SIGTERM', () => {
vitest.kill('SIGTERM');
restorePostCSS();
process.exit(0);
});
} else {
// Si le fichier n'existe pas, lancer Vitest directement
const vitest = spawn('pnpm', ['vitest', '--run'], {
stdio: 'inherit',
});
vitest.on('close', (code) => {
process.exit(code || 0);
});
}

View File

@@ -0,0 +1,9 @@
/**
* Configuration de setup pour les tests Vitest
* Désactive PostCSS pour éviter les erreurs de chargement
*/
// Désactiver PostCSS pendant les tests
process.env.VITE_DISABLE_POSTCSS = 'true';
// Note: NODE_ENV est géré automatiquement par Vitest

View File

@@ -0,0 +1,171 @@
/**
* Tests unitaires pour la synchronisation Jira
*/
import { describe, it, expect } from 'vitest';
import { buildSyncUpdateData } from '@/services/task-management/readonly-fields';
describe('Synchronisation Jira - Logique de préservation des champs', () => {
describe('buildSyncUpdateData pour Jira', () => {
it('devrait préserver title si modifié localement', () => {
const existingTask = {
title: 'Titre modifié localement',
description: 'Description locale',
status: 'todo',
priority: 'high',
dueDate: new Date('2024-01-01'),
};
const syncData = {
title: 'Titre original Jira',
description: 'Description Jira',
status: 'in_progress',
priority: 'medium',
dueDate: new Date('2024-01-02'),
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
expect(result.title).toBe('Titre modifié localement');
expect(result.description).toBe('Description Jira'); // Écrasé
expect(result.status).toBe('in_progress'); // Écrasé
expect(result.priority).toBe('high'); // Préservé
expect(result.dueDate).toEqual(new Date('2024-01-02')); // Écrasé
});
it('devrait préserver priority si modifié localement', () => {
const existingTask = {
title: 'Même titre',
description: 'Description locale',
status: 'todo',
priority: 'low', // Modifié localement
dueDate: new Date('2024-01-01'),
};
const syncData = {
title: 'Même titre',
description: 'Description Jira',
status: 'done',
priority: 'high', // Valeur Jira
dueDate: null,
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
expect(result.priority).toBe('low'); // Préservé car modifié localement
});
it('devrait préserver status archived même si Jira dit done', () => {
const existingTask = {
title: 'Tâche archivée',
description: 'Description',
status: 'archived', // Archivé localement
priority: 'medium',
dueDate: null,
};
const syncData = {
title: 'Tâche archivée',
description: 'Description Jira',
status: 'done', // Jira dit done
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
expect(result.status).toBe('archived'); // Préservé car archived
});
it('devrait écraser status si pas archived', () => {
const existingTask = {
title: 'Tâche',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const syncData = {
title: 'Tâche',
description: 'Description Jira',
status: 'in_progress', // Jira dit in_progress
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
expect(result.status).toBe('in_progress'); // Écrasé
});
it('devrait toujours écraser description', () => {
const existingTask = {
title: 'Tâche',
description: 'Description locale modifiée',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const syncData = {
title: 'Tâche',
description: 'Description Jira',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
expect(result.description).toBe('Description Jira'); // Toujours écrasé
});
it('devrait toujours écraser dueDate', () => {
const existingTask = {
title: 'Tâche',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: new Date('2024-01-01'), // Date locale
};
const syncData = {
title: 'Tâche',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: new Date('2024-12-31'), // Date Jira
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
expect(result.dueDate).toEqual(new Date('2024-12-31')); // Toujours écrasé
});
it('devrait préserver priority même si title identique', () => {
const existingTask = {
title: 'Titre identique',
description: 'Description locale',
status: 'todo',
priority: 'medium', // Modifié localement
dueDate: null,
};
const syncData = {
title: 'Titre identique', // Même titre
description: 'Description Jira',
status: 'done',
priority: 'high', // Valeur Jira différente
dueDate: new Date('2024-01-01'),
};
const result = buildSyncUpdateData('jira', existingTask, syncData);
// Title identique → utilise syncData
expect(result.title).toBe('Titre identique');
// Priority différente → préservée même si title identique
expect(result.priority).toBe('medium'); // Préservé car modifié localement
});
});
});

View File

@@ -0,0 +1,147 @@
/**
* Tests unitaires pour la synchronisation TFS
*/
import { describe, it, expect } from 'vitest';
import { buildSyncUpdateData } from '@/services/task-management/readonly-fields';
describe("Synchronisation TFS - Logique d'écrasement des champs", () => {
describe('buildSyncUpdateData pour TFS', () => {
it('devrait écraser tous les champs même si modifiés localement', () => {
const existingTask = {
title: 'Titre modifié localement',
description: 'Description locale modifiée',
status: 'todo',
priority: 'high',
dueDate: new Date('2024-01-01'),
};
const syncData = {
title: 'PR: Nouvelle Pull Request',
description: 'Description TFS',
status: 'done',
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('tfs', existingTask, syncData);
// Tous les champs doivent être écrasés
expect(result.title).toBe('PR: Nouvelle Pull Request');
expect(result.description).toBe('Description TFS');
expect(result.status).toBe('done');
expect(result.priority).toBe('medium');
expect(result.dueDate).toBeNull();
});
it('devrait écraser title même si différent', () => {
const existingTask = {
title: 'Ancien titre',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const syncData = {
title: 'PR: Nouveau titre',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('tfs', existingTask, syncData);
expect(result.title).toBe('PR: Nouveau titre'); // Écrasé
});
it('devrait écraser priority même si modifié localement', () => {
const existingTask = {
title: 'PR: Tâche',
description: 'Description',
status: 'todo',
priority: 'low', // Modifié localement
dueDate: null,
};
const syncData = {
title: 'PR: Tâche',
description: 'Description',
status: 'todo',
priority: 'high', // Valeur TFS
dueDate: null,
};
const result = buildSyncUpdateData('tfs', existingTask, syncData);
expect(result.priority).toBe('high'); // Écrasé
});
it('devrait écraser status même si archived localement', () => {
const existingTask = {
title: 'PR: Tâche',
description: 'Description',
status: 'archived', // Archivé localement
priority: 'medium',
dueDate: null,
};
const syncData = {
title: 'PR: Tâche',
description: 'Description',
status: 'done', // TFS dit done
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('tfs', existingTask, syncData);
expect(result.status).toBe('done'); // Écrasé même si archived
});
it('devrait écraser description même si modifiée localement', () => {
const existingTask = {
title: 'PR: Tâche',
description: 'Description locale modifiée',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const syncData = {
title: 'PR: Tâche',
description: 'Description TFS',
status: 'todo',
priority: 'medium',
dueDate: null,
};
const result = buildSyncUpdateData('tfs', existingTask, syncData);
expect(result.description).toBe('Description TFS'); // Écrasé
});
it('devrait écraser dueDate même si défini localement', () => {
const existingTask = {
title: 'PR: Tâche',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: new Date('2024-01-01'), // Date locale
};
const syncData = {
title: 'PR: Tâche',
description: 'Description',
status: 'todo',
priority: 'medium',
dueDate: null, // TFS n'a pas de dueDate
};
const result = buildSyncUpdateData('tfs', existingTask, syncData);
expect(result.dueDate).toBeNull(); // Écrasé
});
});
});

View File

@@ -13,6 +13,7 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": ["vitest/globals"],
"plugins": [
{
"name": "next"
@@ -22,6 +23,12 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"vitest.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}

44
vitest.config.ts Normal file
View File

@@ -0,0 +1,44 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
// Solution: créer un mock de postcss.config.mjs pour les tests
// En renommant temporairement le fichier ou en créant une config vide
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/__tests__/**/*.test.ts'],
setupFiles: ['./src/services/__tests__/setup.ts'],
// Afficher plus de détails dans la sortie
reporters: ['verbose'],
// Afficher les tests qui passent aussi
silent: false,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/**/__tests__/**',
'**/*.config.*',
'**/dist/**',
],
// Afficher les fichiers non couverts
reportOnFailure: true,
// Afficher plus de détails
all: false,
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
// Désactiver le traitement CSS pour éviter PostCSS
// Le fichier postcss.config.mjs est renommé par le script test-runner.js
css: {
modules: {
localsConvention: 'camelCase',
},
},
});

1
vitest.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vitest/globals" />