#!/usr/bin/env tsx /** * Simple smoke test for the blog * Tests: Build succeeds, key files exist, data is valid */ import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; console.log('šŸ” Running smoke tests for mintel.me blog...\n'); let passed = 0; let failed = 0; function test(name: string, fn: () => void): void { try { fn(); 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'); // Check for basic structure if (!content.includes('export const blogPosts')) { throw new Error('blogPosts export not found'); } // Check for required fields in posts const postsMatch = content.match(/\{[^}]+\}/g); if (!postsMatch || postsMatch.length === 0) { throw new Error('No posts found in blogPosts.ts'); } // Validate at least one post has required fields 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.tsx', 'src/components/SearchBar.tsx', 'src/components/ArticleBlockquote.tsx' ]; 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')) { 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' ]; 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', // Single page with everything 'src/components/SearchBar.tsx', // Search component 'src/components/MediumCard.tsx' // Post cards ]; 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'); // Check that it imports blogPosts if (!homeContent.includes('import') || !homeContent.includes('blogPosts')) { throw new Error('Home page does not import blogPosts'); } // Check that it renders posts if (!homeContent.includes('allPosts.map') && !homeContent.includes('blogPosts.map')) { throw new Error('Home page does not render blog posts'); } // Check that MediumCard is imported and used 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'); // Extract all posts const posts = content.match(/\{[^}]+\}/g) || []; if (posts.length === 0) { throw new Error('No posts found in blogPosts.ts'); } // Check each post has required fields 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 individual blog post pages exist test('Individual blog post pages exist', () => { const blogDir = path.join(process.cwd(), 'src/pages/blog'); const files = fs.readdirSync(blogDir); const astroFiles = files.filter(f => f.endsWith('.astro')); if (astroFiles.length === 0) { throw new Error('No blog post files found'); } // Check that [slug].astro exists for dynamic routing if (!files.includes('[slug].astro')) { throw new Error('Missing [slug].astro for dynamic blog post routing'); } // Check that individual post files exist const requiredPosts = ['first-note.astro', 'debugging-tips.astro']; for (const post of requiredPosts) { if (!astroFiles.includes(post)) { throw new Error(`Missing blog post file: ${post}`); } } }); // Test 12: Verify MediumCard links to individual posts test('MediumCard links to individual post pages', () => { const mediumCardPath = path.join(process.cwd(), 'src/components/MediumCard.tsx'); const content = fs.readFileSync(mediumCardPath, 'utf8'); // Should contain href to blog posts if (!content.includes('href={`/blog/')) { throw new Error('MediumCard should contain links to individual post pages'); } // Should contain tags if (!content.includes('')) { throw new Error('MediumCard should contain anchor tags'); } // Should also have tag filtering if (!content.includes('onClick')) { throw new Error('MediumCard should have onClick handlers for tags'); } }); // Test 13: 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'); // Should not contain navigation links if (content.includes('href="/about"') || content.includes('href="/blog"')) { throw new Error('Home page contains navigation links'); } // Should not contain nav or header navigation if (content.includes(' { const blogDir = path.join(process.cwd(), 'src/pages/blog'); const files = fs.readdirSync(blogDir); const astroFiles = files.filter(f => f.endsWith('.astro') && f !== '[slug].astro'); for (const file of astroFiles) { const content = fs.readFileSync(path.join(blogDir, file), 'utf8'); if (content.includes('import BlogLayout')) { throw new Error(`${file} still imports BlogLayout which doesn't exist`); } if (!content.includes('import BaseLayout')) { throw new Error(`${file} doesn't import BaseLayout`); } } }); // Test 15: Verify blog post pages have proper structure test('Blog posts have proper structure', () => { const blogDir = path.join(process.cwd(), 'src/pages/blog'); const files = fs.readdirSync(blogDir); const astroFiles = files.filter(f => f.endsWith('.astro') && f !== '[slug].astro'); for (const file of astroFiles) { const content = fs.readFileSync(path.join(blogDir, file), 'utf8'); // Should close BaseLayout properly if (!content.includes('')) { throw new Error(`${file} doesn't close BaseLayout properly`); } // Should have article wrapper if (!content.includes('')) { throw new Error(`${file} missing header section`); } } }); // Test 16: Verify all imports in blog posts are valid test('All imports in blog posts are valid', () => { const blogDir = path.join(process.cwd(), 'src/pages/blog'); const files = fs.readdirSync(blogDir); const astroFiles = files.filter(f => f.endsWith('.astro') && f !== '[slug].astro'); for (const file of astroFiles) { const content = fs.readFileSync(path.join(blogDir, file), 'utf8'); // Extract all imports const importMatches = content.match(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g) || []; for (const importLine of importMatches) { // Extract the path const pathMatch = importLine.match(/from\s+['"]([^'"]+)['"]/); if (!pathMatch) continue; const importPath = pathMatch[1]; // Skip relative imports for now, just check they don't reference BlogLayout if (importPath.includes('BlogLayout')) { throw new Error(`${file} imports BlogLayout which doesn't exist`); } // Check that BaseLayout is imported correctly if (importPath.includes('BaseLayout') && !importPath.includes('../../layouts/BaseLayout')) { throw new Error(`${file} has incorrect BaseLayout import path`); } } } }); // Test 17: 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'); // Should import BaseLayout if (!content.includes('import BaseLayout')) { throw new Error('Tag page does not import BaseLayout'); } // Should have getStaticPaths if (!content.includes('getStaticPaths')) { throw new Error('Tag page missing getStaticPaths function'); } // Should render posts if (!content.includes('posts.map')) { throw new Error('Tag page does not render posts'); } }); // Test 18: Verify [slug].astro page exists and is valid test('Dynamic slug page exists and is valid', () => { const slugPagePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro'); if (!fs.existsSync(slugPagePath)) { throw new Error('Dynamic slug page [slug].astro does not exist'); } const content = fs.readFileSync(slugPagePath, 'utf8'); // Should import BaseLayout if (!content.includes('import BaseLayout')) { throw new Error('Slug page does not import BaseLayout'); } // Should have getStaticPaths if (!content.includes('getStaticPaths')) { throw new Error('Slug page missing getStaticPaths function'); } // Should render post content if (!content.includes('post.title') || !content.includes('post.description')) { throw new Error('Slug page does not render post data'); } }); // Test 19: Verify no syntax errors in any Astro files test('No syntax errors in Astro files', () => { const astroFiles = [ 'src/pages/index.astro', 'src/pages/blog/[slug].astro', 'src/pages/blog/first-note.astro', 'src/pages/blog/debugging-tips.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 HTML tags const openTags = (content.match(/<[^/][^>]*>/g) || []).length; const closeTags = (content.match(/<\/[^>]+>/g) || []).length; // 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'); // Check for basic syntax if (!content.includes('export const blogPosts')) { throw new Error('blogPosts.ts does not export blogPosts'); } // Check that it's valid TypeScript if (content.includes('interface') || content.includes('type')) { // If it has types, they should be properly formatted 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 (catches runtime errors) test('Astro build succeeds without runtime errors', () => { try { // Try to build the project - this will catch runtime errors 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; // Check for common runtime errors 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')) { throw new Error(`Build failed: ${combined.substring(0, 300)}`); } // If it's just a warning, that's okay if (combined.includes('warning')) { return; } throw new Error(`Build failed for unknown reason: ${combined.substring(0, 200)}`); } }); // Test 22: Verify all components can be imported test('All components can be imported without errors', () => { const components = [ 'src/components/MediumCard.tsx', 'src/components/SearchBar.tsx', 'src/components/ArticleBlockquote.tsx', 'src/components/ArticleHeading.tsx', 'src/components/ArticleParagraph.tsx', 'src/components/ArticleList.tsx', 'src/components/Footer.tsx' ]; 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`); } // Extract JSX content (between return and end of component) const jsxMatch = content.match(/return\s*\(([\s\S]*?)\)\s*;?\s*\}\s*$/); if (jsxMatch) { const jsxContent = jsxMatch[1]; // Only count actual HTML/JSX tags, not TypeScript types // Filter out tags that are clearly TypeScript (like , , etc.) const openTags = (jsxContent.match(/<([A-Za-z][A-Za-z0-9-]*)(\s|>)/g) || []).length; const closeTags = (jsxContent.match(/<\/[A-Za-z][A-Za-z0-9-]*>/g) || []).length; if (openTags !== closeTags) { throw new Error(`${component}: unbalanced JSX tags (open: ${openTags}, close: ${closeTags})`); } } } }); // 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 smoke tests passed! Your blog is ready to use.'); 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); }