forked from colin/resume
Add Cal.com calendar meeting links
This commit is contained in:
parent
cd94db9c03
commit
10e340c341
|
@ -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>
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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=');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue