diff --git a/.cursor/rules/deployment-workflow.mdc b/.cursor/rules/deployment-workflow.mdc new file mode 100644 index 0000000..5b696c3 --- /dev/null +++ b/.cursor/rules/deployment-workflow.mdc @@ -0,0 +1,62 @@ +--- +description: +globs: +alwaysApply: false +--- +# Deployment Workflow + +## Testing Requirements +- All changes must be tested locally before deployment +- Tests must pass for both mobile and desktop viewports +- Lighthouse tests must maintain perfect scores (100/100) for accessibility and SEO +- Playwright tests must pass for all viewport sizes + +## Git Operations +- Avoid use direct git commands (`git add`, `git commit`, `git push`) if we do git flows do then as oneliners. +- All git operations must be performed through `./build-test-deploy.sh` +- The script handles: + - Building the Docker container + - Running all tests + - Committing changes + - Pushing to repository + +## Build-Test-Deploy Script +The `./build-test-deploy.sh` script is the single source of truth for deployment: +1. Builds the Docker container +2. Runs the test suite +3. Only proceeds with git operations if tests pass +4. Handles all git operations in the correct order + +## Testing Process +1. Make changes to the codebase +2. Run `./build-test-deploy.sh` to: + - Build the container + - Run tests + - Deploy if tests pass +3. If tests fail: + - Fix the issues + - Run the script again + - Do not proceed with deployment until all tests pass + +## Viewport Testing +The test suite runs against multiple viewport sizes: +- Mobile: 375x667 +- Desktop: 1024x768 + +All tests must pass for all viewport sizes before deployment is allowed. + +## Lighthouse Requirements +Maintain perfect scores: +- Accessibility: 100/100 +- SEO: 100/100 +- Performance: 97/100 minimum +- Best Practices: 93/100 minimum + +## Playwright Tests +Must pass all accessibility tests: +- WCAG 2.1 Level AAA compliance +- ARIA attributes +- Heading structure +- External link validation +- Color contrast +- Image alt text diff --git a/build-test-deploy.sh b/build-test-deploy.sh new file mode 100755 index 0000000..2059749 --- /dev/null +++ b/build-test-deploy.sh @@ -0,0 +1,84 @@ +#!/bin/bash +set -e + +IMAGE_NAME="resume:latest" +CONTAINER_NAME="resume_test_container" +DOCKER_DIR="docker" +RESUME_DIR="$DOCKER_DIR/resume" + +# Recalculate SHA256 hash for styles.css and update integrity in index.html +STYLES_HASH=$(shasum -a 256 $RESUME_DIR/styles.css | awk '{print $1}' | xxd -r -p | base64) +sed -i -E "s|href=\"styles.css\" integrity=\"sha256-[^\"]*\"|href=\"styles.css\" integrity=\"sha256-$STYLES_HASH\"|" $RESUME_DIR/index.html + +# Verify the hash update +if ! grep -q "integrity=\"sha256-$STYLES_HASH\"" $RESUME_DIR/index.html; then + echo "Error: Integrity hash update failed." + exit 1 +fi + +# Test the CSS loading +if ! curl -s http://localhost:8080/styles.css > /dev/null; then + echo "Error: CSS file not accessible." + exit 1 +fi + +# Verify the hash in the container +CONTAINER_HASH=$(docker exec $CONTAINER_NAME cat /srv/styles.css | shasum -a 256 | awk '{print $1}' | xxd -r -p | base64) +if [ "$CONTAINER_HASH" != "$STYLES_HASH" ]; then + echo "Error: Integrity hash mismatch in container." + exit 1 +fi + +# Build Docker image +cd "$DOCKER_DIR" +echo "Building Docker image..." +docker build -t $IMAGE_NAME ./resume/ + +# Stop and remove any previous container +if [ $(docker ps -aq -f name=$CONTAINER_NAME) ]; then + echo "Removing previous test container..." + docker rm -f $CONTAINER_NAME || true +fi + +# Ensure port 8080 is free +echo "Ensuring port 8080 is free..." +lsof -i :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9 || true + +# Run Docker container in the background +echo "Starting Docker container..." +docker run -d --name $CONTAINER_NAME -p 8080:8080 $IMAGE_NAME + +# Wait for the server to be ready +MAX_TRIES=20 +TRIES=0 +until curl -s http://localhost:8080/ > /dev/null; do + TRIES=$((TRIES+1)) + if [ $TRIES -ge $MAX_TRIES ]; then + echo "Server did not start in time." + docker rm -f $CONTAINER_NAME + exit 1 + fi + echo "Waiting for server... ($TRIES/$MAX_TRIES)" + sleep 2 +done + +echo "Server is up. Running tests..." +cd .. + +npm install +npm run setup +if npm test; then + echo "Tests passed. Committing and pushing changes." + git add . + git commit -m "Automated build, test, and deploy" + git push +else + echo "Tests failed. Not deploying." + docker rm -f $CONTAINER_NAME + exit 1 +fi + +echo "Cleaning up Docker container..." +docker rm -f $CONTAINER_NAME + +echo "Done." \ No newline at end of file diff --git a/docker/resume/Caddyfile b/docker/resume/Caddyfile index 957e32c..9f19f31 100644 --- a/docker/resume/Caddyfile +++ b/docker/resume/Caddyfile @@ -1,4 +1,4 @@ -:8080 { +colinknapp.com { root * . file_server encode gzip @@ -10,7 +10,57 @@ -X-Powered-By # HSTS - # Strict-Transport-Security "max-age=31536000; includeSubDomains" + Strict-Transport-Security "max-age=31536000; includeSubDomains" + + # Basic security headers + X-Frame-Options "DENY" + X-Content-Type-Options "nosniff" + Referrer-Policy "strict-origin-when-cross-origin" + + # Permissions policy + Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" + + # Cross-origin isolation headers + Cross-Origin-Embedder-Policy "require-corp" + Cross-Origin-Resource-Policy "same-origin" + Cross-Origin-Opener-Policy "same-origin" + + # Cache control for static assets + Cache-Control "public, max-age=31536000, immutable" + + # CSP with hashes for scripts and styles + Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8='; style-src 'self' 'sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';" + } + + # Handle 404s + handle_errors { + respond "{err.status_code} {err.status_text}" + } + + # Logging + log { + output stdout + format json + } + + # Enable static file serving with caching + file_server { + precompressed + browse + } +} + +# Local development server +:8080 { + root * . + file_server + encode gzip + + # Performance optimizations + header { + # Remove default Caddy headers + -Server + -X-Powered-By # Basic security headers X-Frame-Options "DENY" diff --git a/docker/resume/index.html b/docker/resume/index.html index f2a6b8e..c1b630d 100644 --- a/docker/resume/index.html +++ b/docker/resume/index.html @@ -5,7 +5,7 @@ Colin Knapp Portfolio - + @@ -18,6 +18,13 @@ title="Toggle between light, dark, and auto theme modes" tabindex="0" >🌓 +
@@ -195,5 +202,24 @@

Accessibility: This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.

+ \ No newline at end of file diff --git a/docker/resume/index.html-E b/docker/resume/index.html-E new file mode 100644 index 0000000..c1b630d --- /dev/null +++ b/docker/resume/index.html-E @@ -0,0 +1,225 @@ + + + + + + + Colin Knapp Portfolio + + + + +
+ + +
+ +
+

Colin Knapp

+

Location: Kitchener-Waterloo, Ontario, Canada
+ Contact: recruitme2025@colinknapp.com | colinknapp.com
+ Schedule a Meeting: 30 Minute Meeting

+ +
+ +

Highlights & Measurables

+ + +
+ +

Project Experience

+

DevSecOps at Addis Enterprises

+

Timeframe: 2019-Present
+ Overview: Collaborated on US government projects and airport infrastructure, focusing on scalable, secure systems and domain resilience.
+ Key Contributions:

+ + +

Healthcare Platform Infrastructure

+

Timeframe: 2019-Present
+ Overview: Led infrastructure design and operations for Improving MI Practices (archive) through Addis Enterprises, a critical healthcare education platform.
+ Key Contributions:

+ + +

WordPress Security Automation

+

Timeframe: 2023
+ Overview: Developed an automated solution for WordPress malware removal and hardening.
+ Key Contributions:

+ + +

YouTube Game Development & Cybersecurity

+

Timeframe: 2009-2022
+ Overview: Designed custom video games for prominent online creators, integrating advanced cybersecurity measures.
+ Key Contributions:

+ + +

Web Design & Java Plugin Development

+

Timeframe: 2009-2023
+ Overview: Developed web solutions and Java plugins focusing on CI/CD efficiency and client satisfaction.
+ Key Contributions:

+ + +

App Development for Influencers

+

Timeframe: 2013-2018
+ Overview: Created an ad revenue tracking app to optimize earnings and strategies for content creators.
+ Key Contributions:

+ + +

DevOps & Co-Founder at NitricConcepts

+

Timeframe: 2018-2021
+ Overview: Led a global team in building secure, scalable gaming solutions.
+ Key Contributions:

+ + +

Entrepreneurial Ventures

+

Athion.net Turnaround

+

Timeframe: 2013-2017
+ Overview: Revitalized a struggling business into a self-sustaining operation in two weeks.
+ Key Contributions: Optimized systems and streamlined operations with rapid, effective solutions.
+ Impact: Created a profitable, independent venture.

+ +

MotherboardRepair.ca

+

Timeframe: 2019-Present
+ Overview: Co-founded a company reducing e-waste through circuit board repairs.
+ Key Contributions: Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.
+ Impact: Promoted environmental responsibility in electronics.

+ +

ShowerLoop Project

+

Timeframe: 2016
+ Overview: Revamped the website for an eco-friendly recirculating shower system project, implementing WCAG 2.0 AA compliance and modern design principles.
+ Key Contributions: Designed and implemented a responsive, accessible website with improved user experience and technical documentation.
+ Impact: Enhanced the project's online presence and accessibility while maintaining the site's functionality through periodic maintenance.

+ +
+ +

Additional Information

+

Personal Development

+

Timeframe: 2009-Present

+ + +

Relevant Links & Web Impact

+ + +
+ +
+

Open Source & Infrastructure

+
+

PlotSquared & FastAsyncWorldEdit

+

2013-Present

+

Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.

+
    +
  • Contributed to PlotSquared, a land management plugin with 572+ stars and 809+ forks
  • +
  • Enhanced FastAsyncWorldEdit, improving world manipulation performance with 664+ stars
  • +
  • Implemented security improvements and performance optimizations for large-scale server operations
  • +
+
+
+

Athion.net Infrastructure

+

2013-Present

+

Established and maintained critical infrastructure for Minecraft development community.

+
    +
  • Set up and maintained Jenkins CI/CD pipeline since 2013, supporting continuous integration for game content development
  • +
  • Hosted infrastructure enabling collaboration between developers and Microsoft for game content creation
  • +
  • Implemented robust security measures and performance optimizations for high-traffic development environments
  • +
+
+
+

Software Engineer

+

Oh My Form

+

2020 - Present

+

Led development of Oh My Form, achieving over 1.5 million Docker pulls as verified by Docker Hub and archived.

+
    +
  • Developed and maintained a secure, high-performance form builder application
  • +
  • Implemented robust security measures and best practices
  • +
  • Optimized application performance and user experience
  • +
+
+
+ +
+ +

Accessibility: This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.

+
+ + + \ No newline at end of file diff --git a/docker/resume/styles.css b/docker/resume/styles.css index 7de86da..47629db 100644 --- a/docker/resume/styles.css +++ b/docker/resume/styles.css @@ -144,27 +144,50 @@ hr { position: fixed; top: 20px; right: 20px; - z-index: 100; + z-index: 1000; + display: flex; + gap: 10px; } -#themeToggle { - padding: 8px 16px; - background-color: var(--theme-bg); - border: 1px solid var(--theme-border); - border-radius: 4px; +.theme-switch button { + background: none; + border: none; + font-size: 1.5em; cursor: pointer; - font-size: 14px; - color: var(--text-color); - transition: all 0.3s ease; + padding: 5px; + border-radius: 50%; + transition: background-color 0.3s; } -#themeToggle:hover { - background-color: var(--theme-hover); +.theme-switch button:hover { + background-color: rgba(128, 128, 128, 0.2); } -#themeToggle:focus { - outline: 2px solid var(--accent-color); - outline-offset: 2px; +/* Print styles */ +@media print { + .theme-switch { + display: none; + } + + body { + background: white !important; + color: black !important; + } + + a { + color: black !important; + text-decoration: none !important; + } + + .container-fluid { + max-width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + hr { + border-color: black !important; + } } @media (max-width: 600px) { diff --git a/package-lock.json b/package-lock.json index 435f341..7c26a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,32 @@ "name": "resume", "version": "1.0.0", "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "lighthouse": "^11.6.0", + "playwright": "^1.42.1" + }, "devDependencies": { - "@playwright/test": "^1.42.1", + "@axe-core/playwright": "^4.10.1", + "@playwright/test": "^1.52.0", "chrome-launcher": "^1.1.2", "lighthouse": "^11.4.0", "puppeteer": "^22.4.1" } }, + "node_modules/@axe-core/playwright": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.1.tgz", + "integrity": "sha512-EV5t39VV68kuAfMKqb/RL+YjYKhfuGim9rgIaQ6Vntb2HgaCaau0h98Y3WEUqW1+PbdzxDtDNjFAipbtZuBmEA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.10.2" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -104,13 +123,13 @@ "license": "BSD-3-Clause" }, "node_modules/@playwright/test": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", - "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.51.1" + "playwright": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -334,6 +353,19 @@ "@types/node": "*" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -415,6 +447,12 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -553,6 +591,30 @@ "node": ">=10.0.0" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -588,6 +650,44 @@ "node": "*" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -685,6 +785,27 @@ "node": ">=8" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -695,6 +816,12 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -753,7 +880,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -791,6 +917,25 @@ "node": ">= 14" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1232444", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz", @@ -811,6 +956,26 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -818,6 +983,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -862,6 +1036,36 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -872,6 +1076,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -941,6 +1151,70 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -1004,11 +1278,46 @@ "pend": "~1.2.0" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1019,6 +1328,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1029,6 +1347,43 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -1085,6 +1440,18 @@ "dev": true, "license": "MIT" }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1092,6 +1459,46 @@ "dev": true, "license": "ISC" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-link-header": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", @@ -1190,6 +1597,18 @@ "dev": true, "license": "MIT" }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1245,6 +1664,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/intl-messageformat": { "version": "10.7.16", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", @@ -1272,6 +1697,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -1521,6 +1955,33 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/metaviewport-parser": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", @@ -1528,6 +1989,48 @@ "dev": true, "license": "MIT" }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -1539,9 +2042,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -1552,6 +2063,30 @@ "node": ">= 0.4.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1701,6 +2236,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -1716,13 +2266,12 @@ "license": "ISC" }, "node_modules/playwright": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", - "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", - "dev": true, + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.1" + "playwright-core": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -1735,10 +2284,9 @@ } }, "node_modules/playwright-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", - "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", - "dev": true, + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -1757,6 +2305,19 @@ "node": ">=0.4.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", @@ -1955,6 +2516,45 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1985,6 +2585,32 @@ "node": ">=10.0.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -1995,6 +2621,138 @@ "semver": "bin/semver" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -2111,6 +2869,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/streamx": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", @@ -2221,6 +2988,15 @@ "tldts-core": "^6.1.85" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2228,6 +3004,19 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -2269,6 +3058,15 @@ "node": ">=8" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/urlpattern-polyfill": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", @@ -2276,6 +3074,24 @@ "dev": true, "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 3df0745..abf6a5b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "author": "Colin Knapp", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.42.1", + "@axe-core/playwright": "^4.10.1", + "@playwright/test": "^1.52.0", "chrome-launcher": "^1.1.2", "lighthouse": "^11.4.0", "puppeteer": "^22.4.1" diff --git a/tests/accessibility.test.js b/tests/accessibility.test.js index b895c8e..e8290b9 100644 --- a/tests/accessibility.test.js +++ b/tests/accessibility.test.js @@ -4,6 +4,11 @@ const { AxeBuilder } = require('@axe-core/playwright'); const PRODUCTION_URL = 'https://colinknapp.com'; const LOCAL_URL = 'http://localhost:8080'; +const viewports = [ + { width: 375, height: 667, name: 'mobile' }, + { width: 1024, height: 768, name: 'desktop' } +]; + async function getPageUrl(page) { try { // Try production first @@ -16,179 +21,202 @@ async function getPageUrl(page) { } } -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}`); +viewports.forEach(viewport => { + test.describe(`Accessibility Tests (${viewport.name})`, () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(viewport); + }); - // 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 pass WCAG 2.1 Level AAA standards', async ({ page }) => { + const url = await getPageUrl(page); + console.log(`Running accessibility tests against ${url} on ${viewport.name}`); + + // 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}`); + const url = await getPageUrl(page); + console.log(`Running ARIA tests against ${url} on ${viewport.name}`); - // 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'); + // 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}`); + const url = await getPageUrl(page); + console.log(`Running heading structure tests against ${url} on ${viewport.name}`); - // Check main content area - const mainContent = await page.locator('.container-fluid'); - expect(await mainContent.getAttribute('role')).toBe('main'); + // 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); + // 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'); + const url = await getPageUrl(page); + console.log(`Running link validation tests against ${url} on ${viewport.name}`); + 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; - } + // 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; + // Set a longer timeout for external link checks + test.setTimeout(120000); - 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 - }); + const brokenLinks = []; + for (const link of externalLinks) { + const href = await link.getAttribute('href'); + if (!href) continue; + + let attempts = 0; + const maxAttempts = 3; + let success = false; + while (attempts < maxAttempts && !success) { + attempts++; + try { + const response = await request.head(href, { timeout: 15000 }); + if (response.status() >= 400) { + brokenLinks.push({ + href, + status: response.status(), + attempt: attempts + }); + } else { + success = true; } + } catch (error) { + if (attempts === maxAttempts) { + brokenLinks.push({ + href, + error: error.message, + attempt: attempts + }); + } + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, 2000)); + } } + } - 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'); - } + if (brokenLinks.length > 0) { + console.log('\nBroken or inaccessible links:'); + brokenLinks.forEach(link => { + if (link.error) { + console.log(`- ${link.href}: ${link.error} (Attempt ${link.attempt}/${maxAttempts})`); + } else { + console.log(`- ${link.href}: HTTP ${link.status} (Attempt ${link.attempt}/${maxAttempts})`); + } + }); + 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'); + const url = await getPageUrl(page); + console.log(`Running color contrast tests against ${url} on ${viewport.name}`); + 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; - }; + // 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 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 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); + const style = getComputedStyle(document.body); + const textColor = style.color; + const backgroundColor = style.backgroundColor; + const contrastRatio = getContrastRatio(textColor, backgroundColor); - return { - textColor, - backgroundColor, - contrastRatio: parseFloat(contrastRatio) - }; - }); + 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); + 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'); + const url = await getPageUrl(page); + console.log(`Running image alt text tests against ${url} on ${viewport.name}`); + await page.goto(url, { timeout: 60000 }); + await page.waitForLoadState('networkidle'); - // Get all images - const images = await page.$$('img'); + // 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 test if no images found - if (images.length === 0) { - console.log('No images found, skipping test'); - return; + // Skip decorative images (empty alt is fine) + const role = await img.getAttribute('role'); + if (role === 'presentation' || role === 'none') { + continue; } - 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 (!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'); - } + 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'); + } }); + }); }); \ No newline at end of file diff --git a/tests/headers.spec.js b/tests/headers.spec.js index a50599a..3082c94 100644 --- a/tests/headers.spec.js +++ b/tests/headers.spec.js @@ -2,60 +2,80 @@ const { test, expect } = require('@playwright/test'); test.describe('Security Headers Tests', () => { test('should have all required security headers', async ({ page }) => { - // Navigate to the page - await page.goto('http://localhost:8080'); + try { + // Navigate to the page with a timeout + await page.goto('http://localhost:8080', { timeout: 30000 }); - // Get response headers - const response = await page.waitForResponse('http://localhost:8080'); - const headers = response.headers(); + // Get response headers + const response = await page.waitForResponse('http://localhost:8080', { timeout: 30000 }); + const headers = response.headers(); - // Define required headers and their expected values - const requiredHeaders = { - 'Content-Security-Policy': expect.stringContaining("default-src 'self'"), - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Permissions-Policy': expect.stringContaining('geolocation=()'), - 'Strict-Transport-Security': expect.stringContaining('max-age=31536000'), - }; + // Define required headers and their expected values + const requiredHeaders = { + 'Content-Security-Policy': expect.stringContaining("default-src 'self'"), + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Permissions-Policy': expect.stringContaining('geolocation=()'), + 'Strict-Transport-Security': expect.stringContaining('max-age=31536000'), + }; - // Check each required header - for (const [header, expectedValue] of Object.entries(requiredHeaders)) { - const headerValue = headers[header.toLowerCase()]; - expect(headerValue).toBeDefined(); - if (typeof expectedValue === 'string') { - expect(headerValue).toBe(expectedValue); - } else { - expect(headerValue).toMatch(expectedValue); + // Check each required header + for (const [header, expectedValue] of Object.entries(requiredHeaders)) { + const headerValue = headers[header.toLowerCase()]; + console.log(`Checking header: ${header}, Value: ${headerValue}`); + expect(headerValue, `${header} is not defined`).toBeDefined(); + if (typeof expectedValue === 'string') { + expect(headerValue, `${header} does not match expected value`).toBe(expectedValue); + } else { + expect(headerValue, `${header} does not match expected pattern`).toMatch(expectedValue); + } } + } catch (error) { + console.error('Error in security headers test:', error); + throw new Error(`Failed to load local server for security headers test: ${error.message}`); } }); - test('should have correct CSP directives with nonce and hash', async ({ page }) => { - await page.goto('http://localhost:8080'); - const response = await page.waitForResponse('http://localhost:8080'); - const headers = response.headers(); - const csp = headers['content-security-policy']; + test('should have correct CSP directives with hash', async ({ page }) => { + try { + await page.goto('http://localhost:8080', { timeout: 30000 }); + const response = await page.waitForResponse('http://localhost:8080', { timeout: 30000 }); + const headers = response.headers(); + const csp = headers['content-security-policy']; - // Check for essential CSP directives - expect(csp).toContain("default-src 'self'"); - expect(csp).toContain("script-src 'self' 'nonce-"); - expect(csp).toContain("'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='"); - expect(csp).toContain("style-src 'self' 'unsafe-inline'"); - expect(csp).toContain("img-src 'self' data: https: http:"); - expect(csp).toContain("font-src 'self'"); - expect(csp).toContain("connect-src 'self'"); + // Check for essential CSP directives + console.log('CSP Header:', csp); + expect(csp, 'CSP header is not defined').toBeDefined(); + expect(csp, 'CSP does not contain default-src directive').toContain("default-src 'none'"); + expect(csp, 'CSP does not contain script-src with hash').toContain("script-src 'self' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='"); + expect(csp, 'CSP does not contain style-src with hash').toContain("style-src 'self' 'sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI='"); + expect(csp, 'CSP does not contain img-src directive').toContain("img-src 'self' data:"); + expect(csp, 'CSP does not contain font-src directive').toContain("font-src 'self' data:"); + expect(csp, 'CSP does not contain connect-src directive').toContain("connect-src 'self'"); + } catch (error) { + console.error('Error in CSP directives test:', error); + throw new Error(`Failed to load local server for CSP test: ${error.message}`); + } }); - test('should have nonce attributes on script tags', async ({ page }) => { - await page.goto('http://localhost:8080'); - - // Check that all script tags have nonce attributes - const scripts = await page.$$('script'); - for (const script of scripts) { - const hasNonce = await script.evaluate(el => el.hasAttribute('nonce')); - expect(hasNonce).toBeTruthy(); + test('should have integrity attributes on script tags', async ({ page }) => { + try { + await page.goto('http://localhost:8080', { timeout: 30000 }); + + // Check that all script tags have integrity attributes + const scripts = await page.$$('script'); + console.log(`Found ${scripts.length} script tags`); + for (const script of scripts) { + const hasIntegrity = await script.evaluate(el => el.hasAttribute('integrity')); + const src = await script.evaluate(el => el.getAttribute('src') || 'inline script'); + console.log(`Script ${src} has integrity: ${hasIntegrity}`); + expect(hasIntegrity, `Script tag ${src} missing integrity attribute`).toBeTruthy(); + } + } catch (error) { + console.error('Error in integrity attributes test:', error); + throw new Error(`Failed to load local server for integrity test: ${error.message}`); } }); }); \ No newline at end of file