forked from colin/resume
2
0
Fork 0

Add Playwright and Lighthouse testing infrastructure

This commit is contained in:
Your Name 2025-03-31 04:42:57 -04:00
parent 39caf88782
commit a10eea979f
5 changed files with 2449 additions and 0 deletions

2187
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "resume",
"version": "1.0.0",
"description": "Colin Knapp's professional resume website",
"main": "index.js",
"scripts": {
"serve": "node tests/serve.js",
"test": "npm run test:lighthouse && npm run test:playwright",
"test:playwright": "npx playwright test",
"test:lighthouse": "node tests/lighthouse.js",
"setup": "npx playwright install"
},
"repository": {
"type": "git",
"url": "git@git.nixc.us:colin/resume.git"
},
"keywords": [
"resume",
"portfolio",
"accessibility"
],
"author": "Colin Knapp",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.42.1",
"lighthouse": "^11.4.0",
"puppeteer": "^22.4.1"
}
}

98
tests/lighthouse.js Normal file
View File

@ -0,0 +1,98 @@
const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const { URL } = require('url');
const fs = require('fs');
const path = require('path');
// Configuration for Lighthouse
const LIGHTHOUSE_CONFIG = {
extends: 'lighthouse:default',
settings: {
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
formFactor: 'desktop',
throttlingMethod: 'simulate',
},
};
// Set threshold values for each category
const THRESHOLDS = {
performance: 90,
accessibility: 90,
'best-practices': 90,
seo: 90,
};
async function runLighthouse(url) {
// Launch a headless browser
const browser = await puppeteer.launch({ headless: true });
// Create a new Lighthouse runner
const { lhr } = await lighthouse(url, {
port: (new URL(browser.wsEndpoint())).port,
output: 'json',
logLevel: 'info',
}, LIGHTHOUSE_CONFIG);
await browser.close();
// Create directory for reports if it doesn't exist
const reportsDir = path.join(__dirname, 'reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir);
}
// Save the report
const reportPath = path.join(reportsDir, `lighthouse-report-${Date.now()}.json`);
fs.writeFileSync(reportPath, JSON.stringify(lhr, null, 2));
console.log('\nLighthouse Results:');
console.log('--------------------');
// Process and display results
let allPassed = true;
Object.keys(THRESHOLDS).forEach(category => {
const score = Math.round(lhr.categories[category].score * 100);
const threshold = THRESHOLDS[category];
const passed = score >= threshold;
if (!passed) {
allPassed = false;
}
console.log(`${category}: ${score}/100 - ${passed ? 'PASS' : 'FAIL'} (Threshold: ${threshold})`);
});
// Display audits that failed
console.log('\nFailed Audits:');
console.log('--------------');
let hasFailedAudits = false;
Object.values(lhr.audits).forEach(audit => {
if (audit.score !== null && audit.score < 0.9) {
hasFailedAudits = true;
console.log(`- ${audit.title}: ${Math.round(audit.score * 100)}/100`);
console.log(` ${audit.description}`);
}
});
if (!hasFailedAudits) {
console.log('No significant audit failures!');
}
console.log(`\nDetailed report saved to: ${reportPath}`);
return allPassed;
}
// URL to test - this should be your local server address
const url = process.argv[2] || 'http://localhost:8080';
runLighthouse(url)
.then(passed => {
console.log(`\nOverall: ${passed ? 'PASSED' : 'FAILED'}`);
process.exit(passed ? 0 : 1);
})
.catch(error => {
console.error('Error running Lighthouse:', error);
process.exit(1);
});

73
tests/serve.js Normal file
View File

@ -0,0 +1,73 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const PORT = 8080;
const RESUME_DIR = path.join(__dirname, '..', 'docker', 'resume');
// MIME types for common file extensions
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain',
};
// Create a simple HTTP server
const server = http.createServer((req, res) => {
// Parse the request URL
const parsedUrl = url.parse(req.url);
let pathname = parsedUrl.pathname;
// Set default file to index.html
if (pathname === '/') {
pathname = '/index.html';
}
// Construct the file path
const filePath = path.join(RESUME_DIR, pathname);
// Get the file extension
const ext = path.extname(filePath);
// Set the content type based on the file extension
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
// Read the file and serve it
fs.readFile(filePath, (err, data) => {
if (err) {
// If the file doesn't exist, return 404
if (err.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
console.log(`404: ${pathname}`);
return;
}
// For other errors, return 500
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('500 Internal Server Error');
console.error(`Error serving ${pathname}:`, err);
return;
}
// If file is found, serve it with the correct content type
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
console.log(`200: ${pathname}`);
});
});
// Start the server
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
console.log(`Serving files from: ${RESUME_DIR}`);
});

View File

@ -0,0 +1,62 @@
const { test, expect } = require('@playwright/test');
test.describe('Theme Toggle Tests', () => {
test('theme toggle should cycle through modes', async ({ page }) => {
// Serve the site locally for testing
await page.goto('http://localhost:8080');
// Get the theme toggle button
const themeToggle = await page.locator('#themeToggle');
// Verify initial state (should be auto)
let ariaLabel = await themeToggle.getAttribute('aria-label');
expect(ariaLabel).toBe('Theme mode: Auto');
// Click to change to light mode
await themeToggle.click();
ariaLabel = await themeToggle.getAttribute('aria-label');
expect(ariaLabel).toBe('Theme mode: Light');
// Verify data-theme attribute is set to light
const dataTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(dataTheme).toBe('light');
// Click to change to dark mode
await themeToggle.click();
ariaLabel = await themeToggle.getAttribute('aria-label');
expect(ariaLabel).toBe('Theme mode: Dark');
// Verify data-theme attribute is set to dark
const darkDataTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(darkDataTheme).toBe('dark');
// Click to change back to auto mode
await themeToggle.click();
ariaLabel = await themeToggle.getAttribute('aria-label');
expect(ariaLabel).toBe('Theme mode: Auto');
// Verify data-theme attribute is removed
const autoDataTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(autoDataTheme).toBeNull();
});
test('theme toggle should be keyboard accessible', async ({ page }) => {
await page.goto('http://localhost:8080');
// Focus the theme toggle button using Tab
await page.keyboard.press('Tab');
// Verify the button is focused
const isFocused = await page.evaluate(() => {
return document.activeElement.id === 'themeToggle';
});
expect(isFocused).toBeTruthy();
// Activate with space key
await page.keyboard.press('Space');
// Verify it changes to light mode
const ariaLabel = await page.locator('#themeToggle').getAttribute('aria-label');
expect(ariaLabel).toBe('Theme mode: Light');
});
});