325 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| #!/usr/bin/env node
 | |
| 
 | |
| /**
 | |
|  * Unit tests for front-matter enrichment
 | |
|  */
 | |
| 
 | |
| import { promises as fs } from 'fs';
 | |
| import path from 'path';
 | |
| import { fileURLToPath } from 'url';
 | |
| import { enrichFrontmatterOnOpen, extractFrontmatter } from './ensureFrontmatter.mjs';
 | |
| 
 | |
| const __filename = fileURLToPath(import.meta.url);
 | |
| const __dirname = path.dirname(__filename);
 | |
| 
 | |
| const TEST_DIR = path.join(__dirname, '..', 'test-vault-frontmatter');
 | |
| 
 | |
| // Test utilities
 | |
| async function setupTestDir() {
 | |
|   await fs.mkdir(TEST_DIR, { recursive: true });
 | |
| }
 | |
| 
 | |
| async function cleanupTestDir() {
 | |
|   try {
 | |
|     await fs.rm(TEST_DIR, { recursive: true, force: true });
 | |
|   } catch (err) {
 | |
|     console.warn('Cleanup warning:', err.message);
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function createTestFile(filename, content) {
 | |
|   const filePath = path.join(TEST_DIR, filename);
 | |
|   await fs.writeFile(filePath, content, 'utf-8');
 | |
|   return filePath;
 | |
| }
 | |
| 
 | |
| // Test runner
 | |
| const tests = [];
 | |
| function test(name, fn) {
 | |
|   tests.push({ name, fn });
 | |
| }
 | |
| 
 | |
| // Tests
 | |
| test('Should add front-matter to file without any', async () => {
 | |
|   const filePath = await createTestFile('no-frontmatter.md', '# Hello World\n\nThis is content.');
 | |
|   
 | |
|   const result = await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   if (!result.modified) {
 | |
|     throw new Error('Expected file to be modified');
 | |
|   }
 | |
|   
 | |
|   const content = await fs.readFile(filePath, 'utf-8');
 | |
|   if (!content.startsWith('---\n')) {
 | |
|     throw new Error('Expected front-matter to be added');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('titre: no-frontmatter')) {
 | |
|     throw new Error('Expected titre to be set to filename');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('auteur: Bruno Charest')) {
 | |
|     throw new Error('Expected auteur to be set');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('favoris: false')) {
 | |
|     throw new Error('Expected favoris to be false');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('template: false')) {
 | |
|     throw new Error('Expected template to be false');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('task: false')) {
 | |
|     throw new Error('Expected task to be false');
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Front-matter added successfully');
 | |
| });
 | |
| 
 | |
| test('Should be idempotent (no changes on second run)', async () => {
 | |
|   const filePath = await createTestFile('idempotent.md', '# Test\n\nContent here.');
 | |
|   
 | |
|   // First enrichment
 | |
|   const result1 = await enrichFrontmatterOnOpen(filePath);
 | |
|   if (!result1.modified) {
 | |
|     throw new Error('Expected first enrichment to modify file');
 | |
|   }
 | |
|   
 | |
|   const content1 = await fs.readFile(filePath, 'utf-8');
 | |
|   
 | |
|   // Wait a bit to ensure timestamp would differ if modified
 | |
|   await new Promise(resolve => setTimeout(resolve, 100));
 | |
|   
 | |
|   // Second enrichment
 | |
|   const result2 = await enrichFrontmatterOnOpen(filePath);
 | |
|   if (result2.modified) {
 | |
|     throw new Error('Expected second enrichment to NOT modify file (idempotent)');
 | |
|   }
 | |
|   
 | |
|   const content2 = await fs.readFile(filePath, 'utf-8');
 | |
|   
 | |
|   if (content1 !== content2) {
 | |
|     throw new Error('Expected content to be identical after second enrichment');
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Idempotence verified');
 | |
| });
 | |
| 
 | |
| test('Should preserve existing properties', async () => {
 | |
|   const initialContent = `---
 | |
| titre: Custom Title
 | |
| auteur: Bruno Charest
 | |
| custom_field: custom value
 | |
| favoris: true
 | |
| ---
 | |
| 
 | |
| # Content`;
 | |
|   
 | |
|   const filePath = await createTestFile('preserve.md', initialContent);
 | |
|   
 | |
|   const result = await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   const content = await fs.readFile(filePath, 'utf-8');
 | |
|   
 | |
|   if (!content.includes('custom_field: custom value')) {
 | |
|     throw new Error('Expected custom field to be preserved');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('favoris: true')) {
 | |
|     throw new Error('Expected favoris: true to be preserved');
 | |
|   }
 | |
|   
 | |
|   if (!content.includes('titre: Custom Title')) {
 | |
|     throw new Error('Expected custom title to be preserved');
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Existing properties preserved');
 | |
| });
 | |
| 
 | |
| test('Should maintain correct key order', async () => {
 | |
|   const filePath = await createTestFile('order.md', '# Test\n\nContent.');
 | |
|   
 | |
|   await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   const content = await fs.readFile(filePath, 'utf-8');
 | |
|   
 | |
|   // Extract front-matter
 | |
|   const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
 | |
|   if (!fmMatch) {
 | |
|     throw new Error('No front-matter found');
 | |
|   }
 | |
|   
 | |
|   const lines = fmMatch[1].split('\n').filter(l => l.trim());
 | |
|   
 | |
|   // Check order of required keys
 | |
|   const expectedOrder = [
 | |
|     'titre:',
 | |
|     'auteur:',
 | |
|     'creation_date:',
 | |
|     'modification_date:',
 | |
|     'catégorie:',
 | |
|     'tags:',
 | |
|     'aliases:',
 | |
|     'status:',
 | |
|     'favoris:',
 | |
|     'publish:',    
 | |
|     'draft:',
 | |
|     'template:',
 | |
|     'task:',    
 | |
|     'private:',
 | |
|     'archive:'
 | |
|   ];
 | |
|   
 | |
|   let lastIndex = -1;
 | |
|   for (const key of expectedOrder) {
 | |
|     const currentIndex = lines.findIndex(l => l.startsWith(key));
 | |
|     if (currentIndex === -1) {
 | |
|       throw new Error(`Expected key ${key} not found`);
 | |
|     }
 | |
|     if (currentIndex < lastIndex) {
 | |
|       throw new Error(`Key order incorrect: ${key} appears before previous key`);
 | |
|     }
 | |
|     lastIndex = currentIndex;
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Key order is correct');
 | |
| });
 | |
| 
 | |
| test('Should have no blank lines in front-matter', async () => {
 | |
|   const filePath = await createTestFile('no-blanks.md', '# Test\n\nContent.');
 | |
|   
 | |
|   await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   const content = await fs.readFile(filePath, 'utf-8');
 | |
|   
 | |
|   const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
 | |
|   if (!fmMatch) {
 | |
|     throw new Error('No front-matter found');
 | |
|   }
 | |
|   
 | |
|   const fmContent = fmMatch[1];
 | |
|   if (fmContent.includes('\n\n')) {
 | |
|     throw new Error('Found blank lines in front-matter');
 | |
|   }
 | |
|   
 | |
|   console.log('✓ No blank lines in front-matter');
 | |
| });
 | |
| 
 | |
| test('Should use correct boolean types', async () => {
 | |
|   const filePath = await createTestFile('booleans.md', '# Test\n\nContent.');
 | |
|   
 | |
|   await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   const fm = await extractFrontmatter(filePath);
 | |
|   
 | |
|   if (fm.favoris !== false) {
 | |
|     throw new Error(`Expected favoris to be boolean false, got ${typeof fm.favoris}: ${fm.favoris}`);
 | |
|   }
 | |
|   
 | |
|   if (fm.template !== false) {
 | |
|     throw new Error(`Expected template to be boolean false, got ${typeof fm.template}: ${fm.template}`);
 | |
|   }
 | |
|   
 | |
|   if (fm.task !== false) {
 | |
|     throw new Error(`Expected task to be boolean false, got ${typeof fm.task}: ${fm.task}`);
 | |
|   }
 | |
|   
 | |
|   if (fm.publish !== false) {
 | |
|     throw new Error(`Expected publish to be boolean false, got ${typeof fm.publish}: ${fm.publish}`);
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Boolean types are correct');
 | |
| });
 | |
| 
 | |
| test('Should use correct array types for tags and aliases', async () => {
 | |
|   const filePath = await createTestFile('arrays.md', '# Test\n\nContent.');
 | |
|   
 | |
|   await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   const fm = await extractFrontmatter(filePath);
 | |
|   
 | |
|   if (!Array.isArray(fm.tags)) {
 | |
|     throw new Error(`Expected tags to be array, got ${typeof fm.tags}`);
 | |
|   }
 | |
|   
 | |
|   if (!Array.isArray(fm.aliases)) {
 | |
|     throw new Error(`Expected aliases to be array, got ${typeof fm.aliases}`);
 | |
|   }
 | |
|   
 | |
|   if (fm.tags.length !== 0) {
 | |
|     throw new Error(`Expected tags to be empty array, got length ${fm.tags.length}`);
 | |
|   }
 | |
|   
 | |
|   if (fm.aliases.length !== 0) {
 | |
|     throw new Error(`Expected aliases to be empty array, got length ${fm.aliases.length}`);
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Array types are correct');
 | |
| });
 | |
| 
 | |
| test('Should format dates in ISO 8601 with timezone', async () => {
 | |
|   const filePath = await createTestFile('dates.md', '# Test\n\nContent.');
 | |
|   
 | |
|   await enrichFrontmatterOnOpen(filePath);
 | |
|   
 | |
|   const fm = await extractFrontmatter(filePath);
 | |
|   
 | |
|   // Check creation_date format
 | |
|   if (!fm.creation_date || typeof fm.creation_date !== 'string') {
 | |
|     throw new Error('Expected creation_date to be a string');
 | |
|   }
 | |
|   
 | |
|   if (!fm.creation_date.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)) {
 | |
|     throw new Error(`creation_date format incorrect: ${fm.creation_date}`);
 | |
|   }
 | |
|   
 | |
|   // Check modification_date format
 | |
|   if (!fm.modification_date || typeof fm.modification_date !== 'string') {
 | |
|     throw new Error('Expected modification_date to be a string');
 | |
|   }
 | |
|   
 | |
|   if (!fm.modification_date.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)) {
 | |
|     throw new Error(`modification_date format incorrect: ${fm.modification_date}`);
 | |
|   }
 | |
|   
 | |
|   console.log('✓ Date formats are correct');
 | |
| });
 | |
| 
 | |
| // Run all tests
 | |
| async function runTests() {
 | |
|   console.log('\n🧪 Running front-matter enrichment tests...\n');
 | |
|   
 | |
|   await setupTestDir();
 | |
|   
 | |
|   let passed = 0;
 | |
|   let failed = 0;
 | |
|   
 | |
|   for (const { name, fn } of tests) {
 | |
|     try {
 | |
|       await fn();
 | |
|       passed++;
 | |
|     } catch (err) {
 | |
|       console.error(`✗ ${name}`);
 | |
|       console.error(`  ${err.message}`);
 | |
|       failed++;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   await cleanupTestDir();
 | |
|   
 | |
|   console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
 | |
|   
 | |
|   if (failed > 0) {
 | |
|     process.exit(1);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Execute if run directly
 | |
| if (process.argv[1] === __filename) {
 | |
|   runTests().catch(err => {
 | |
|     console.error('Test runner error:', err);
 | |
|     process.exit(1);
 | |
|   });
 | |
| }
 |