#!/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('