forked from colin/resume
Add Playwright and Lighthouse testing infrastructure
This commit is contained in:
parent
39caf88782
commit
a10eea979f
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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}`);
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue