diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39bf3efc..e9db2983 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,10 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 7 + + - name: Upload documentation screenshots + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: docs-screenshots + path: docs-screenshots/ + retention-days: 30 diff --git a/.github/workflows/sync-docs-screenshots.yml b/.github/workflows/sync-docs-screenshots.yml new file mode 100644 index 00000000..e793b830 --- /dev/null +++ b/.github/workflows/sync-docs-screenshots.yml @@ -0,0 +1,73 @@ +name: Sync Documentation Screenshots + +on: + pull_request: + types: [closed] + +permissions: + contents: read + +jobs: + sync: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Find CI run for merged PR + id: find-run + run: | + SHA="${{ github.event.pull_request.head.sha }}" + RUN_ID=$(gh api "repos/${{ github.repository }}/actions/runs?head_sha=$SHA&status=success" \ + --jq '.workflow_runs[] | select(.name == "CI") | .id' | head -n1) + if [ -z "$RUN_ID" ]; then + echo "No successful CI run found for $SHA" + exit 1 + fi + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + + - name: Download screenshots artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: docs-screenshots + path: docs-screenshots + run-id: ${{ steps.find-run.outputs.run_id }} + github-token: ${{ github.token }} + + - name: Checkout static site + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: ianpaschal/combat-command-static + token: ${{ secrets.WORKFLOW_AUTOMATION_TOKEN }} + path: static-site + + - name: Copy screenshots + run: | + mkdir -p static-site/src/assets/docs/guides + cp -r docs-screenshots/. static-site/src/assets/docs/guides/ + + - name: Open PR with updated screenshots + working-directory: static-site + run: | + BRANCH="ci/sync-docs-screenshots-${{ github.event.pull_request.number }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add src/assets/docs/guides/ + if git diff --staged --quiet; then + echo "No screenshot changes, skipping PR" + exit 0 + fi + git commit -m "Docs: Update Screenshots (ianpaschal/combat-command#${{ github.event.pull_request.number }})" + git push -f origin "$BRANCH" + gh pr create \ + --title "Docs: Update Screenshots (ianpaschal/combat-command#${{ github.event.pull_request.number }})" \ + --body "Automated screenshot update triggered by ianpaschal/combat-command#${{ github.event.pull_request.number }}." \ + --head "$BRANCH" \ + --base main \ + --reviewer ianpaschal \ + || echo "PR may already exist for this branch" + env: + GH_TOKEN: ${{ secrets.WORKFLOW_AUTOMATION_TOKEN }} diff --git a/.gitignore b/.gitignore index b80c04eb..91ff73b6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* coverage playwright-report test-results +docs-screenshots test/.auth node_modules dist diff --git a/playwright.config.ts b/playwright.config.ts index 61ea6cd5..357f709a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ use: { baseURL: 'http://localhost:5173', trace: 'on-first-retry', + viewport: { width: 1920, height: 1080 }, }, webServer: [ @@ -61,6 +62,7 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'], launchOptions: { slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0 }, + viewport: { width: 1280, height: 960 }, }, }, ], diff --git a/src/components/ToastProvider/ToastProvider.tsx b/src/components/ToastProvider/ToastProvider.tsx index 32fff882..83581550 100644 --- a/src/components/ToastProvider/ToastProvider.tsx +++ b/src/components/ToastProvider/ToastProvider.tsx @@ -61,6 +61,7 @@ export const ToastProvider = ({ {toast && ( { + await page.waitForFunction(() => !document.getAnimations().some((a) => { + if (a.playState !== 'running') { + return false; + } + const timing = (a.effect as KeyframeEffect | null)?.getTiming(); + return Number.isFinite(timing?.iterations); + })); + + const filePath = path.join(screenshotsRoot, `${name}.png`); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + await page.screenshot({ path: filePath }); +}; diff --git a/test/tournament-pairings.spec.ts b/test/tournament-pairings.spec.ts index b8548462..a0cb3e5a 100644 --- a/test/tournament-pairings.spec.ts +++ b/test/tournament-pairings.spec.ts @@ -1,6 +1,7 @@ import { type Locator } from '@playwright/test'; import { dragTo } from './helpers/dragTo'; +import { screenshot } from './helpers/screenshot'; import { signIn } from './helpers/signIn'; import { snapshotPairings } from './helpers/snapshotPairings'; import { api } from '../convex/_generated/api'; @@ -32,6 +33,9 @@ test.describe('Tournament Pairing Creation', () => { const dialog = page.getByRole('dialog', { name: 'Auto-Generate Pairings' }); await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('checkbox', { name: 'Select all' })).toBeVisible(); + await screenshot(page, 'creating-pairings/auto-generate-dialog'); + // Select all competitors, then deselect 2 so 22 are included → 11 pairings needed: await dialog.getByRole('checkbox', { name: 'Select all' }).check(); await dialog.getByRole('checkbox', { name: 'Alex Carter' }).uncheck(); @@ -53,12 +57,15 @@ test.describe('Tournament Pairing Creation', () => { const pairingRows = page.getByTestId('pairing-item'); await expect(pairingRows).toHaveCount(11); await expect(pairingRows.getByLabel('Table Assignment')).toContainText(Array(11).fill('Auto')); + await page.waitForSelector('[data-testid="toast"]', { state: 'hidden' }); + await screenshot(page, 'creating-pairings/pairing-list-auto'); // Confirm pairings with table assignments: await page.getByRole('button', { name: 'Proceed' }).click(); const confirmDialog = page.getByRole('dialog', { name: 'Confirm Pairings' }); await expect(confirmDialog).toBeVisible(); await expect(confirmDialog.getByText('The following pairings will be created:')).toBeVisible(); + await screenshot(page, 'creating-pairings/confirm-pairings-dialog'); // Create pairings and return to the tournament detail page: await confirmDialog.getByRole('button', { name: 'Create' }).click(); @@ -92,6 +99,7 @@ test.describe('Tournament Pairing Creation', () => { await page.getByRole('button', { name: 'Configure' }).click(); const dialog = page.getByRole('dialog', { name: 'Configure Pairing Rules' }); await expect(dialog).toBeVisible(); + await screenshot(page, 'creating-pairings/configure-pairing-rules-dialog'); // Adjust table count to 3: await dialog.getByLabel('Table Count').fill('3'); @@ -104,6 +112,7 @@ test.describe('Tournament Pairing Creation', () => { await addButton.click(); await addButton.click(); await expect(rows).toHaveCount(3); + await screenshot(page, 'creating-pairings/empty-pairing-slots'); // Create the first pairing by dragging 2 competitors into the slots: await dragTo( @@ -129,6 +138,7 @@ test.describe('Tournament Pairing Creation', () => { rows.nth(1).locator('[data-over]').nth(0), ); await expect(rows.nth(1).getByText('Bye')).toBeVisible(); + await screenshot(page, 'creating-pairings/pairing-row-bye'); // Remove the unused third row: await rows.nth(2).getByRole('button', { name: 'Remove Pairing' }).click(); @@ -204,6 +214,7 @@ test.describe('Tournament Pairing Creation', () => { const confirmReplaceDialog = page.getByRole('dialog', { name: 'Are you sure you want to auto-generate?' }); await expect(confirmReplaceDialog).toBeVisible(); await expect(confirmReplaceDialog.getByText('Your current pairings will be replaced.')).toBeVisible(); + await screenshot(page, 'creating-pairings/replace-confirmation-dialog'); // Cancel, and expect pairings to remain unchanged, and the generate dialog still open: await confirmReplaceDialog.getByRole('button', { name: 'Cancel' }).click(); @@ -300,6 +311,7 @@ test.describe('Tournament Pairing Creation', () => { // Observe repeat match up error: await expect(rows.nth(0).locator('[data-color="red"]')).toBeVisible(); + await screenshot(page, 'creating-pairings/repeat-opponent-warning'); // Replace idB with next unpaired competitor by dragging it onto side B: await dragTo( @@ -315,6 +327,8 @@ test.describe('Tournament Pairing Creation', () => { // Observe repeat table warning: await expect(rows.nth(0).locator('[data-color="yellow"]')).toBeVisible(); + await page.getByRole('listbox').waitFor({ state: 'hidden' }); + await screenshot(page, 'creating-pairings/repeat-table-warning'); // Create another pairing: await addButton.click();