#!/usr/bin/env tsx /** * Updated smoke test for the blog with file examples * Tests: Build succeeds, key files exist, data is valid, file examples work */ import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; console.log('šŸ” Running updated smoke tests for mintel.me blog...\n'); let passed = 0; let failed = 0; async function test(name: string, fn: () => void | Promise): Promise { try { const result = fn(); if (result instanceof Promise) { await result; } console.log(`āœ… ${name}`); passed++; } catch (error) { console.log(`āŒ ${name}`); if (error instanceof Error) { console.log(` Error: ${error.message}`); } failed++; } } // Test 1: Check required files exist test('Required files exist', () => { const requiredFiles = [ 'package.json', 'astro.config.mjs', 'tailwind.config.js', 'src/pages/index.astro', 'src/layouts/BaseLayout.astro', 'src/data/blogPosts.ts', 'src/styles/global.css' ]; for (const file of requiredFiles) { if (!fs.existsSync(path.join(process.cwd(), file))) { throw new Error(`Missing file: ${file}`); } } }); // Test 2: Validate blog posts data test('Blog posts data is valid', () => { const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts'); const content = fs.readFileSync(blogPostsPath, 'utf8'); if (!content.includes('export const blogPosts')) { throw new Error('blogPosts export not found'); } const postsMatch = content.match(/\{[^}]+\}/g); if (!postsMatch || postsMatch.length === 0) { throw new Error('No posts found in blogPosts.ts'); } const firstPost = postsMatch[0]; const requiredFields = ['title', 'description', 'date', 'slug', 'tags']; for (const field of requiredFields) { if (!firstPost.includes(field)) { throw new Error(`First post missing required field: ${field}`); } } }); // Test 3: Check Astro config test('Astro configuration is valid', () => { const configPath = path.join(process.cwd(), 'astro.config.mjs'); const content = fs.readFileSync(configPath, 'utf8'); if (!content.includes('site:')) { throw new Error('site URL not configured'); } if (!content.includes('react()')) { throw new Error('React integration not configured'); } }); // Test 4: Validate Tailwind config test('Tailwind configuration is valid', () => { const configPath = path.join(process.cwd(), 'tailwind.config.js'); const content = fs.readFileSync(configPath, 'utf8'); if (!content.includes('content:')) { throw new Error('content paths not configured'); } if (!content.includes('plugins:')) { throw new Error('plugins not configured'); } }); // Test 5: Check for syntax errors in key components test('Key components have valid syntax', () => { const components = [ 'src/components/MediumCard.astro', 'src/components/SearchBar.tsx', 'src/components/ArticleBlockquote.tsx', 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro' ]; for (const component of components) { const componentPath = path.join(process.cwd(), component); if (fs.existsSync(componentPath)) { const content = fs.readFileSync(componentPath, 'utf8'); // Basic syntax checks if (content.includes('import') && !content.includes('export') && !content.includes('---')) { throw new Error(`${component}: has imports but no exports`); } // Check for balanced braces const openBraces = (content.match(/{/g) || []).length; const closeBraces = (content.match(/}/g) || []).length; if (openBraces !== closeBraces) { throw new Error(`${component}: unbalanced braces`); } } } }); // Test 6: Check global styles test('Global styles include required classes', () => { const stylesPath = path.join(process.cwd(), 'src/styles/global.css'); const content = fs.readFileSync(stylesPath, 'utf8'); const requiredClasses = [ '.container', '.highlighter-tag', '.post-card', '.article-content', '.search-box', '.file-example', // New file example classes ]; for (const className of requiredClasses) { if (!content.includes(className)) { throw new Error(`Missing required class: ${className}`); } } }); // Test 7: Verify package.json scripts test('Package.json has required scripts', () => { const packagePath = path.join(process.cwd(), 'package.json'); const content = fs.readFileSync(packagePath, 'utf8'); const requiredScripts = ['dev', 'build', 'preview', 'test:smoke']; for (const script of requiredScripts) { if (!content.includes(`"${script}"`)) { throw new Error(`Missing script: ${script}`); } } }); // Test 8: Check for single-page blog functionality test('Single-page blog functionality exists', () => { const functionalityFiles = [ 'src/pages/index.astro', 'src/components/SearchBar.tsx', 'src/components/MediumCard.astro' ]; for (const file of functionalityFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { throw new Error(`Missing functionality file: ${file}`); } } }); // Test 9: Check that all blog posts load in the home page test('All blog posts load in home page', () => { const homePagePath = path.join(process.cwd(), 'src/pages/index.astro'); const homeContent = fs.readFileSync(homePagePath, 'utf8'); if (!homeContent.includes('import') || !homeContent.includes('blogPosts')) { throw new Error('Home page does not import blogPosts'); } if (!homeContent.includes('allPosts.map') && !homeContent.includes('blogPosts.map')) { throw new Error('Home page does not render blog posts'); } if (!homeContent.includes('MediumCard')) { throw new Error('MediumCard component not used in home page'); } }); // Test 10: Verify blog posts have all required fields test('Blog posts have all required fields', () => { const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts'); const content = fs.readFileSync(blogPostsPath, 'utf8'); const posts = content.match(/\{[^}]+\}/g) || []; if (posts.length === 0) { throw new Error('No posts found in blogPosts.ts'); } const requiredFields = ['title', 'description', 'date', 'slug', 'tags']; posts.forEach((post, index) => { for (const field of requiredFields) { if (!post.includes(field)) { throw new Error(`Post ${index + 1} missing required field: ${field}`); } } }); }); // Test 11: Verify dynamic blog post routing exists test('Dynamic blog post routing exists', () => { const blogDir = path.join(process.cwd(), 'src/pages/blog'); const files = fs.readdirSync(blogDir); if (!files.includes('[slug].astro')) { throw new Error('Missing [slug].astro for dynamic blog post routing'); } const slugPagePath = path.join(blogDir, '[slug].astro'); const content = fs.readFileSync(slugPagePath, 'utf8'); if (!content.includes('getStaticPaths')) { throw new Error('[slug].astro missing getStaticPaths function'); } if (!content.includes('blogPosts')) { throw new Error('[slug].astro does not use blogPosts data'); } }); // Test 12: Verify home page has no navigation test('Home page has no navigation links', () => { const homePagePath = path.join(process.cwd(), 'src/pages/index.astro'); const content = fs.readFileSync(homePagePath, 'utf8'); if (content.includes('href="/about"') || content.includes('href="/blog"')) { throw new Error('Home page contains navigation links'); } if (content.includes(' { const slugPagePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro'); const content = fs.readFileSync(slugPagePath, 'utf8'); if (!content.includes('import BaseLayout')) { throw new Error('Slug page does not import BaseLayout'); } if (content.includes('import BlogLayout')) { throw new Error('Slug page still imports BlogLayout which does not exist'); } }); // Test 14: Verify tag pages exist and are valid test('Tag pages exist and are valid', () => { const tagPagePath = path.join(process.cwd(), 'src/pages/tags/[tag].astro'); if (!fs.existsSync(tagPagePath)) { throw new Error('Tag page [tag].astro does not exist'); } const content = fs.readFileSync(tagPagePath, 'utf8'); if (!content.includes('import BaseLayout')) { throw new Error('Tag page does not import BaseLayout'); } if (!content.includes('getStaticPaths')) { throw new Error('Tag page missing getStaticPaths function'); } if (!content.includes('posts.map')) { throw new Error('Tag page does not render posts'); } }); // Test 15: Verify file examples functionality exists test('File examples functionality exists', () => { const requiredFiles = [ 'src/data/fileExamples.ts', 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro', 'src/pages/api/download-zip.ts', ]; for (const file of requiredFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { throw new Error(`Missing file examples file: ${file}`); } } }); // Test 16: Verify file examples data structure test('File examples data is valid', () => { const fileExamplesPath = path.join(process.cwd(), 'src/data/fileExamples.ts'); const content = fs.readFileSync(fileExamplesPath, 'utf8'); if (!content.includes('export interface FileExample')) { throw new Error('FileExample interface not found'); } if (!content.includes('export const sampleFileExamples')) { throw new Error('sampleFileExamples export not found'); } if (!content.includes('FileExampleManager')) { throw new Error('FileExampleManager class not found'); } }); // Test 17: Verify blog template supports file examples test('Blog template supports file examples', () => { const blogTemplatePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro'); const content = fs.readFileSync(blogTemplatePath, 'utf8'); if (!content.includes('FileExamplesList')) { throw new Error('Blog template does not import FileExamplesList'); } if (!content.includes('showFileExamples')) { throw new Error('Blog template does not have file examples logic'); } }); // Test 18: Verify API endpoint exists test('Download API endpoint exists', () => { const apiPath = path.join(process.cwd(), 'src/pages/api/download-zip.ts'); const content = fs.readFileSync(apiPath, 'utf8'); if (!content.includes('export const POST')) { throw new Error('API endpoint missing POST handler'); } if (!content.includes('export const GET')) { throw new Error('API endpoint missing GET handler'); } if (!content.includes('FileExampleManager')) { throw new Error('API endpoint does not use FileExampleManager'); } }); // Test 19: Verify no syntax errors in Astro files test('No syntax errors in Astro files', () => { const astroFiles = [ 'src/pages/index.astro', 'src/pages/blog/[slug].astro', 'src/pages/tags/[tag].astro', 'src/layouts/BaseLayout.astro' ]; for (const file of astroFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { throw new Error(`File ${file} does not exist`); } const content = fs.readFileSync(filePath, 'utf8'); // Check for balanced braces in script sections const scriptSections = content.match(/---[\s\S]*?---/g) || []; let totalOpenBraces = 0; let totalCloseBraces = 0; for (const section of scriptSections) { totalOpenBraces += (section.match(/{/g) || []).length; totalCloseBraces += (section.match(/}/g) || []).length; } if (totalOpenBraces !== totalCloseBraces) { throw new Error(`${file} has unbalanced braces in script section`); } } }); // Test 20: Verify blogPosts.ts can be imported without errors test('blogPosts.ts can be imported without errors', () => { const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts'); const content = fs.readFileSync(blogPostsPath, 'utf8'); if (!content.includes('export const blogPosts')) { throw new Error('blogPosts.ts does not export blogPosts'); } const openBraces = (content.match(/{/g) || []).length; const closeBraces = (content.match(/}/g) || []).length; if (openBraces !== closeBraces) { throw new Error('blogPosts.ts has unbalanced braces'); } }); // Test 21: Verify Astro build succeeds test('Astro build succeeds without runtime errors', () => { try { execSync('npm run build', { cwd: process.cwd(), stdio: 'pipe', timeout: 60000 }); } catch (error: any) { const stderr = error.stderr?.toString() || ''; const stdout = error.stdout?.toString() || ''; const combined = stderr + stdout; if (combined.includes('children.trim is not a function')) { throw new Error('Build failed: children.trim error - component children issue'); } if (combined.includes('is not a function')) { throw new Error(`Build failed with runtime error: ${combined.substring(0, 200)}`); } if (combined.includes('Build failed') && !combined.includes('warning')) { throw new Error(`Build failed: ${combined.substring(0, 300)}`); } } }); // Test 22: Verify all components can be imported test('All components can be imported without errors', () => { const components = [ 'src/components/MediumCard.astro', 'src/components/SearchBar.tsx', 'src/components/ArticleBlockquote.tsx', 'src/components/ArticleHeading.tsx', 'src/components/ArticleParagraph.tsx', 'src/components/ArticleList.tsx', 'src/components/Footer.tsx', 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro' ]; for (const component of components) { const componentPath = path.join(process.cwd(), component); if (!fs.existsSync(componentPath)) { throw new Error(`Component ${component} does not exist`); } const content = fs.readFileSync(componentPath, 'utf8'); // Check for common issues if (content.includes('children.trim') && !content.includes('children?.trim')) { throw new Error(`${component}: uses children.trim without null check`); } } }); // Test 23: Run comprehensive file examples tests test('File examples data system works correctly', async () => { // Import and run the file examples tests const { runFileExamplesTests } = await import('../src/utils/test-file-examples.ts'); await runFileExamplesTests(); }); // Test 24: Run blog post integration tests test('Blog post and file examples integration works', async () => { // Import and run the integration tests const { testBlogPostIntegration } = await import('../src/utils/test-component-integration.ts'); await testBlogPostIntegration(); }); // Summary async function runAllTests() { // Run synchronous tests first const syncTests = [ () => test('Required files exist', () => { const requiredFiles = [ 'package.json', 'astro.config.mjs', 'tailwind.config.js', 'src/pages/index.astro', 'src/layouts/BaseLayout.astro', 'src/data/blogPosts.ts', 'src/styles/global.css' ]; for (const file of requiredFiles) { if (!fs.existsSync(path.join(process.cwd(), file))) { throw new Error(`Missing file: ${file}`); } } }), () => test('Blog posts data is valid', () => { const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts'); const content = fs.readFileSync(blogPostsPath, 'utf8'); if (!content.includes('export const blogPosts')) { throw new Error('blogPosts export not found'); } const postsMatch = content.match(/\{[^}]+\}/g); if (!postsMatch || postsMatch.length === 0) { throw new Error('No posts found in blogPosts.ts'); } const firstPost = postsMatch[0]; const requiredFields = ['title', 'description', 'date', 'slug', 'tags']; for (const field of requiredFields) { if (!firstPost.includes(field)) { throw new Error(`First post missing required field: ${field}`); } } }), () => test('Astro configuration is valid', () => { const configPath = path.join(process.cwd(), 'astro.config.mjs'); const content = fs.readFileSync(configPath, 'utf8'); if (!content.includes('site:')) { throw new Error('site URL not configured'); } if (!content.includes('react()')) { throw new Error('React integration not configured'); } }), () => test('Tailwind configuration is valid', () => { const configPath = path.join(process.cwd(), 'tailwind.config.js'); const content = fs.readFileSync(configPath, 'utf8'); if (!content.includes('content:')) { throw new Error('content paths not configured'); } if (!content.includes('plugins:')) { throw new Error('plugins not configured'); } }), () => test('Key components have valid syntax', () => { const components = [ 'src/components/MediumCard.astro', 'src/components/SearchBar.tsx', 'src/components/ArticleBlockquote.tsx', 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro' ]; for (const component of components) { const componentPath = path.join(process.cwd(), component); if (fs.existsSync(componentPath)) { const content = fs.readFileSync(componentPath, 'utf8'); // Basic syntax checks if (content.includes('import') && !content.includes('export') && !content.includes('---')) { throw new Error(`${component}: has imports but no exports`); } // Check for balanced braces const openBraces = (content.match(/{/g) || []).length; const closeBraces = (content.match(/}/g) || []).length; if (openBraces !== closeBraces) { throw new Error(`${component}: unbalanced braces`); } } } }), () => test('Global styles include required classes', () => { const stylesPath = path.join(process.cwd(), 'src/styles/global.css'); const content = fs.readFileSync(stylesPath, 'utf8'); const requiredClasses = [ '.container', '.highlighter-tag', '.post-card', '.article-content', '.search-box', '.file-example', ]; for (const className of requiredClasses) { if (!content.includes(className)) { throw new Error(`Missing required class: ${className}`); } } }), () => test('Package.json has required scripts', () => { const packagePath = path.join(process.cwd(), 'package.json'); const content = fs.readFileSync(packagePath, 'utf8'); const requiredScripts = ['dev', 'build', 'preview', 'test:smoke']; for (const script of requiredScripts) { if (!content.includes(`"${script}"`)) { throw new Error(`Missing script: ${script}`); } } }), () => test('Single-page blog functionality exists', () => { const functionalityFiles = [ 'src/pages/index.astro', 'src/components/SearchBar.tsx', 'src/components/MediumCard.astro' ]; for (const file of functionalityFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { throw new Error(`Missing functionality file: ${file}`); } } }), () => test('All blog posts load in home page', () => { const homePagePath = path.join(process.cwd(), 'src/pages/index.astro'); const homeContent = fs.readFileSync(homePagePath, 'utf8'); if (!homeContent.includes('import') || !homeContent.includes('blogPosts')) { throw new Error('Home page does not import blogPosts'); } if (!homeContent.includes('allPosts.map') && !homeContent.includes('blogPosts.map')) { throw new Error('Home page does not render blog posts'); } if (!homeContent.includes('MediumCard')) { throw new Error('MediumCard component not used in home page'); } }), () => test('Blog posts have all required fields', () => { const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts'); const content = fs.readFileSync(blogPostsPath, 'utf8'); const posts = content.match(/\{[^}]+\}/g) || []; if (posts.length === 0) { throw new Error('No posts found in blogPosts.ts'); } const requiredFields = ['title', 'description', 'date', 'slug', 'tags']; posts.forEach((post, index) => { for (const field of requiredFields) { if (!post.includes(field)) { throw new Error(`Post ${index + 1} missing required field: ${field}`); } } }); }), () => test('Dynamic blog post routing exists', () => { const blogDir = path.join(process.cwd(), 'src/pages/blog'); const files = fs.readdirSync(blogDir); if (!files.includes('[slug].astro')) { throw new Error('Missing [slug].astro for dynamic blog post routing'); } const slugPagePath = path.join(blogDir, '[slug].astro'); const content = fs.readFileSync(slugPagePath, 'utf8'); if (!content.includes('getStaticPaths')) { throw new Error('[slug].astro missing getStaticPaths function'); } if (!content.includes('blogPosts')) { throw new Error('[slug].astro does not use blogPosts data'); } }), () => test('Home page has no navigation links', () => { const homePagePath = path.join(process.cwd(), 'src/pages/index.astro'); const content = fs.readFileSync(homePagePath, 'utf8'); if (content.includes('href="/about"') || content.includes('href="/blog"')) { throw new Error('Home page contains navigation links'); } if (content.includes(' test('Blog posts use BaseLayout', () => { const slugPagePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro'); const content = fs.readFileSync(slugPagePath, 'utf8'); if (!content.includes('import BaseLayout')) { throw new Error('Slug page does not import BaseLayout'); } if (content.includes('import BlogLayout')) { throw new Error('Slug page still imports BlogLayout which does not exist'); } }), () => test('Tag pages exist and are valid', () => { const tagPagePath = path.join(process.cwd(), 'src/pages/tags/[tag].astro'); if (!fs.existsSync(tagPagePath)) { throw new Error('Tag page [tag].astro does not exist'); } const content = fs.readFileSync(tagPagePath, 'utf8'); if (!content.includes('import BaseLayout')) { throw new Error('Tag page does not import BaseLayout'); } if (!content.includes('getStaticPaths')) { throw new Error('Tag page missing getStaticPaths function'); } if (!content.includes('posts.map')) { throw new Error('Tag page does not render posts'); } }), () => test('File examples functionality exists', () => { const requiredFiles = [ 'src/data/fileExamples.ts', 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro', 'src/pages/api/download-zip.ts', ]; for (const file of requiredFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { throw new Error(`Missing file examples file: ${file}`); } } }), () => test('File examples data is valid', () => { const fileExamplesPath = path.join(process.cwd(), 'src/data/fileExamples.ts'); const content = fs.readFileSync(fileExamplesPath, 'utf8'); if (!content.includes('export interface FileExample')) { throw new Error('FileExample interface not found'); } if (!content.includes('export const sampleFileExamples')) { throw new Error('sampleFileExamples export not found'); } if (!content.includes('FileExampleManager')) { throw new Error('FileExampleManager class not found'); } }), () => test('Blog template supports file examples', () => { const blogTemplatePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro'); const content = fs.readFileSync(blogTemplatePath, 'utf8'); if (!content.includes('FileExamplesList')) { throw new Error('Blog template does not import FileExamplesList'); } if (!content.includes('showFileExamples')) { throw new Error('Blog template does not have file examples logic'); } }), () => test('Download API endpoint exists', () => { const apiPath = path.join(process.cwd(), 'src/pages/api/download-zip.ts'); const content = fs.readFileSync(apiPath, 'utf8'); if (!content.includes('export const POST')) { throw new Error('API endpoint missing POST handler'); } if (!content.includes('export const GET')) { throw new Error('API endpoint missing GET handler'); } if (!content.includes('FileExampleManager')) { throw new Error('API endpoint does not use FileExampleManager'); } }), () => test('No syntax errors in Astro files', () => { const astroFiles = [ 'src/pages/index.astro', 'src/pages/blog/[slug].astro', 'src/pages/tags/[tag].astro', 'src/layouts/BaseLayout.astro' ]; for (const file of astroFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { throw new Error(`File ${file} does not exist`); } const content = fs.readFileSync(filePath, 'utf8'); // Check for balanced braces in script sections const scriptSections = content.match(/---[\s\S]*?---/g) || []; let totalOpenBraces = 0; let totalCloseBraces = 0; for (const section of scriptSections) { totalOpenBraces += (section.match(/{/g) || []).length; totalCloseBraces += (section.match(/}/g) || []).length; } if (totalOpenBraces !== totalCloseBraces) { throw new Error(`${file} has unbalanced braces in script section`); } } }), () => test('blogPosts.ts can be imported without errors', () => { const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts'); const content = fs.readFileSync(blogPostsPath, 'utf8'); if (!content.includes('export const blogPosts')) { throw new Error('blogPosts.ts does not export blogPosts'); } const openBraces = (content.match(/{/g) || []).length; const closeBraces = (content.match(/}/g) || []).length; if (openBraces !== closeBraces) { throw new Error('blogPosts.ts has unbalanced braces'); } }), () => test('Astro build succeeds without runtime errors', () => { try { execSync('npm run build', { cwd: process.cwd(), stdio: 'pipe', timeout: 60000 }); } catch (error: any) { const stderr = error.stderr?.toString() || ''; const stdout = error.stdout?.toString() || ''; const combined = stderr + stdout; if (combined.includes('children.trim is not a function')) { throw new Error('Build failed: children.trim error - component children issue'); } if (combined.includes('is not a function')) { throw new Error(`Build failed with runtime error: ${combined.substring(0, 200)}`); } if (combined.includes('Build failed') && !combined.includes('warning')) { throw new Error(`Build failed: ${combined.substring(0, 300)}`); } } }), () => test('All components can be imported without errors', () => { const components = [ 'src/components/MediumCard.astro', 'src/components/SearchBar.tsx', 'src/components/ArticleBlockquote.tsx', 'src/components/ArticleHeading.tsx', 'src/components/ArticleParagraph.tsx', 'src/components/ArticleList.tsx', 'src/components/Footer.tsx', 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro' ]; for (const component of components) { const componentPath = path.join(process.cwd(), component); if (!fs.existsSync(componentPath)) { throw new Error(`Component ${component} does not exist`); } const content = fs.readFileSync(componentPath, 'utf8'); // Check for common issues if (content.includes('children.trim') && !content.includes('children?.trim')) { throw new Error(`${component}: uses children.trim without null check`); } } }), ]; // Run all sync tests for (const testFn of syncTests) { await testFn(); } // Summary console.log('\n' + '='.repeat(50)); console.log(`Tests passed: ${passed}`); console.log(`Tests failed: ${failed}`); console.log('='.repeat(50)); if (failed === 0) { console.log('\nšŸŽ‰ All updated smoke tests passed! Your blog with file examples is ready to use.'); console.log('\nKey features verified:'); console.log(' āœ… Single-page blog with search and filtering'); console.log(' āœ… Dynamic blog post routing'); console.log(' āœ… Tag filtering pages'); console.log(' āœ… File examples with copy/download/zip functionality'); console.log(' āœ… API endpoints for file downloads'); console.log(' āœ… Comprehensive data and integration tests'); console.log('\nTo start development:'); console.log(' npm run dev'); console.log('\nTo build for production:'); console.log(' npm run build'); process.exit(0); } else { console.log('\nāŒ Some tests failed. Please check the errors above.'); process.exit(1); } } runAllTests().catch(err => { console.error('Test runner failed:', err); process.exit(1); });