mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test(landing): fix six stale e2e specs and wire the suite into CI (#572)
The landing Playwright suite ran in no CI workflow, so six specs had drifted red on main. Five subpages navigation tests asserted bare paths while the site emits trailing-slash URLs (format: directory), and one asserted a localized tool-detail page that is English-only by design. Fix the assertions and rewrite the tool test to the real invariant, then add a test-e2e-landing job gated on a new landing path filter so the suite runs on landing-relevant PRs and can't silently rot again.
This commit is contained in:
@@ -37,6 +37,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
code: ${{ steps.filter.outputs.code }}
|
code: ${{ steps.filter.outputs.code }}
|
||||||
|
landing: ${{ steps.filter.outputs.landing }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- id: filter
|
- id: filter
|
||||||
@@ -51,7 +52,11 @@ jobs:
|
|||||||
else
|
else
|
||||||
base="$PUSH_BEFORE_SHA"
|
base="$PUSH_BEFORE_SHA"
|
||||||
fi
|
fi
|
||||||
|
# `code`: anything outside the docs/landing surfaces changed (gates the
|
||||||
|
# main pipeline). `landing`: the landing's own surface changed (gates the
|
||||||
|
# landing e2e job). Both fail open to true when the base is unknown.
|
||||||
code=true
|
code=true
|
||||||
|
landing=true
|
||||||
if [ -n "$base" ] && [ "$base" != "0000000000000000000000000000000000000000" ]; then
|
if [ -n "$base" ] && [ "$base" != "0000000000000000000000000000000000000000" ]; then
|
||||||
if git fetch --no-tags --depth=1 origin "$base" && git cat-file -e "$base" 2>/dev/null; then
|
if git fetch --no-tags --depth=1 origin "$base" && git cat-file -e "$base" 2>/dev/null; then
|
||||||
changed=$(git diff --name-only "$base" HEAD)
|
changed=$(git diff --name-only "$base" HEAD)
|
||||||
@@ -64,6 +69,12 @@ jobs:
|
|||||||
*) code=true; break ;;
|
*) code=true; break ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
landing=false
|
||||||
|
for f in $changed; do
|
||||||
|
case "$f" in
|
||||||
|
apps/landing/*|tests/e2e-landing/*|playwright.landing.config.ts|packages/shared/*) landing=true; break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
else
|
else
|
||||||
echo "Base commit unavailable; running the full pipeline"
|
echo "Base commit unavailable; running the full pipeline"
|
||||||
fi
|
fi
|
||||||
@@ -71,6 +82,7 @@ jobs:
|
|||||||
echo "No usable base (dispatch, force push, or new branch); running the full pipeline"
|
echo "No usable base (dispatch, force push, or new branch); running the full pipeline"
|
||||||
fi
|
fi
|
||||||
echo "code=$code" >> "$GITHUB_OUTPUT"
|
echo "code=$code" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "landing=$landing" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
@@ -323,6 +335,42 @@ jobs:
|
|||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
test-e2e-landing:
|
||||||
|
name: E2E Landing (Chromium)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.landing == 'true'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
- uses: ./.github/actions/setup
|
||||||
|
- name: Get Playwright version
|
||||||
|
id: pw-version
|
||||||
|
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
|
with:
|
||||||
|
path: ~/.cache/ms-playwright
|
||||||
|
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
run: pnpm playwright install --with-deps chromium
|
||||||
|
- name: Run landing e2e suite
|
||||||
|
# The landing is a static Astro site with no database, so this needs no
|
||||||
|
# Postgres/Redis services. The suite's webServer starts `astro dev` on 4350.
|
||||||
|
run: pnpm test:e2e:landing
|
||||||
|
env:
|
||||||
|
# Authenticated GitHub API so the dev server's build-time star-count fetch
|
||||||
|
# isn't rate-limited on shared runner IPs. It falls back to a constant on
|
||||||
|
# failure either way, so this only keeps the run quiet, not correct.
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload report on failure
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: e2e-landing-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
pip-audit:
|
pip-audit:
|
||||||
name: Python Dependency Audit
|
name: Python Dependency Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -26,10 +26,22 @@ test.describe("landing i18n routing", () => {
|
|||||||
await expect(page.locator("[data-mt-banner]")).toBeAttached();
|
await expect(page.locator("[data-mt-banner]")).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a localized tool page renders with the translated tool name", async ({ page }) => {
|
test("localized tools section pages render; tool-detail pages stay English-only", async ({
|
||||||
const res = await page.goto("/de/tools/image/resize/");
|
page,
|
||||||
expect(res?.status()).toBeLessThan(400);
|
}) => {
|
||||||
|
// The tools SECTION pages (/tools/<section>/) are localized and render the
|
||||||
|
// translated section title as their heading.
|
||||||
|
const section = await page.goto("/de/tools/image/");
|
||||||
|
expect(section?.status()).toBeLessThan(400);
|
||||||
await expect(page.locator("html")).toHaveAttribute("lang", "de");
|
await expect(page.locator("html")).toHaveAttribute("lang", "de");
|
||||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||||
|
|
||||||
|
// Tool DETAIL pages (/tools/<section>/<tool>/) are English-only by design, so
|
||||||
|
// a locale-prefixed detail URL has no built page and must not resolve. The
|
||||||
|
// canonical English detail page is the one that exists.
|
||||||
|
const localizedDetail = await page.goto("/de/tools/image/resize/");
|
||||||
|
expect(localizedDetail?.status()).toBe(404);
|
||||||
|
const englishDetail = await page.goto("/tools/image/resize/");
|
||||||
|
expect(englishDetail?.status()).toBeLessThan(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,38 +94,41 @@ test.describe("Terms Page", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Cross-Page Navigation", () => {
|
test.describe("Cross-Page Navigation", () => {
|
||||||
test("footer Privacy link navigates to /privacy", async ({ page }) => {
|
// Astro's `format: "directory"` emits trailing-slash URLs, and localizeHref
|
||||||
|
// builds hrefs the same way, so navigation lands on "/privacy/" not "/privacy".
|
||||||
|
// This matches the sitemap and canonical URLs.
|
||||||
|
test("footer Privacy link navigates to /privacy/", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.locator("footer").getByRole("link", { name: "Privacy" }).click();
|
await page.locator("footer").getByRole("link", { name: "Privacy" }).click();
|
||||||
await expect(page).toHaveURL("/privacy");
|
await expect(page).toHaveURL("/privacy/");
|
||||||
await expect(page.getByText("Privacy Policy").first()).toBeVisible();
|
await expect(page.getByText("Privacy Policy").first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("footer Terms link navigates to /terms", async ({ page }) => {
|
test("footer Terms link navigates to /terms/", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.locator("footer").getByRole("link", { name: "Terms" }).click();
|
await page.locator("footer").getByRole("link", { name: "Terms" }).click();
|
||||||
await expect(page).toHaveURL("/terms");
|
await expect(page).toHaveURL("/terms/");
|
||||||
await expect(page.getByText("Terms and Conditions").first()).toBeVisible();
|
await expect(page.getByText("Terms and Conditions").first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("footer FAQ link navigates to /faq", async ({ page }) => {
|
test("footer FAQ link navigates to /faq/", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByRole("link", { name: "FAQ" }).click();
|
await page.getByRole("link", { name: "FAQ" }).click();
|
||||||
await expect(page).toHaveURL("/faq");
|
await expect(page).toHaveURL("/faq/");
|
||||||
await expect(page.getByText("Frequently Asked Questions")).toBeVisible();
|
await expect(page.getByText("Frequently Asked Questions")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navbar Talk to a human link navigates to /contact", async ({ page }) => {
|
test("navbar Talk to a human link navigates to /contact/", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByRole("link", { name: "Talk to a human" }).first().click();
|
await page.getByRole("link", { name: "Talk to a human" }).first().click();
|
||||||
await expect(page).toHaveURL("/contact");
|
await expect(page).toHaveURL("/contact/");
|
||||||
await expect(page.getByText("Get in touch")).toBeVisible();
|
await expect(page.getByText("Get in touch")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pricing enterprise CTA links to /contact", async ({ page }) => {
|
test("pricing enterprise CTA links to /contact/", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
const enterpriseCta = page.getByRole("link", { name: /Let.s talk/ }).first();
|
const enterpriseCta = page.getByRole("link", { name: /Let.s talk/ }).first();
|
||||||
await expect(enterpriseCta).toHaveAttribute("href", "/contact");
|
await expect(enterpriseCta).toHaveAttribute("href", "/contact/");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navbar Pricing link scrolls to the pricing section", async ({ page }) => {
|
test("navbar Pricing link scrolls to the pricing section", async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user