forked from colin/resume
2
0
Fork 0

Add Cal.com calendar meeting links

This commit is contained in:
Your Name 2025-04-21 22:14:57 -04:00
parent cd94db9c03
commit 10e340c341
3 changed files with 281 additions and 1 deletions

View File

@ -23,7 +23,8 @@
<div class="container-fluid" role="main">
<h1>Colin Knapp</h1>
<p><strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
<strong>Contact:</strong> <a href="mailto:recruitme2025@colinknapp.com">recruitme2025@colinknapp.com</a> | <a href="https://colinknapp.com">colinknapp.com</a></p>
<strong>Contact:</strong> <a href="mailto:recruitme2025@colinknapp.com">recruitme2025@colinknapp.com</a> | <a href="https://colinknapp.com">colinknapp.com</a><br>
<strong>Schedule a Meeting:</strong> <a href="https://cal.com/colin-/30min" target="_blank">30 Minute Meeting</a> | <a href="https://cal.com/colin-/60-min-meeting" target="_blank">60 Minute Meeting</a></p>
<hr>

194
tests/accessibility.test.js Normal file
View File

@ -0,0 +1,194 @@
const { test, expect } = require('@playwright/test');
const { AxeBuilder } = require('@axe-core/playwright');
const PRODUCTION_URL = 'https://colinknapp.com';
const LOCAL_URL = 'http://localhost:8080';
async function getPageUrl(page) {
try {
// Try production first
await page.goto(PRODUCTION_URL, { timeout: 60000 });
return PRODUCTION_URL;
} catch (error) {
console.log('Production site not available, falling back to local');
await page.goto(LOCAL_URL, { timeout: 60000 });
return LOCAL_URL;
}
}
test.describe('Accessibility Tests', () => {
test('should pass WCAG 2.1 Level AAA standards', async ({ page }) => {
const url = await getPageUrl(page);
console.log(`Running accessibility tests against ${url}`);
// Run axe accessibility tests
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa'])
.analyze();
// Check for any violations
expect(results.violations).toHaveLength(0);
});
test('should have proper ARIA attributes for theme toggle', async ({ page }) => {
const url = await getPageUrl(page);
console.log(`Running ARIA tests against ${url}`);
// Check theme toggle button
const themeToggle = await page.locator('#themeToggle');
expect(await themeToggle.getAttribute('aria-label')).toBe('Theme mode: Auto');
expect(await themeToggle.getAttribute('role')).toBe('switch');
expect(await themeToggle.getAttribute('aria-checked')).toBe('false');
expect(await themeToggle.getAttribute('title')).toBe('Toggle between light, dark, and auto theme modes');
expect(await themeToggle.getAttribute('tabindex')).toBe('0');
});
test('should have proper heading structure', async ({ page }) => {
const url = await getPageUrl(page);
console.log(`Running heading structure tests against ${url}`);
// Check main content area
const mainContent = await page.locator('.container-fluid');
expect(await mainContent.getAttribute('role')).toBe('main');
// Check heading hierarchy
const h1 = await page.locator('h1');
expect(await h1.count()).toBe(1);
const h2s = await page.locator('h2');
expect(await h2s.count()).toBeGreaterThan(0);
});
test('should have working external links', async ({ page, request }) => {
const url = await getPageUrl(page);
console.log(`Running link validation tests against ${url}`);
await page.goto(url, { timeout: 60000 });
await page.waitForLoadState('networkidle');
// Get all external links
const externalLinks = await page.$$('a[href^="http"]:not([href^="http://localhost"])');
// Skip test if no external links found
if (externalLinks.length === 0) {
console.log('No external links found, skipping test');
return;
}
const brokenLinks = [];
for (const link of externalLinks) {
const href = await link.getAttribute('href');
if (!href) continue;
try {
const response = await request.head(href);
if (response.status() >= 400) {
brokenLinks.push({
href,
status: response.status()
});
}
} catch (error) {
brokenLinks.push({
href,
error: error.message
});
}
}
if (brokenLinks.length > 0) {
console.log('\nBroken or inaccessible links:');
brokenLinks.forEach(link => {
if (link.error) {
console.log(`- ${link.href}: ${link.error}`);
} else {
console.log(`- ${link.href}: HTTP ${link.status}`);
}
});
throw new Error('Some external links are broken or inaccessible');
}
});
test('should have proper color contrast', async ({ page }) => {
const url = await getPageUrl(page);
console.log(`Running color contrast tests against ${url}`);
await page.goto(url, { timeout: 60000 });
await page.waitForLoadState('networkidle');
// Check text color contrast in both light and dark modes
const contrastInfo = await page.evaluate(() => {
const getContrastRatio = (color1, color2) => {
const getLuminance = (r, g, b) => {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
};
const parseColor = (color) => {
const rgb = color.match(/\d+/g).map(Number);
return rgb.length === 3 ? rgb : [0, 0, 0];
};
const l1 = getLuminance(...parseColor(color1));
const l2 = getLuminance(...parseColor(color2));
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
return ratio.toFixed(2);
};
const style = getComputedStyle(document.body);
const textColor = style.color;
const backgroundColor = style.backgroundColor;
const contrastRatio = getContrastRatio(textColor, backgroundColor);
return {
textColor,
backgroundColor,
contrastRatio: parseFloat(contrastRatio)
};
});
console.log('Color contrast information:', contrastInfo);
// WCAG 2.1 Level AAA requires a contrast ratio of at least 7:1 for normal text
expect(contrastInfo.contrastRatio).toBeGreaterThanOrEqual(7);
});
test('should have alt text for all images', async ({ page }) => {
const url = await getPageUrl(page);
console.log(`Running image alt text tests against ${url}`);
await page.goto(url, { timeout: 60000 });
await page.waitForLoadState('networkidle');
// Get all images
const images = await page.$$('img');
// Skip test if no images found
if (images.length === 0) {
console.log('No images found, skipping test');
return;
}
const missingAlt = [];
for (const img of images) {
const alt = await img.getAttribute('alt');
const src = await img.getAttribute('src');
// Skip decorative images (empty alt is fine)
const role = await img.getAttribute('role');
if (role === 'presentation' || role === 'none') {
continue;
}
if (!alt) {
missingAlt.push(src);
}
}
if (missingAlt.length > 0) {
console.log('\nImages missing alt text:');
missingAlt.forEach(src => console.log(`- ${src}`));
throw new Error('Some images are missing alt text');
}
});
});

85
tests/headers.test.js Normal file
View File

@ -0,0 +1,85 @@
const { test, expect } = require('@playwright/test');
const PRODUCTION_URL = 'https://colinknapp.com';
const LOCAL_URL = 'http://localhost:8080';
async function getPageUrl(page) {
try {
// Try production first
await page.goto(PRODUCTION_URL, { timeout: 60000 });
return PRODUCTION_URL;
} catch (error) {
console.log('Production site not available, falling back to local');
await page.goto(LOCAL_URL, { timeout: 60000 });
return LOCAL_URL;
}
}
test.describe('Security Headers Tests', () => {
test('should have all required security headers', async ({ page, request }) => {
const url = await getPageUrl(page);
console.log(`Running security header tests against ${url}`);
// Get headers directly from the main page
const response = await request.get(url);
const headers = response.headers();
// Check Content Security Policy
const csp = headers['content-security-policy'];
expect(csp).toBeTruthy();
expect(csp).toContain("default-src 'none'");
expect(csp).toContain("script-src 'self'");
expect(csp).toContain("style-src 'self'");
expect(csp).toContain("img-src 'self' data:");
expect(csp).toContain("font-src 'self' data:");
expect(csp).toContain("connect-src 'self'");
expect(csp).toContain("object-src 'none'");
expect(csp).toContain("frame-ancestors 'none'");
expect(csp).toContain("base-uri 'none'");
expect(csp).toContain("form-action 'none'");
// Check other security headers
expect(headers['x-content-type-options']).toBe('nosniff');
expect(headers['x-frame-options']).toBe('DENY');
expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin');
});
test('should have correct Subresource Integrity (SRI) attributes', async ({ page }) => {
const url = await getPageUrl(page);
console.log(`Running SRI tests against ${url}`);
await page.goto(url, { timeout: 60000 });
await page.waitForLoadState('networkidle');
// Check stylesheet
const stylesheet = await page.locator('link[rel="stylesheet"]').first();
const stylesheetIntegrity = await stylesheet.getAttribute('integrity');
const stylesheetCrossorigin = await stylesheet.getAttribute('crossorigin');
expect(stylesheetIntegrity).toBeTruthy();
expect(stylesheetCrossorigin).toBe('anonymous');
// Check script
const script = await page.locator('script[src]').first();
const scriptIntegrity = await script.getAttribute('integrity');
const scriptCrossorigin = await script.getAttribute('crossorigin');
expect(scriptIntegrity).toBeTruthy();
expect(scriptCrossorigin).toBe('anonymous');
});
test('should have correct caching headers for static assets', async ({ request }) => {
const url = await getPageUrl({ goto: async () => {} });
console.log(`Running caching header tests against ${url}`);
const baseUrl = url.replace(/\/$/, '');
// Check styles.css
const stylesResponse = await request.get(`${baseUrl}/styles.css`);
const stylesCacheControl = stylesResponse.headers()['cache-control'];
expect(stylesCacheControl).toContain('public');
expect(stylesCacheControl).toContain('max-age=');
// Check theme.js
const scriptResponse = await request.get(`${baseUrl}/theme.js`);
const scriptCacheControl = scriptResponse.headers()['cache-control'];
expect(scriptCacheControl).toContain('public');
expect(scriptCacheControl).toContain('max-age=');
});
});