From 576844430540918ad92f837354993faad6537db9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:58:31 +0000 Subject: [PATCH 1/9] Initial plan From 7f0c5c3c7851ac2d61288c763a5014bc8de25755 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:06:15 +0000 Subject: [PATCH 2/9] feat: add Playwright E2E pipeline using Bun and @playwright/test@1.59.1 Agent-Logs-Url: https://github.com/podcodar/webapp/sessions/6eab9b84-8b76-456d-ace7-5e544f11970f Co-authored-by: marco-souza <4452113+marco-souza@users.noreply.github.com> --- .github/workflows/playwright-e2e.yml | 130 +++++++++++++++++++++++++++ e2e/blog.spec.ts | 67 ++++++++++++++ e2e/site.spec.ts | 101 +++++++++++++++++++++ package.json | 2 + 4 files changed, 300 insertions(+) create mode 100644 .github/workflows/playwright-e2e.yml create mode 100644 e2e/blog.spec.ts create mode 100644 e2e/site.spec.ts diff --git a/.github/workflows/playwright-e2e.yml b/.github/workflows/playwright-e2e.yml new file mode 100644 index 0000000..f33194b --- /dev/null +++ b/.github/workflows/playwright-e2e.yml @@ -0,0 +1,130 @@ +name: Playwright E2E + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +concurrency: + group: e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + timeout-minutes: 30 + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # needed for the PR comment step + + steps: + - uses: actions/checkout@v4 + + # Astro requires Node >=22.12.0 (see .node-version / package.json engines) + - uses: actions/setup-node@v4 + with: + node-version-file: ".node-version" + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + # Install only Chromium in CI – matches playwright.config.ts project list + - name: Install Playwright browsers + run: bunx playwright install --with-deps chromium + + # Build the Astro site and start the preview server on port 4321 + - name: Build Astro site + run: bun run build + + - name: Start Astro preview server + run: bunx astro preview --port 4321 & + + # Give the preview server a moment to be ready + - name: Wait for preview server + run: | + for i in $(seq 1 30); do + curl -sf http://localhost:4321/ && echo "Server ready" && break + echo "Waiting for server... attempt $i" + sleep 2 + done + + - name: Run Playwright E2E tests + run: bunx playwright test + env: + CI: "true" + PLAYWRIGHT_BASE_URL: "http://localhost:4321" + + # Always upload artifacts so failures can be investigated + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 14 + + - name: Upload test results (traces, videos, screenshots) + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-results + path: test-results/ + retention-days: 14 + + # Comment on the PR with run status and artifact links. + # Uses github-script so it works from fork PRs without leaking secrets + # (GITHUB_TOKEN is scoped to the target repo). + - name: Comment on PR + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + const runId = context.runId; + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; + const status = '${{ job.status }}'; + const emoji = status === 'success' ? '✅' : '❌'; + const statusText = status === 'success' ? 'All tests passed' : 'Some tests failed'; + + const body = [ + `## ${emoji} Playwright E2E — ${statusText}`, + '', + `| Detail | Link |`, + `|--------|------|`, + `| Workflow run | [View run](${runUrl}) |`, + `| HTML report | Download the **playwright-report** artifact from the run page above |`, + `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, + '', + `> **How to view the HTML report locally:**`, + `> 1. Download and unzip the \`playwright-report\` artifact.`, + `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, + '', + `> Traces and videos are captured **on first retry** (failures only).`, + ].join('\n'); + + // Update existing comment if one already exists, otherwise create a new one + const { data: comments } = await github.rest.issues.listComments({ + owner, repo, + issue_number: context.issue.number, + }); + const existing = comments.find( + (c) => c.user?.type === 'Bot' && c.body?.includes('Playwright E2E') + ); + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/e2e/blog.spec.ts b/e2e/blog.spec.ts new file mode 100644 index 0000000..4b88b01 --- /dev/null +++ b/e2e/blog.spec.ts @@ -0,0 +1,67 @@ +import { expect, test } from "@playwright/test"; + +// ────────────────────────────────────────────────────────────────────────────── +// Blog +// ────────────────────────────────────────────────────────────────────────────── + +test.describe("Blog", () => { + test("navigates to blog homepage via nav link", async ({ page }) => { + await page.goto("/"); + + // Primary nav has a "Blog" link + await page.getByRole("navigation", { name: "Primary" }).getByRole("link", { name: /^blog$/i }).click(); + + await expect(page).toHaveURL(/\/blog/); + + // The blog index page should list at least one post + const postLinks = page.getByRole("link").filter({ hasText: /./u }); + await expect(postLinks.first()).toBeVisible(); + }); + + test("blog homepage lists posts", async ({ page }) => { + await page.goto("/blog"); + + await expect(page).toHaveTitle(/PodCodar/i); + + // There should be at least one article/post link on the listing page + const firstPost = page.locator("ul li a").first(); + await expect(firstPost).toBeVisible(); + }); + + test("opens the first blog post from the listing", async ({ page }) => { + await page.goto("/blog"); + + // Click the very first post link in the listing + const firstPost = page.locator("ul li a").first(); + await expect(firstPost).toBeVisible(); + + const postTitle = await firstPost.textContent(); + await firstPost.click(); + + // We should be on a blog post URL + await expect(page).toHaveURL(/\/blog\/.+/); + + // The post page should have a

matching the post title text + if (postTitle) { + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + } + }); + + test("scrolls down the first blog post", async ({ page }) => { + await page.goto("/blog"); + + // Open the first post + await page.locator("ul li a").first().click(); + await expect(page).toHaveURL(/\/blog\/.+/); + + // Confirm the post heading is visible + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + + // Scroll to the bottom of the post + await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" })); + + // Footer should come into view + const footer = page.locator("footer"); + await expect(footer).toBeInViewport(); + }); +}); diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts new file mode 100644 index 0000000..c4202b8 --- /dev/null +++ b/e2e/site.spec.ts @@ -0,0 +1,101 @@ +import { expect, test } from "@playwright/test"; + +// ────────────────────────────────────────────────────────────────────────────── +// Homepage +// ────────────────────────────────────────────────────────────────────────────── + +test.describe("Homepage", () => { + test("has page title and hero section", async ({ page }) => { + await page.goto("/"); + + // The document title should mention PodCodar + await expect(page).toHaveTitle(/PodCodar/i); + + // Hero heading is visible + const heroHeading = page.locator("#hero-heading"); + await expect(heroHeading).toBeVisible(); + }); + + test("scrolls down the homepage", async ({ page }) => { + await page.goto("/"); + + // Confirm hero is visible before scrolling + await expect(page.locator("#hero-heading")).toBeVisible(); + + // Scroll to the bottom of the page + await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" })); + + // Footer is now in view + const footer = page.locator("footer"); + await expect(footer).toBeInViewport(); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Navigation: Contributing page (/contributing) +// ────────────────────────────────────────────────────────────────────────────── + +test.describe("Contributing page", () => { + test("navigates to Contributing page via header CTA", async ({ page }) => { + await page.goto("/"); + + // The header CTA "Como posso ajudar?" links to /contributing + await page.getByRole("link", { name: /como posso ajudar/i }).first().click(); + + await expect(page).toHaveURL(/\/contributing/); + await expect(page.getByRole("heading", { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); + + test("can navigate directly to /contributing", async ({ page }) => { + await page.goto("/contributing"); + + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole("heading", { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Navigation: Join Us page (/join-us) +// ────────────────────────────────────────────────────────────────────────────── + +test.describe("Join Us page", () => { + test("navigates to Join Us page via header CTA", async ({ page }) => { + await page.goto("/"); + + // The header primary CTA "Faça parte!" links to /join-us + await page.getByRole("link", { name: /faça parte/i }).first().click(); + + await expect(page).toHaveURL(/\/join-us/); + await expect(page.getByRole("heading", { name: /faça parte/i, level: 1 })).toBeVisible(); + }); + + test("can navigate directly to /join-us", async ({ page }) => { + await page.goto("/join-us"); + + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole("heading", { name: /faça parte/i, level: 1 })).toBeVisible(); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Navigation: Contact page (/contact) +// ────────────────────────────────────────────────────────────────────────────── + +test.describe("Contact page", () => { + test("navigates to Contact page via nav link", async ({ page }) => { + await page.goto("/"); + + // Primary nav has a "Contato" link + await page.getByRole("navigation", { name: "Primary" }).getByRole("link", { name: /contato/i }).click(); + + await expect(page).toHaveURL(/\/contact/); + await expect(page.getByRole("heading", { name: /contato|entre em contato/i })).toBeVisible(); + }); + + test("can navigate directly to /contact", async ({ page }) => { + await page.goto("/contact"); + + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole("heading", { name: /contato|entre em contato/i })).toBeVisible(); + }); +}); diff --git a/package.json b/package.json index dc9465f..ec0bf2e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "typecheck": "astro sync", "test": "vitest run", "test:watch": "vitest", + "e2e": "playwright test", "lefthook": "lefthook" }, "dependencies": { @@ -36,6 +37,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.12", + "@playwright/test": "^1.59.1", "lefthook": "^2.1.6", "vitest": "^4.1.4" } From 989d5298328012ecf1b1a48857ab9fa3cfaff841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:18:59 +0000 Subject: [PATCH 3/9] refactor: extract inline github-script JS to .github/workflows/comment-pr.js Agent-Logs-Url: https://github.com/podcodar/webapp/sessions/86950002-82c3-463f-975f-c4ee807dd523 Co-authored-by: marco-souza <4452113+marco-souza@users.noreply.github.com> --- .github/workflows/comment-pr.js | 51 ++++++++++++++++++++++++++++ .github/workflows/playwright-e2e.yml | 48 ++------------------------ 2 files changed, 54 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/comment-pr.js diff --git a/.github/workflows/comment-pr.js b/.github/workflows/comment-pr.js new file mode 100644 index 0000000..0519afd --- /dev/null +++ b/.github/workflows/comment-pr.js @@ -0,0 +1,51 @@ +// Called by the "Comment on PR" step in playwright-e2e.yml via actions/github-script. +// Required env vars (injected by the workflow): +// JOB_STATUS – the value of ${{ job.status }} + +const { owner, repo } = context.repo; +const runId = context.runId; +const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; +const status = process.env.JOB_STATUS; +const emoji = status === "success" ? "✅" : "❌"; +const statusText = status === "success" ? "All tests passed" : "Some tests failed"; + +const body = [ + `## ${emoji} Playwright E2E — ${statusText}`, + "", + `| Detail | Link |`, + `|--------|------|`, + `| Workflow run | [View run](${runUrl}) |`, + `| HTML report | Download the **playwright-report** artifact from the run page above |`, + `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, + "", + `> **How to view the HTML report locally:**`, + `> 1. Download and unzip the \`playwright-report\` artifact.`, + `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, + "", + `> Traces and videos are captured **on first retry** (failures only).`, +].join("\n"); + +// Update existing bot comment if one already exists, otherwise create a new one +const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: context.issue.number, +}); +const existing = comments.find( + (c) => c.user?.type === "Bot" && c.body?.includes("Playwright E2E"), +); +if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); +} else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: context.issue.number, + body, + }); +} diff --git a/.github/workflows/playwright-e2e.yml b/.github/workflows/playwright-e2e.yml index f33194b..1db8ba2 100644 --- a/.github/workflows/playwright-e2e.yml +++ b/.github/workflows/playwright-e2e.yml @@ -81,50 +81,8 @@ jobs: - name: Comment on PR if: always() && github.event_name == 'pull_request' uses: actions/github-script@v7 + env: + JOB_STATUS: ${{ job.status }} with: github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo } = context.repo; - const runId = context.runId; - const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; - const status = '${{ job.status }}'; - const emoji = status === 'success' ? '✅' : '❌'; - const statusText = status === 'success' ? 'All tests passed' : 'Some tests failed'; - - const body = [ - `## ${emoji} Playwright E2E — ${statusText}`, - '', - `| Detail | Link |`, - `|--------|------|`, - `| Workflow run | [View run](${runUrl}) |`, - `| HTML report | Download the **playwright-report** artifact from the run page above |`, - `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, - '', - `> **How to view the HTML report locally:**`, - `> 1. Download and unzip the \`playwright-report\` artifact.`, - `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, - '', - `> Traces and videos are captured **on first retry** (failures only).`, - ].join('\n'); - - // Update existing comment if one already exists, otherwise create a new one - const { data: comments } = await github.rest.issues.listComments({ - owner, repo, - issue_number: context.issue.number, - }); - const existing = comments.find( - (c) => c.user?.type === 'Bot' && c.body?.includes('Playwright E2E') - ); - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner, repo, - issue_number: context.issue.number, - body, - }); - } + script-path: .github/workflows/comment-pr.js From 2b6a12014ca0ecb8ac6fcd0f8ee0ff221497dd16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:43:41 +0000 Subject: [PATCH 4/9] fix: update bun lockfile with playwright, fix github-script, always post new PR comment Agent-Logs-Url: https://github.com/podcodar/webapp/sessions/ac05ea3e-4555-4bf0-a32d-6b89506970cc Co-authored-by: marco-souza <4452113+marco-souza@users.noreply.github.com> --- .github/workflows/comment-pr.js | 63 +++++++++++----------------- .github/workflows/playwright-e2e.yml | 4 +- bun.lock | 7 ++++ 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/.github/workflows/comment-pr.js b/.github/workflows/comment-pr.js index 0519afd..0e4905c 100644 --- a/.github/workflows/comment-pr.js +++ b/.github/workflows/comment-pr.js @@ -2,50 +2,35 @@ // Required env vars (injected by the workflow): // JOB_STATUS – the value of ${{ job.status }} -const { owner, repo } = context.repo; -const runId = context.runId; -const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; -const status = process.env.JOB_STATUS; -const emoji = status === "success" ? "✅" : "❌"; -const statusText = status === "success" ? "All tests passed" : "Some tests failed"; +module.exports = async ({ github, context }) => { + const { owner, repo } = context.repo; + const runId = context.runId; + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; + const status = process.env.JOB_STATUS; + const emoji = status === "success" ? "✅" : "❌"; + const statusText = status === "success" ? "All tests passed" : "Some tests failed"; -const body = [ - `## ${emoji} Playwright E2E — ${statusText}`, - "", - `| Detail | Link |`, - `|--------|------|`, - `| Workflow run | [View run](${runUrl}) |`, - `| HTML report | Download the **playwright-report** artifact from the run page above |`, - `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, - "", - `> **How to view the HTML report locally:**`, - `> 1. Download and unzip the \`playwright-report\` artifact.`, - `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, - "", - `> Traces and videos are captured **on first retry** (failures only).`, -].join("\n"); + const body = [ + `## ${emoji} Playwright E2E — ${statusText}`, + "", + `| Detail | Link |`, + `|--------|------|`, + `| Workflow run | [View run](${runUrl}) |`, + `| HTML report | Download the **playwright-report** artifact from the run page above |`, + `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, + "", + `> **How to view the HTML report locally:**`, + `> 1. Download and unzip the \`playwright-report\` artifact.`, + `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, + "", + `> Traces and videos are captured **on first retry** (failures only).`, + ].join("\n"); -// Update existing bot comment if one already exists, otherwise create a new one -const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: context.issue.number, -}); -const existing = comments.find( - (c) => c.user?.type === "Bot" && c.body?.includes("Playwright E2E"), -); -if (existing) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body, - }); -} else { + // Always post a new comment so each commit push gets its own status entry await github.rest.issues.createComment({ owner, repo, issue_number: context.issue.number, body, }); -} +}; diff --git a/.github/workflows/playwright-e2e.yml b/.github/workflows/playwright-e2e.yml index 1db8ba2..5e8c4b6 100644 --- a/.github/workflows/playwright-e2e.yml +++ b/.github/workflows/playwright-e2e.yml @@ -85,4 +85,6 @@ jobs: JOB_STATUS: ${{ job.status }} with: github-token: ${{ secrets.GITHUB_TOKEN }} - script-path: .github/workflows/comment-pr.js + script: | + const script = require('./.github/workflows/comment-pr.js') + await script({ github, context, core }) diff --git a/bun.lock b/bun.lock index 28c7239..710df36 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.12", + "@playwright/test": "^1.59.1", "lefthook": "^2.1.6", "vitest": "^4.1.4", }, @@ -78,6 +79,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], @@ -904,6 +907,10 @@ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], From 2e7a6568eb81e488da8117d065584ac1a2de798a Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 18 Apr 2026 09:51:21 -0300 Subject: [PATCH 5/9] fix: run bun format --- .github/workflows/comment-pr.js | 56 +++++++------- e2e/blog.spec.ts | 95 ++++++++++++----------- e2e/site.spec.ts | 129 +++++++++++++++++--------------- 3 files changed, 148 insertions(+), 132 deletions(-) diff --git a/.github/workflows/comment-pr.js b/.github/workflows/comment-pr.js index 0e4905c..c8e48cf 100644 --- a/.github/workflows/comment-pr.js +++ b/.github/workflows/comment-pr.js @@ -3,34 +3,34 @@ // JOB_STATUS – the value of ${{ job.status }} module.exports = async ({ github, context }) => { - const { owner, repo } = context.repo; - const runId = context.runId; - const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; - const status = process.env.JOB_STATUS; - const emoji = status === "success" ? "✅" : "❌"; - const statusText = status === "success" ? "All tests passed" : "Some tests failed"; + const { owner, repo } = context.repo; + const runId = context.runId; + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; + const status = process.env.JOB_STATUS; + const emoji = status === 'success' ? '✅' : '❌'; + const statusText = status === 'success' ? 'All tests passed' : 'Some tests failed'; - const body = [ - `## ${emoji} Playwright E2E — ${statusText}`, - "", - `| Detail | Link |`, - `|--------|------|`, - `| Workflow run | [View run](${runUrl}) |`, - `| HTML report | Download the **playwright-report** artifact from the run page above |`, - `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, - "", - `> **How to view the HTML report locally:**`, - `> 1. Download and unzip the \`playwright-report\` artifact.`, - `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, - "", - `> Traces and videos are captured **on first retry** (failures only).`, - ].join("\n"); + const body = [ + `## ${emoji} Playwright E2E — ${statusText}`, + '', + `| Detail | Link |`, + `|--------|------|`, + `| Workflow run | [View run](${runUrl}) |`, + `| HTML report | Download the **playwright-report** artifact from the run page above |`, + `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, + '', + `> **How to view the HTML report locally:**`, + `> 1. Download and unzip the \`playwright-report\` artifact.`, + `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, + '', + `> Traces and videos are captured **on first retry** (failures only).`, + ].join('\n'); - // Always post a new comment so each commit push gets its own status entry - await github.rest.issues.createComment({ - owner, - repo, - issue_number: context.issue.number, - body, - }); + // Always post a new comment so each commit push gets its own status entry + await github.rest.issues.createComment({ + owner, + repo, + issue_number: context.issue.number, + body, + }); }; diff --git a/e2e/blog.spec.ts b/e2e/blog.spec.ts index 4b88b01..e1bbe5d 100644 --- a/e2e/blog.spec.ts +++ b/e2e/blog.spec.ts @@ -1,67 +1,72 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from '@playwright/test'; // ────────────────────────────────────────────────────────────────────────────── // Blog // ────────────────────────────────────────────────────────────────────────────── -test.describe("Blog", () => { - test("navigates to blog homepage via nav link", async ({ page }) => { - await page.goto("/"); +test.describe('Blog', () => { + test('navigates to blog homepage via nav link', async ({ page }) => { + await page.goto('/'); - // Primary nav has a "Blog" link - await page.getByRole("navigation", { name: "Primary" }).getByRole("link", { name: /^blog$/i }).click(); + // Primary nav has a "Blog" link + await page + .getByRole('navigation', { name: 'Primary' }) + .getByRole('link', { name: /^blog$/i }) + .click(); - await expect(page).toHaveURL(/\/blog/); + await expect(page).toHaveURL(/\/blog/); - // The blog index page should list at least one post - const postLinks = page.getByRole("link").filter({ hasText: /./u }); - await expect(postLinks.first()).toBeVisible(); - }); + // The blog index page should list at least one post + const postLinks = page.getByRole('link').filter({ hasText: /./u }); + await expect(postLinks.first()).toBeVisible(); + }); - test("blog homepage lists posts", async ({ page }) => { - await page.goto("/blog"); + test('blog homepage lists posts', async ({ page }) => { + await page.goto('/blog'); - await expect(page).toHaveTitle(/PodCodar/i); + await expect(page).toHaveTitle(/PodCodar/i); - // There should be at least one article/post link on the listing page - const firstPost = page.locator("ul li a").first(); - await expect(firstPost).toBeVisible(); - }); + // There should be at least one article/post link on the listing page + const firstPost = page.locator('ul li a').first(); + await expect(firstPost).toBeVisible(); + }); - test("opens the first blog post from the listing", async ({ page }) => { - await page.goto("/blog"); + test('opens the first blog post from the listing', async ({ page }) => { + await page.goto('/blog'); - // Click the very first post link in the listing - const firstPost = page.locator("ul li a").first(); - await expect(firstPost).toBeVisible(); + // Click the very first post link in the listing + const firstPost = page.locator('ul li a').first(); + await expect(firstPost).toBeVisible(); - const postTitle = await firstPost.textContent(); - await firstPost.click(); + const postTitle = await firstPost.textContent(); + await firstPost.click(); - // We should be on a blog post URL - await expect(page).toHaveURL(/\/blog\/.+/); + // We should be on a blog post URL + await expect(page).toHaveURL(/\/blog\/.+/); - // The post page should have a

matching the post title text - if (postTitle) { - await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); - } - }); + // The post page should have a

matching the post title text + if (postTitle) { + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + } + }); - test("scrolls down the first blog post", async ({ page }) => { - await page.goto("/blog"); + test('scrolls down the first blog post', async ({ page }) => { + await page.goto('/blog'); - // Open the first post - await page.locator("ul li a").first().click(); - await expect(page).toHaveURL(/\/blog\/.+/); + // Open the first post + await page.locator('ul li a').first().click(); + await expect(page).toHaveURL(/\/blog\/.+/); - // Confirm the post heading is visible - await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + // Confirm the post heading is visible + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); - // Scroll to the bottom of the post - await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" })); + // Scroll to the bottom of the post + await page.evaluate(() => + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + ); - // Footer should come into view - const footer = page.locator("footer"); - await expect(footer).toBeInViewport(); - }); + // Footer should come into view + const footer = page.locator('footer'); + await expect(footer).toBeInViewport(); + }); }); diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts index c4202b8..d2474bb 100644 --- a/e2e/site.spec.ts +++ b/e2e/site.spec.ts @@ -1,101 +1,112 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from '@playwright/test'; // ────────────────────────────────────────────────────────────────────────────── // Homepage // ────────────────────────────────────────────────────────────────────────────── -test.describe("Homepage", () => { - test("has page title and hero section", async ({ page }) => { - await page.goto("/"); +test.describe('Homepage', () => { + test('has page title and hero section', async ({ page }) => { + await page.goto('/'); - // The document title should mention PodCodar - await expect(page).toHaveTitle(/PodCodar/i); + // The document title should mention PodCodar + await expect(page).toHaveTitle(/PodCodar/i); - // Hero heading is visible - const heroHeading = page.locator("#hero-heading"); - await expect(heroHeading).toBeVisible(); - }); + // Hero heading is visible + const heroHeading = page.locator('#hero-heading'); + await expect(heroHeading).toBeVisible(); + }); - test("scrolls down the homepage", async ({ page }) => { - await page.goto("/"); + test('scrolls down the homepage', async ({ page }) => { + await page.goto('/'); - // Confirm hero is visible before scrolling - await expect(page.locator("#hero-heading")).toBeVisible(); + // Confirm hero is visible before scrolling + await expect(page.locator('#hero-heading')).toBeVisible(); - // Scroll to the bottom of the page - await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" })); + // Scroll to the bottom of the page + await page.evaluate(() => + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + ); - // Footer is now in view - const footer = page.locator("footer"); - await expect(footer).toBeInViewport(); - }); + // Footer is now in view + const footer = page.locator('footer'); + await expect(footer).toBeInViewport(); + }); }); // ────────────────────────────────────────────────────────────────────────────── // Navigation: Contributing page (/contributing) // ────────────────────────────────────────────────────────────────────────────── -test.describe("Contributing page", () => { - test("navigates to Contributing page via header CTA", async ({ page }) => { - await page.goto("/"); +test.describe('Contributing page', () => { + test('navigates to Contributing page via header CTA', async ({ page }) => { + await page.goto('/'); - // The header CTA "Como posso ajudar?" links to /contributing - await page.getByRole("link", { name: /como posso ajudar/i }).first().click(); + // The header CTA "Como posso ajudar?" links to /contributing + await page + .getByRole('link', { name: /como posso ajudar/i }) + .first() + .click(); - await expect(page).toHaveURL(/\/contributing/); - await expect(page.getByRole("heading", { name: /como posso ajudar/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveURL(/\/contributing/); + await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); - test("can navigate directly to /contributing", async ({ page }) => { - await page.goto("/contributing"); + test('can navigate directly to /contributing', async ({ page }) => { + await page.goto('/contributing'); - await expect(page).toHaveTitle(/PodCodar/i); - await expect(page.getByRole("heading", { name: /como posso ajudar/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); }); // ────────────────────────────────────────────────────────────────────────────── // Navigation: Join Us page (/join-us) // ────────────────────────────────────────────────────────────────────────────── -test.describe("Join Us page", () => { - test("navigates to Join Us page via header CTA", async ({ page }) => { - await page.goto("/"); +test.describe('Join Us page', () => { + test('navigates to Join Us page via header CTA', async ({ page }) => { + await page.goto('/'); - // The header primary CTA "Faça parte!" links to /join-us - await page.getByRole("link", { name: /faça parte/i }).first().click(); + // The header primary CTA "Faça parte!" links to /join-us + await page + .getByRole('link', { name: /faça parte/i }) + .first() + .click(); - await expect(page).toHaveURL(/\/join-us/); - await expect(page.getByRole("heading", { name: /faça parte/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveURL(/\/join-us/); + await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); + }); - test("can navigate directly to /join-us", async ({ page }) => { - await page.goto("/join-us"); + test('can navigate directly to /join-us', async ({ page }) => { + await page.goto('/join-us'); - await expect(page).toHaveTitle(/PodCodar/i); - await expect(page.getByRole("heading", { name: /faça parte/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); + }); }); // ────────────────────────────────────────────────────────────────────────────── // Navigation: Contact page (/contact) // ────────────────────────────────────────────────────────────────────────────── -test.describe("Contact page", () => { - test("navigates to Contact page via nav link", async ({ page }) => { - await page.goto("/"); +test.describe('Contact page', () => { + test('navigates to Contact page via nav link', async ({ page }) => { + await page.goto('/'); - // Primary nav has a "Contato" link - await page.getByRole("navigation", { name: "Primary" }).getByRole("link", { name: /contato/i }).click(); + // Primary nav has a "Contato" link + await page + .getByRole('navigation', { name: 'Primary' }) + .getByRole('link', { name: /contato/i }) + .click(); - await expect(page).toHaveURL(/\/contact/); - await expect(page.getByRole("heading", { name: /contato|entre em contato/i })).toBeVisible(); - }); + await expect(page).toHaveURL(/\/contact/); + await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); + }); - test("can navigate directly to /contact", async ({ page }) => { - await page.goto("/contact"); + test('can navigate directly to /contact', async ({ page }) => { + await page.goto('/contact'); - await expect(page).toHaveTitle(/PodCodar/i); - await expect(page.getByRole("heading", { name: /contato|entre em contato/i })).toBeVisible(); - }); + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); + }); }); From e27a630e1ad70312d57cd1854673197ea1c7f99d Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 18 Apr 2026 09:58:37 -0300 Subject: [PATCH 6/9] fix: formatted date types --- src/components/FormattedDate.astro | 4 ++-- test-results/.last-run.json | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 test-results/.last-run.json diff --git a/src/components/FormattedDate.astro b/src/components/FormattedDate.astro index 4b77ad6..ad194a5 100644 --- a/src/components/FormattedDate.astro +++ b/src/components/FormattedDate.astro @@ -1,7 +1,7 @@ --- -interface Props { +type Props = { date: Date; -} +}; const { date } = Astro.props; --- diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..3120dd4 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} From 788cb181fa75f0ab0404fbfef2b3d263dc337828 Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 18 Apr 2026 11:02:06 -0300 Subject: [PATCH 7/9] fix: run bun format --- .github/workflows/comment-pr.js | 56 ++--- .gitignore | 4 + biome.json | 1 + bun.lock | 9 +- e2e/blog.spec.ts | 96 ++++---- e2e/site.spec.ts | 130 +++++------ package.json | 3 + playwright.config.ts | 45 ++++ src/components/FormattedDate.test.ts | 14 +- src/components/Logo.astro | 6 +- src/components/marketing/Hero.astro | 15 +- src/consts.ts | 2 +- src/content.config.ts | 24 +- src/data/marketing.ts | 320 +++++++++++++-------------- src/data/social-links.ts | 80 +++---- src/i18n/ui.ts | 18 +- src/i18n/utils.ts | 8 +- src/lib/theme-cookie.ts | 58 ++--- src/pages/rss.xml.js | 20 +- test-results/.last-run.json | 4 - tsconfig.json | 5 +- vitest.config.ts | 8 +- 22 files changed, 487 insertions(+), 439 deletions(-) create mode 100644 playwright.config.ts delete mode 100644 test-results/.last-run.json diff --git a/.github/workflows/comment-pr.js b/.github/workflows/comment-pr.js index c8e48cf..e4bdc0c 100644 --- a/.github/workflows/comment-pr.js +++ b/.github/workflows/comment-pr.js @@ -3,34 +3,34 @@ // JOB_STATUS – the value of ${{ job.status }} module.exports = async ({ github, context }) => { - const { owner, repo } = context.repo; - const runId = context.runId; - const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; - const status = process.env.JOB_STATUS; - const emoji = status === 'success' ? '✅' : '❌'; - const statusText = status === 'success' ? 'All tests passed' : 'Some tests failed'; + const { owner, repo } = context.repo; + const runId = context.runId; + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; + const status = process.env.JOB_STATUS; + const emoji = status === 'success' ? '✅' : '❌'; + const statusText = status === 'success' ? 'All tests passed' : 'Some tests failed'; - const body = [ - `## ${emoji} Playwright E2E — ${statusText}`, - '', - `| Detail | Link |`, - `|--------|------|`, - `| Workflow run | [View run](${runUrl}) |`, - `| HTML report | Download the **playwright-report** artifact from the run page above |`, - `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, - '', - `> **How to view the HTML report locally:**`, - `> 1. Download and unzip the \`playwright-report\` artifact.`, - `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, - '', - `> Traces and videos are captured **on first retry** (failures only).`, - ].join('\n'); + const body = [ + `## ${emoji} Playwright E2E — ${statusText}`, + '', + `| Detail | Link |`, + `|--------|------|`, + `| Workflow run | [View run](${runUrl}) |`, + `| HTML report | Download the **playwright-report** artifact from the run page above |`, + `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, + '', + `> **How to view the HTML report locally:**`, + `> 1. Download and unzip the \`playwright-report\` artifact.`, + `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, + '', + `> Traces and videos are captured **on first retry** (failures only).`, + ].join('\n'); - // Always post a new comment so each commit push gets its own status entry - await github.rest.issues.createComment({ - owner, - repo, - issue_number: context.issue.number, - body, - }); + // Always post a new comment so each commit push gets its own status entry + await github.rest.issues.createComment({ + owner, + repo, + issue_number: context.issue.number, + body, + }); }; diff --git a/.gitignore b/.gitignore index 30bfc78..400724f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ pnpm-debug.log* # Maestro Setup .maestro/ + +# E2E Tests - Playwright +test-results/ +playwright-report/ diff --git a/biome.json b/biome.json index 3f21f7e..23e01ac 100644 --- a/biome.json +++ b/biome.json @@ -22,6 +22,7 @@ }, "formatter": { "enabled": true, + "useEditorconfig": true, "indentStyle": "tab", "indentWidth": 2, "lineWidth": 100, diff --git a/bun.lock b/bun.lock index 710df36..f6852dd 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "@biomejs/biome": "^2.4.12", "@playwright/test": "^1.59.1", "lefthook": "^2.1.6", + "playwright": "^1.59.1", "vitest": "^4.1.4", }, }, @@ -79,8 +80,6 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], - "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], - "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], @@ -231,6 +230,8 @@ "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], @@ -907,12 +908,12 @@ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], diff --git a/e2e/blog.spec.ts b/e2e/blog.spec.ts index e1bbe5d..9235bfa 100644 --- a/e2e/blog.spec.ts +++ b/e2e/blog.spec.ts @@ -5,68 +5,68 @@ import { expect, test } from '@playwright/test'; // ────────────────────────────────────────────────────────────────────────────── test.describe('Blog', () => { - test('navigates to blog homepage via nav link', async ({ page }) => { - await page.goto('/'); + test('navigates to blog homepage via nav link', async ({ page }) => { + await page.goto('/'); - // Primary nav has a "Blog" link - await page - .getByRole('navigation', { name: 'Primary' }) - .getByRole('link', { name: /^blog$/i }) - .click(); + // Primary nav has a "Blog" link + await page + .getByRole('navigation', { name: 'Primary' }) + .getByRole('link', { name: /^blog$/i }) + .click(); - await expect(page).toHaveURL(/\/blog/); + await expect(page).toHaveURL(/\/blog/); - // The blog index page should list at least one post - const postLinks = page.getByRole('link').filter({ hasText: /./u }); - await expect(postLinks.first()).toBeVisible(); - }); + // The blog index page should list at least one post + const postLinks = page.getByRole('link').filter({ hasText: /./u }); + await expect(postLinks.first()).toBeVisible(); + }); - test('blog homepage lists posts', async ({ page }) => { - await page.goto('/blog'); + test('blog homepage lists posts', async ({ page }) => { + await page.goto('/blog'); - await expect(page).toHaveTitle(/PodCodar/i); + await expect(page).toHaveTitle(/PodCodar/i); - // There should be at least one article/post link on the listing page - const firstPost = page.locator('ul li a').first(); - await expect(firstPost).toBeVisible(); - }); + // There should be at least one article/post link on the listing page + const firstPost = page.locator('ul li a').first(); + await expect(firstPost).toBeVisible(); + }); - test('opens the first blog post from the listing', async ({ page }) => { - await page.goto('/blog'); + test('opens the first blog post from the listing', async ({ page }) => { + await page.goto('/blog'); - // Click the very first post link in the listing - const firstPost = page.locator('ul li a').first(); - await expect(firstPost).toBeVisible(); + // Click the very first post link in the listing + const firstPost = page.locator('ul li a').first(); + await expect(firstPost).toBeVisible(); - const postTitle = await firstPost.textContent(); - await firstPost.click(); + const postTitle = await firstPost.textContent(); + await firstPost.click(); - // We should be on a blog post URL - await expect(page).toHaveURL(/\/blog\/.+/); + // We should be on a blog post URL + await expect(page).toHaveURL(/\/blog\/.+/); - // The post page should have a

matching the post title text - if (postTitle) { - await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); - } - }); + // The post page should have a

matching the post title text + if (postTitle) { + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + } + }); - test('scrolls down the first blog post', async ({ page }) => { - await page.goto('/blog'); + test('scrolls down the first blog post', async ({ page }) => { + await page.goto('/blog'); - // Open the first post - await page.locator('ul li a').first().click(); - await expect(page).toHaveURL(/\/blog\/.+/); + // Open the first post + await page.locator('ul li a').first().click(); + await expect(page).toHaveURL(/\/blog\/.+/); - // Confirm the post heading is visible - await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + // Confirm the post heading is visible + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); - // Scroll to the bottom of the post - await page.evaluate(() => - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) - ); + // Scroll to the bottom of the post + await page.evaluate(() => + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + ); - // Footer should come into view - const footer = page.locator('footer'); - await expect(footer).toBeInViewport(); - }); + // Footer should come into view + const footer = page.locator('footer'); + await expect(footer).toBeInViewport(); + }); }); diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts index d2474bb..68f6a45 100644 --- a/e2e/site.spec.ts +++ b/e2e/site.spec.ts @@ -5,32 +5,32 @@ import { expect, test } from '@playwright/test'; // ────────────────────────────────────────────────────────────────────────────── test.describe('Homepage', () => { - test('has page title and hero section', async ({ page }) => { - await page.goto('/'); + test('has page title and hero section', async ({ page }) => { + await page.goto('/'); - // The document title should mention PodCodar - await expect(page).toHaveTitle(/PodCodar/i); + // The document title should mention PodCodar + await expect(page).toHaveTitle(/PodCodar/i); - // Hero heading is visible - const heroHeading = page.locator('#hero-heading'); - await expect(heroHeading).toBeVisible(); - }); + // Hero heading is visible + const heroHeading = page.locator('#hero-heading'); + await expect(heroHeading).toBeVisible(); + }); - test('scrolls down the homepage', async ({ page }) => { - await page.goto('/'); + test('scrolls down the homepage', async ({ page }) => { + await page.goto('/'); - // Confirm hero is visible before scrolling - await expect(page.locator('#hero-heading')).toBeVisible(); + // Confirm hero is visible before scrolling + await expect(page.locator('#hero-heading')).toBeVisible(); - // Scroll to the bottom of the page - await page.evaluate(() => - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) - ); + // Scroll to the bottom of the page + await page.evaluate(() => + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + ); - // Footer is now in view - const footer = page.locator('footer'); - await expect(footer).toBeInViewport(); - }); + // Footer is now in view + const footer = page.locator('footer'); + await expect(footer).toBeInViewport(); + }); }); // ────────────────────────────────────────────────────────────────────────────── @@ -38,25 +38,25 @@ test.describe('Homepage', () => { // ────────────────────────────────────────────────────────────────────────────── test.describe('Contributing page', () => { - test('navigates to Contributing page via header CTA', async ({ page }) => { - await page.goto('/'); + test('navigates to Contributing page via header CTA', async ({ page }) => { + await page.goto('/'); - // The header CTA "Como posso ajudar?" links to /contributing - await page - .getByRole('link', { name: /como posso ajudar/i }) - .first() - .click(); + // The header CTA "Como posso ajudar?" links to /contributing + await page + .getByRole('link', { name: /como posso ajudar/i }) + .first() + .click(); - await expect(page).toHaveURL(/\/contributing/); - await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveURL(/\/contributing/); + await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); - test('can navigate directly to /contributing', async ({ page }) => { - await page.goto('/contributing'); + test('can navigate directly to /contributing', async ({ page }) => { + await page.goto('/contributing'); - await expect(page).toHaveTitle(/PodCodar/i); - await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); }); // ────────────────────────────────────────────────────────────────────────────── @@ -64,25 +64,25 @@ test.describe('Contributing page', () => { // ────────────────────────────────────────────────────────────────────────────── test.describe('Join Us page', () => { - test('navigates to Join Us page via header CTA', async ({ page }) => { - await page.goto('/'); + test('navigates to Join Us page via header CTA', async ({ page }) => { + await page.goto('/'); - // The header primary CTA "Faça parte!" links to /join-us - await page - .getByRole('link', { name: /faça parte/i }) - .first() - .click(); + // The header primary CTA "Faça parte!" links to /join-us + await page + .getByRole('link', { name: /faça parte/i }) + .first() + .click(); - await expect(page).toHaveURL(/\/join-us/); - await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveURL(/\/join-us/); + await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); + }); - test('can navigate directly to /join-us', async ({ page }) => { - await page.goto('/join-us'); + test('can navigate directly to /join-us', async ({ page }) => { + await page.goto('/join-us'); - await expect(page).toHaveTitle(/PodCodar/i); - await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); - }); + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); + }); }); // ────────────────────────────────────────────────────────────────────────────── @@ -90,23 +90,23 @@ test.describe('Join Us page', () => { // ────────────────────────────────────────────────────────────────────────────── test.describe('Contact page', () => { - test('navigates to Contact page via nav link', async ({ page }) => { - await page.goto('/'); + test('navigates to Contact page via nav link', async ({ page }) => { + await page.goto('/'); - // Primary nav has a "Contato" link - await page - .getByRole('navigation', { name: 'Primary' }) - .getByRole('link', { name: /contato/i }) - .click(); + // Primary nav has a "Contato" link + await page + .getByRole('navigation', { name: 'Primary' }) + .getByRole('link', { name: /contato/i }) + .click(); - await expect(page).toHaveURL(/\/contact/); - await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); - }); + await expect(page).toHaveURL(/\/contact/); + await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); + }); - test('can navigate directly to /contact', async ({ page }) => { - await page.goto('/contact'); + test('can navigate directly to /contact', async ({ page }) => { + await page.goto('/contact'); - await expect(page).toHaveTitle(/PodCodar/i); - await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); - }); + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); + }); }); diff --git a/package.json b/package.json index ec0bf2e..0c9af00 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", + "deps": "bun x playwright install-deps", + "postinstall": "lefthook install && playwright install", "update": "bun update --all", "lint": "biome check", "lint:fix": "biome check --write && astro check --fix", @@ -39,6 +41,7 @@ "@biomejs/biome": "^2.4.12", "@playwright/test": "^1.59.1", "lefthook": "^2.1.6", + "playwright": "^1.59.1", "vitest": "^4.1.4" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..7f581ee --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,45 @@ +import { defineConfig, devices } from '@playwright/test'; + +const targetUrl = 'http://localhost:4321'; + +export default defineConfig({ + // Look for test files in the "tests" directory, relative to this configuration file. + testDir: 'e2e', + + // Run all tests in parallel. + fullyParallel: true, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, + + // Retry on CI only. + retries: process.env.CI ? 2 : 0, + + // Opt out of parallel tests on CI. + workers: process.env.CI ? 1 : undefined, + + // Reporter to use + reporter: 'html', + + use: { + // Base URL to use in actions like `await page.goto('/')`. + baseURL: targetUrl, + + // Collect trace when retrying the failed test. + trace: 'on-first-retry', + }, + // Configure projects for major browsers. + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + // Run your local dev server before starting the tests. + webServer: { + command: 'bun run dev', + url: targetUrl, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/components/FormattedDate.test.ts b/src/components/FormattedDate.test.ts index 506bf6b..d695ff2 100644 --- a/src/components/FormattedDate.test.ts +++ b/src/components/FormattedDate.test.ts @@ -3,12 +3,12 @@ import { expect, test } from 'vitest'; import FormattedDate from './FormattedDate.astro'; test('FormattedDate renders a