diff --git a/.github/workflows/e2e-testing.yml.example b/.github/workflows/e2e-testing.yml.example
new file mode 100644
index 0000000..5f519d6
--- /dev/null
+++ b/.github/workflows/e2e-testing.yml.example
@@ -0,0 +1,497 @@
+name: E2E Testing Suite (Screenshots & Accessibility)
+
+concurrency:
+ group: ${{ github.event_name == 'pull_request' && format('e2e-pr-{0}', github.event.pull_request.number) || format('e2e-{0}', github.ref) }}
+ cancel-in-progress: false # Don't cancel immediately - wait for timeout
+
+permissions:
+ contents: write # needed to push to gh-pages
+ pages: write # deploy with actions/deploy-pages
+ id-token: write # required by actions/deploy-pages
+ pull-requests: write
+ issues: write
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ e2e-tests:
+ name: E2E Tests (Screenshots & A11y)
+ runs-on: ubuntu-latest
+ env:
+ PORT: 4173 # Port for preview server and Playwright tests
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # Simple notification for PR-specific concurrency
+ - name: PR Test Info
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prNumber = context.payload.pull_request.number;
+ core.info(`Starting E2E tests for PR ${prNumber} with isolated concurrency group.`);
+ core.info(`This PR will publish screenshots to: /(Dev-Shots)/PR-${prNumber}/`);
+
+ - name: Use Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install deps
+ run: npm ci
+
+ - name: Build site
+ run: npm run build
+
+ # --- Added steps to start and wait for the preview server ---
+ - name: Start preview server
+ run: npm run preview -- --port $PORT &
+
+ - name: Wait for server to be ready
+ run: npx wait-on http://localhost:$PORT
+ # -----------------------------------------------------------
+
+ - name: Cache Playwright browsers
+ id: playwright-cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ run: npx playwright install --with-deps
+
+ - name: Run Playwright tests (screenshots & accessibility)
+ run: |
+ npx playwright test tests/e2e.spec.ts tests/a11y.spec.ts
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-test-results
+ path: |
+ playwright-logs/portfolio-web-light.png
+ playwright-logs/portfolio-web-dark.png
+ playwright-logs/portfolio-mobile-light.png
+ playwright-logs/portfolio-mobile-dark.png
+ playwright-report/**
+ axe-report.json
+
+ - name: Add screenshot to job summary
+ run: |
+ web_light="playwright-logs/portfolio-web-light.png"
+ web_dark="playwright-logs/portfolio-web-dark.png"
+ mobile_light="playwright-logs/portfolio-mobile-light.png"
+ mobile_dark="playwright-logs/portfolio-mobile-dark.png"
+
+ has_any=false
+ if [ -f "$web_light" ] || [ -f "$web_dark" ] || [ -f "$mobile_light" ] || [ -f "$mobile_dark" ]; then
+ has_any=true
+ fi
+
+ if [ "$has_any" = true ]; then
+ {
+ echo "## Web Render"
+ if [ -f "$web_light" ] || [ -f "$web_dark" ]; then
+ echo
+ echo "### Web"
+ if [ -f "$web_light" ]; then
+ echo
+ echo "#### Light"
+ echo
+ echo ""
+ fi
+ if [ -f "$web_dark" ]; then
+ echo
+ echo "#### Dark"
+ echo
+ echo ""
+ fi
+ fi
+ if [ -f "$mobile_light" ] || [ -f "$mobile_dark" ]; then
+ echo
+ echo "### Mobile"
+ if [ -f "$mobile_light" ]; then
+ echo
+ echo "#### Light"
+ echo
+ echo ""
+ fi
+ if [ -f "$mobile_dark" ]; then
+ echo
+ echo "#### Dark"
+ echo
+ echo ""
+ fi
+ fi
+ echo
+ echo "_This screenshot was generated by Playwright during the latest workflow run._"
+ } >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "⚠️ Screenshots not found." >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ - name: Prepare Pages payload and workspace archive
+ id: prep_pages
+ run: |
+ # Use PR-specific directory structure instead of run-based
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ pr_number="${{ github.event.pull_request.number }}"
+ pages_root="_screenshot_publish/(Dev-Shots)/PR-${pr_number}"
+ repo_root="(Dev-Shots)/PR-${pr_number}"
+ else
+ # For main branch, use run-based structure
+ run_dir=${{ github.run_id }}
+ pages_root="_screenshot_publish/(Dev-Shots)/screenshots/$run_dir"
+ repo_root="(Dev-Shots)/screenshots/$run_dir"
+ fi
+
+ rm -rf _screenshot_publish
+ mkdir -p "$pages_root"
+ mkdir -p "$repo_root"
+
+ files=(
+ "portfolio-web-light.png"
+ "portfolio-web-dark.png"
+ "portfolio-mobile-light.png"
+ "portfolio-mobile-dark.png"
+ )
+
+ found=false
+ found_web=false
+ found_mobile=false
+ found_web_light=false
+ found_web_dark=false
+ found_mobile_light=false
+ found_mobile_dark=false
+
+ for file in "${files[@]}"; do
+ src="playwright-logs/$file"
+ if [ -f "$src" ]; then
+ cp "$src" "$pages_root/$file"
+ cp "$src" "$repo_root/$file"
+ found=true
+
+ case "$file" in
+ portfolio-web-light.png)
+ found_web=true
+ found_web_light=true
+ ;;
+ portfolio-web-dark.png)
+ found_web=true
+ found_web_dark=true
+ ;;
+ portfolio-mobile-light.png)
+ found_mobile=true
+ found_mobile_light=true
+ ;;
+ portfolio-mobile-dark.png)
+ found_mobile=true
+ found_mobile_dark=true
+ ;;
+ esac
+ fi
+ done
+
+ if [ "$found" = true ]; then
+ echo "found=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$found_web" = true ]; then
+ echo "found_web=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found_web=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$found_mobile" = true ]; then
+ echo "found_mobile=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found_mobile=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$found_web_light" = true ]; then
+ echo "found_web_light=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found_web_light=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$found_web_dark" = true ]; then
+ echo "found_web_dark=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found_web_dark=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$found_mobile_light" = true ]; then
+ echo "found_mobile_light=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found_mobile_light=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$found_mobile_dark" = true ]; then
+ echo "found_mobile_dark=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "found_mobile_dark=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ # Store the repo root for use in later steps (remove the leading parentheses part)
+ repo_root_clean="${repo_root#(Dev-Shots)/}"
+ echo "repo_root=$repo_root_clean" >> "$GITHUB_OUTPUT"
+
+ # Publish screenshot via GitHub Pages for same-repo runs/PRs
+ - name: Configure GitHub Pages
+ if: steps.prep_pages.outputs.found == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
+ uses: actions/configure-pages@v4
+
+ - name: Upload screenshot to Pages artifact
+ if: steps.prep_pages.outputs.found == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: _screenshot_publish
+ name: github-pages-screenshot-${{ github.run_id }}
+
+ - name: Deploy GitHub Pages site
+ id: deploy
+ if: steps.prep_pages.outputs.found == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
+ uses: actions/deploy-pages@v4
+ with:
+ preview: ${{ github.event_name == 'pull_request' }}
+ artifact_name: github-pages-screenshot-${{ github.run_id }}
+
+ - name: Add GitHub Pages link to job summary
+ if: steps.prep_pages.outputs.found == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
+ run: |
+ page_url="${{ steps.deploy.outputs.preview_url }}"
+ if [ -z "$page_url" ]; then
+ page_url="${{ steps.deploy.outputs.page_url }}"
+ fi
+
+ if [ -n "$page_url" ]; then
+ base_url="${page_url%/}/(Dev-Shots)"
+ repo_root="${{ steps.prep_pages.outputs.repo_root }}"
+ web_found="${{ steps.prep_pages.outputs.found_web }}"
+ web_light_found="${{ steps.prep_pages.outputs.found_web_light }}"
+ web_dark_found="${{ steps.prep_pages.outputs.found_web_dark }}"
+ mobile_found="${{ steps.prep_pages.outputs.found_mobile }}"
+ mobile_light_found="${{ steps.prep_pages.outputs.found_mobile_light }}"
+ mobile_dark_found="${{ steps.prep_pages.outputs.found_mobile_dark }}"
+ {
+ echo "### GitHub Pages Screenshots"
+ echo
+ if [ "$web_found" = 'true' ]; then
+ if [ "$web_light_found" = 'true' ]; then
+ echo "- [View Web · Light](${base_url}/${repo_root}/portfolio-web-light.png)"
+ fi
+ if [ "$web_dark_found" = 'true' ]; then
+ echo "- [View Web · Dark](${base_url}/${repo_root}/portfolio-web-dark.png)"
+ fi
+ fi
+ if [ "$mobile_found" = 'true' ]; then
+ if [ "$mobile_light_found" = 'true' ]; then
+ echo "- [View Mobile · Light](${base_url}/${repo_root}/portfolio-mobile-light.png)"
+ fi
+ if [ "$mobile_dark_found" = 'true' ]; then
+ echo "- [View Mobile · Dark](${base_url}/${repo_root}/portfolio-mobile-dark.png)"
+ fi
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ # Comment inline (same-repo PRs only)
+ - name: Comment on PR with inline screenshot (Pages URL)
+ if: steps.prep_pages.outputs.found == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
+ uses: actions/github-script@v7
+ env:
+ PAGE_URL: ${{ steps.deploy.outputs.page_url }}
+ PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
+ WEB_FOUND: ${{ steps.prep_pages.outputs.found_web }}
+ MOBILE_FOUND: ${{ steps.prep_pages.outputs.found_mobile }}
+ WEB_LIGHT_FOUND: ${{ steps.prep_pages.outputs.found_web_light }}
+ WEB_DARK_FOUND: ${{ steps.prep_pages.outputs.found_web_dark }}
+ MOBILE_LIGHT_FOUND: ${{ steps.prep_pages.outputs.found_mobile_light }}
+ MOBILE_DARK_FOUND: ${{ steps.prep_pages.outputs.found_mobile_dark }}
+ REPO_ROOT: ${{ steps.prep_pages.outputs.repo_root }}
+ with:
+ script: |
+ const prNumber = context.issue.number;
+ const baseUrl = (process.env.PREVIEW_URL || process.env.PAGE_URL || '').replace(/\/$/, '');
+ if (!baseUrl) {
+ core.info('Pages URL missing; skipping comment.');
+ return;
+ }
+ const siteBaseUrl = `${baseUrl}/(Dev-Shots)`;
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const issueNumber = context.issue.number;
+ const marker = '';
+ const commitSha = context.sha.slice(0, 7);
+ const commitUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${context.sha}`;
+ const webFound = process.env.WEB_FOUND === 'true';
+ const mobileFound = process.env.MOBILE_FOUND === 'true';
+ const webLightFound = process.env.WEB_LIGHT_FOUND === 'true';
+ const webDarkFound = process.env.WEB_DARK_FOUND === 'true';
+ const mobileLightFound = process.env.MOBILE_LIGHT_FOUND === 'true';
+ const mobileDarkFound = process.env.MOBILE_DARK_FOUND === 'true';
+ const repoRoot = process.env.REPO_ROOT;
+
+ const sections = [
+ marker,
+ '**Web Render**',
+ '',
+ `Commit: [\`${commitSha}\`](${commitUrl})`,
+ ''
+ ];
+
+ if (webFound) {
+ const webLightUrl = `${siteBaseUrl}/${repoRoot}/portfolio-web-light.png`;
+ const webDarkUrl = `${siteBaseUrl}/${repoRoot}/portfolio-web-dark.png`;
+ const webContent = [];
+
+ if (webLightFound || webDarkFound) {
+ webContent.push('
');
+ if (webLightFound) {
+ webContent.push(
+ `| ` +
+ ` Light ` +
+ ` ` +
+ ` | `
+ );
+ }
+ if (webDarkFound) {
+ webContent.push(
+ `` +
+ ` Dark ` +
+ ` ` +
+ ` | `
+ );
+ }
+ webContent.push('
', '');
+ } else {
+ webContent.push('_Web render unavailable for this run._', '');
+ }
+
+ sections.push('', 'View Web
', '', ...webContent, ' ', '');
+ }
+
+ if (mobileFound) {
+ const mobileLightUrl = `${siteBaseUrl}/${repoRoot}/portfolio-mobile-light.png`;
+ const mobileDarkUrl = `${siteBaseUrl}/${repoRoot}/portfolio-mobile-dark.png`;
+ const mobileContent = [];
+
+ if (mobileLightFound || mobileDarkFound) {
+ mobileContent.push('');
+ if (mobileLightFound) {
+ mobileContent.push(
+ `| ` +
+ ` Light ` +
+ ` ` +
+ ` | `
+ );
+ }
+ if (mobileDarkFound) {
+ mobileContent.push(
+ `` +
+ ` Dark ` +
+ ` ` +
+ ` | `
+ );
+ }
+ mobileContent.push('
', '');
+ } else {
+ mobileContent.push('_Mobile render unavailable for this run._', '');
+ }
+
+ sections.push('', 'View Mobile
', '', ...mobileContent, ' ', '');
+ }
+
+ if (!webFound && !mobileFound) {
+ sections.push('_No screenshots available for this run._', '');
+ }
+
+ sections.push('_This screenshot was generated by Playwright during the latest workflow run._');
+
+ const body = sections.join('\n');
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number: issueNumber,
+ });
+
+ // Check for existing screenshot comments
+ const existing = comments.find((comment) => comment.body && comment.body.includes(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner,
+ repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body,
+ });
+ }
+
+ # Fallback for forked PRs (can't push to gh-pages)
+ - name: Comment on PR (fork fallback)
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
+ const marker = '';
+ const commitSha = context.sha.slice(0, 7);
+ const commitUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${context.sha}`;
+ const body = [
+ marker,
+ '**Web Render**',
+ '',
+ `Commit: [\`${commitSha}\`](${commitUrl})`,
+ '',
+ 'Inline image is unavailable for forked PRs due to token restrictions.',
+ '',
+ `Please view the Light & Dark captures in the [workflow run summary](${runUrl}).`,
+ '',
+ '_This screenshot was generated by Playwright during the latest workflow run._'
+ ].join('\n');
+
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const issueNumber = context.issue.number;
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number: issueNumber,
+ });
+
+ const existing = comments.find((comment) => comment.body && comment.body.includes(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner,
+ repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body,
+ });
+ }
diff --git a/README.md b/README.md
index fd61f75..666a346 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,391 @@
-# E2E
-Github Action that takes a screenshot of your website PR in both mobile and dark mode
+# E2E Testing Suite (Screenshots & Accessibility)
+
+A comprehensive GitHub Action that automatically captures screenshots of your website in multiple modes (web/mobile, light/dark) and performs accessibility testing using Playwright.
+
+## 🌟 Features
+
+- **📸 Automatic Screenshot Capture**: Takes screenshots in web and mobile viewports
+- **🌓 Light & Dark Mode Support**: Captures both light and dark theme variations
+- **♿ Accessibility Testing**: Runs Axe accessibility tests to ensure WCAG compliance
+- **📊 GitHub Pages Integration**: Publishes screenshots to GitHub Pages with preview URLs
+- **💬 PR Comments**: Automatically posts screenshots directly in pull request comments
+- **🔄 Concurrency Control**: Smart concurrency management for PRs and main branch
+- **🍴 Fork-Friendly**: Special handling for forked repository PRs
+- **📦 Artifact Archiving**: Uploads test results and reports as workflow artifacts
+
+## 🚀 Usage
+
+### Option 1: Use as a GitHub Action (Recommended)
+
+Add this to your workflow file (`.github/workflows/e2e.yml`):
+
+```yaml
+name: E2E Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run E2E Tests
+ uses: SillyLittleTech/E2E@v1
+ with:
+ port: '4173'
+ node-version: '20'
+ build-command: 'npm run build'
+ preview-command: 'npm run preview'
+```
+
+### Option 2: Copy the Complete Workflow
+
+### Option 2: Copy the Complete Workflow
+
+For more control and GitHub Pages integration, copy the full workflow file to your repository:
+
+1. **Copy the example workflow file** from this repository to yours:
+ ```bash
+ # Copy .github/workflows/e2e-testing.yml.example to your repository as:
+ .github/workflows/e2e-testing.yml
+ ```
+
+2. **Set up required dependencies** in your project:
+ ```bash
+ npm install --save-dev playwright @axe-core/playwright wait-on
+ ```
+
+3. **Create Playwright test files** in your repository:
+ - `tests/e2e.spec.ts` - For screenshot capture tests
+ - `tests/a11y.spec.ts` - For accessibility tests
+
+4. **Configure your `package.json`** with required scripts:
+ ```json
+ {
+ "scripts": {
+ "build": "your-build-command",
+ "preview": "your-preview-server-command"
+ }
+ }
+ ```
+
+5. **Enable GitHub Pages** in your repository settings:
+ - Go to Settings > Pages
+ - Source: GitHub Actions
+
+### Prerequisites
+
+Before using this action, ensure your project has:
+
+- **Node.js project** with `package.json`
+- **Build script** defined (e.g., `npm run build`)
+- **Preview server script** defined (e.g., `npm run preview`)
+- **Playwright tests** in your repository
+
+Install required dependencies:
+```bash
+npm install --save-dev playwright @axe-core/playwright wait-on
+```
+
+## 📋 Action Inputs
+
+| Input | Description | Required | Default |
+|-------|-------------|----------|---------|
+| `port` | Port for the preview server and Playwright tests | No | `4173` |
+| `node-version` | Node.js version to use | No | `20` |
+| `build-command` | Command to build the site | No | `npm run build` |
+| `preview-command` | Command to start the preview server | No | `npm run preview` |
+| `test-files` | Playwright test files to run (space-separated) | No | `tests/e2e.spec.ts tests/a11y.spec.ts` |
+| `screenshot-prefix` | Prefix for screenshot filenames | No | `portfolio` |
+
+### Example with Custom Inputs
+
+```yaml
+- name: Run E2E Tests
+ uses: SillyLittleTech/E2E@v1
+ with:
+ port: '3000'
+ node-version: '18'
+ build-command: 'npm run build:prod'
+ preview-command: 'npm run serve'
+ test-files: 'tests/*.spec.ts'
+ screenshot-prefix: 'my-app'
+```
+
+## 📝 Example Playwright Test Files
+
+#### `tests/e2e.spec.ts` (Screenshot Capture)
+```typescript
+import { test, expect } from '@playwright/test';
+
+test.describe('Screenshot Capture', () => {
+ test('capture web light mode', async ({ page }) => {
+ await page.goto('http://localhost:4173');
+ await page.emulateMedia({ colorScheme: 'light' });
+ await page.screenshot({
+ path: 'playwright-logs/portfolio-web-light.png',
+ fullPage: true
+ });
+ });
+
+ test('capture web dark mode', async ({ page }) => {
+ await page.goto('http://localhost:4173');
+ await page.emulateMedia({ colorScheme: 'dark' });
+ await page.screenshot({
+ path: 'playwright-logs/portfolio-web-dark.png',
+ fullPage: true
+ });
+ });
+
+ test('capture mobile light mode', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.goto('http://localhost:4173');
+ await page.emulateMedia({ colorScheme: 'light' });
+ await page.screenshot({
+ path: 'playwright-logs/portfolio-mobile-light.png',
+ fullPage: true
+ });
+ });
+
+ test('capture mobile dark mode', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.goto('http://localhost:4173');
+ await page.emulateMedia({ colorScheme: 'dark' });
+ await page.screenshot({
+ path: 'playwright-logs/portfolio-mobile-dark.png',
+ fullPage: true
+ });
+ });
+});
+```
+
+#### `tests/a11y.spec.ts` (Accessibility Testing)
+```typescript
+import { test, expect } from '@playwright/test';
+import AxeBuilder from '@axe-core/playwright';
+import fs from 'fs';
+
+test.describe('Accessibility Tests', () => {
+ test('should not have any automatically detectable accessibility issues', async ({ page }) => {
+ await page.goto('http://localhost:4173');
+
+ const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
+
+ // Save report
+ fs.writeFileSync('axe-report.json', JSON.stringify(accessibilityScanResults, null, 2));
+
+ expect(accessibilityScanResults.violations).toEqual([]);
+ });
+});
+```
+
+## ⚙️ Configuration
+
+### Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `PORT` | `4173` | Port for the preview server and Playwright tests |
+
+You can customize the port by modifying the `env` section in the workflow:
+
+```yaml
+env:
+ PORT: 4173 # Change to your preferred port
+```
+
+### Workflow Triggers
+
+The workflow is triggered on:
+- **Push to main branch**: Runs tests and publishes to GitHub Pages
+- **Pull requests**: Runs tests, posts screenshots as PR comments
+
+```yaml
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+```
+
+### Required Permissions
+
+The workflow requires the following permissions:
+
+```yaml
+permissions:
+ contents: write # needed to push to gh-pages
+ pages: write # deploy with actions/deploy-pages
+ id-token: write # required by actions/deploy-pages
+ pull-requests: write # comment on PRs
+ issues: write # comment on issues
+```
+
+### Concurrency Groups
+
+The workflow uses smart concurrency management:
+
+- **Pull Requests**: Each PR gets its own concurrency group (`e2e-pr-{PR_NUMBER}`)
+- **Main Branch**: Uses the branch ref as concurrency group (`e2e-{REF}`)
+- **Cancel in Progress**: Disabled to allow tests to complete
+
+```yaml
+concurrency:
+ group: ${{ github.event_name == 'pull_request' && format('e2e-pr-{0}', github.event.pull_request.number) || format('e2e-{0}', github.ref) }}
+ cancel-in-progress: false
+```
+
+## 📁 Output Structure
+
+### Screenshot Locations
+
+Screenshots are saved in the following structure:
+
+**For Pull Requests:**
+```
+/(Dev-Shots)/PR-{PR_NUMBER}/
+ ├── portfolio-web-light.png
+ ├── portfolio-web-dark.png
+ ├── portfolio-mobile-light.png
+ └── portfolio-mobile-dark.png
+```
+
+**For Main Branch:**
+```
+/(Dev-Shots)/screenshots/{RUN_ID}/
+ ├── portfolio-web-light.png
+ ├── portfolio-web-dark.png
+ ├── portfolio-mobile-light.png
+ └── portfolio-mobile-dark.png
+```
+
+### Artifacts
+
+The workflow uploads the following artifacts:
+
+- `e2e-test-results` containing:
+ - All screenshot PNG files
+ - Playwright HTML report (`playwright-report/**`)
+ - Axe accessibility report (`axe-report.json`)
+
+## 📝 Workflow Behavior
+
+### For Pull Requests (Same Repository)
+
+1. Runs E2E and accessibility tests
+2. Captures screenshots in all modes
+3. Publishes screenshots to GitHub Pages with preview URL
+4. Posts an inline comment on the PR with embedded screenshots
+5. Updates the comment on subsequent commits
+
+### For Pull Requests (Forked Repository)
+
+1. Runs E2E and accessibility tests
+2. Captures screenshots in all modes
+3. Uploads screenshots as workflow artifacts
+4. Posts a comment with link to workflow run summary
+5. ⚠️ **Note**: Cannot publish to GitHub Pages due to token restrictions
+
+### For Main Branch Pushes
+
+1. Runs E2E and accessibility tests
+2. Captures screenshots in all modes
+3. Publishes screenshots to GitHub Pages
+4. Adds links to job summary
+
+## 🎯 Customization
+
+### Changing Screenshot Names
+
+Modify the file names in the workflow:
+
+```yaml
+- name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-test-results
+ path: |
+ playwright-logs/your-custom-name-light.png
+ playwright-logs/your-custom-name-dark.png
+```
+
+### Changing Node Version
+
+Update the Node.js version:
+
+```yaml
+- name: Use Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20' # Change to your preferred version
+```
+
+### Adding Custom Test Files
+
+Add more test files to the Playwright test command:
+
+```yaml
+- name: Run Playwright tests
+ run: |
+ npx playwright test tests/e2e.spec.ts tests/a11y.spec.ts tests/custom.spec.ts
+```
+
+### Customizing Build and Preview Commands
+
+Update the commands to match your project:
+
+```yaml
+- name: Build site
+ run: npm run build # Change to your build command
+
+- name: Start preview server
+ run: npm run preview -- --port $PORT & # Change to your preview command
+```
+
+## 🔍 Troubleshooting
+
+### Screenshots Not Generated
+
+- Ensure your Playwright tests save screenshots to `playwright-logs/` directory
+- Check that the file names match the expected pattern
+- Verify the preview server is running on the correct port
+
+### Preview Server Not Starting
+
+- Ensure your `package.json` has a `preview` script
+- Check that the port is not already in use
+- Verify the build output is in the correct location
+
+### Accessibility Tests Failing
+
+- Review the `axe-report.json` artifact for violation details
+- Fix accessibility issues in your application
+- Temporarily adjust Axe rules if needed (but fix issues ASAP)
+
+### PR Comments Not Appearing
+
+- Verify workflow has `pull-requests: write` permission
+- Check that the workflow completed successfully
+- For forked PRs, comments are limited - check workflow artifacts
+
+### GitHub Pages Not Deploying
+
+- Enable GitHub Pages in repository settings
+- Ensure workflow has required permissions (`pages: write`, `id-token: write`)
+- Check the Pages settings are set to "GitHub Actions" as source
+
+## 📚 Additional Resources
+
+- [Playwright Documentation](https://playwright.dev)
+- [Axe Core Playwright Integration](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright)
+- [GitHub Actions Documentation](https://docs.github.com/en/actions)
+- [GitHub Pages Documentation](https://docs.github.com/en/pages)
+
+## 📄 License
+
+See [LICENSE](LICENSE) file for details.
diff --git a/action.yml b/action.yml
new file mode 100644
index 0000000..4b1bde6
--- /dev/null
+++ b/action.yml
@@ -0,0 +1,141 @@
+name: 'E2E Testing Suite'
+description: 'Automatically capture screenshots of your website in web/mobile and light/dark modes, plus accessibility testing with Playwright'
+author: 'SillyLittleTech'
+
+branding:
+ icon: 'camera'
+ color: 'purple'
+
+inputs:
+ port:
+ description: 'Port for the preview server and Playwright tests'
+ required: false
+ default: '4173'
+ node-version:
+ description: 'Node.js version to use'
+ required: false
+ default: '20'
+ build-command:
+ description: 'Command to build the site'
+ required: false
+ default: 'npm run build'
+ preview-command:
+ description: 'Command to start the preview server'
+ required: false
+ default: 'npm run preview'
+ test-files:
+ description: 'Playwright test files to run (space-separated)'
+ required: false
+ default: 'tests/e2e.spec.ts tests/a11y.spec.ts'
+ screenshot-prefix:
+ description: 'Prefix for screenshot filenames'
+ required: false
+ default: 'portfolio'
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Use Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ inputs.node-version }}
+
+ - name: Install deps
+ shell: bash
+ run: npm ci
+
+ - name: Build site
+ shell: bash
+ run: ${{ inputs.build-command }}
+
+ - name: Start preview server
+ shell: bash
+ run: ${{ inputs.preview-command }} -- --port ${{ inputs.port }} &
+
+ - name: Wait for server to be ready
+ shell: bash
+ run: npx wait-on http://localhost:${{ inputs.port }}
+
+ - name: Cache Playwright browsers
+ id: playwright-cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ shell: bash
+ run: npx playwright install --with-deps
+
+ - name: Run Playwright tests (screenshots & accessibility)
+ shell: bash
+ run: |
+ npx playwright test ${{ inputs.test-files }}
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-test-results
+ path: |
+ playwright-logs/${{ inputs.screenshot-prefix }}-web-light.png
+ playwright-logs/${{ inputs.screenshot-prefix }}-web-dark.png
+ playwright-logs/${{ inputs.screenshot-prefix }}-mobile-light.png
+ playwright-logs/${{ inputs.screenshot-prefix }}-mobile-dark.png
+ playwright-report/**
+ axe-report.json
+
+ - name: Add screenshot to job summary
+ shell: bash
+ run: |
+ web_light="playwright-logs/${{ inputs.screenshot-prefix }}-web-light.png"
+ web_dark="playwright-logs/${{ inputs.screenshot-prefix }}-web-dark.png"
+ mobile_light="playwright-logs/${{ inputs.screenshot-prefix }}-mobile-light.png"
+ mobile_dark="playwright-logs/${{ inputs.screenshot-prefix }}-mobile-dark.png"
+
+ has_any=false
+ if [ -f "$web_light" ] || [ -f "$web_dark" ] || [ -f "$mobile_light" ] || [ -f "$mobile_dark" ]; then
+ has_any=true
+ fi
+
+ if [ "$has_any" = true ]; then
+ {
+ echo "## Web Render"
+ if [ -f "$web_light" ] || [ -f "$web_dark" ]; then
+ echo
+ echo "### Web"
+ if [ -f "$web_light" ]; then
+ echo
+ echo "#### Light"
+ echo
+ echo ""
+ fi
+ if [ -f "$web_dark" ]; then
+ echo
+ echo "#### Dark"
+ echo
+ echo ""
+ fi
+ fi
+ if [ -f "$mobile_light" ] || [ -f "$mobile_dark" ]; then
+ echo
+ echo "### Mobile"
+ if [ -f "$mobile_light" ]; then
+ echo
+ echo "#### Light"
+ echo
+ echo ""
+ fi
+ if [ -f "$mobile_dark" ]; then
+ echo
+ echo "#### Dark"
+ echo
+ echo ""
+ fi
+ fi
+ echo
+ echo "_This screenshot was generated by Playwright during the latest workflow run._"
+ } >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "⚠️ Screenshots not found." >> "$GITHUB_STEP_SUMMARY"
+ fi