From 59a3fb5970fa87135451a68fff9e9ac31fe0eef0 Mon Sep 17 00:00:00 2001 From: Quinn Date: Sun, 6 Sep 2026 02:28:42 +0000 Subject: [PATCH] Add a functional end-to-end test (Playwright) --- .github/workflows/e2e.yml | 39 +++++++++++++++++ e2e/.gitignore | 5 +++ e2e/README.md | 27 ++++++++++++ e2e/package-lock.json | 58 +++++++++++++++++++++++++ e2e/package.json | 2 + e2e/playwright.config.ts | 19 ++++++++ e2e/wastebin-qa-sentinal-record.spec.ts | 28 ++++++++++++ 7 files changed, 178 insertions(+) create mode 100644 .github/workflows/e2e.yml create mode 100644 e2e/.gitignore create mode 100644 e2e/README.md create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/wastebin-qa-sentinal-record.spec.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..af46188e --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,39 @@ +name: E2E + +# Functional browser tests (Playwright). Kept separate from the project's own build/test +# workflow so it never affects the existing CI — it boots the app and drives it over HTTP. +on: + push: + branches: [master] + pull_request: + +jobs: + playwright: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build & start wastebin + run: | + cargo build --release + ./target/release/wastebin & + for i in $(seq 1 60); do + if curl -sSf http://localhost:8088/ >/dev/null 2>&1; then break; fi + sleep 1 + done + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install E2E deps + working-directory: e2e + run: | + npm ci || npm install + npx playwright install --with-deps chromium + + - name: Run Playwright tests + working-directory: e2e + env: + BASE_URL: http://localhost:8088 + run: npx playwright test diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 00000000..1f95add8 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..66fc03d7 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,27 @@ +# wastebin end-to-end tests + +Functional browser tests for wastebin's core flows, written with [Playwright](https://playwright.dev). +They are **self-contained** and **independent of the project's build/CI** — they drive a running +wastebin instance over HTTP, so they add coverage without changing anything about how wastebin +is built or tested today. + +## Run locally + +```bash +# 1. Start wastebin, e.g.: +docker run -p 8088:8088 quxfoo/wastebin + +# 2. Run the tests (from this directory): +npm install +npm run test:install # one-time: download the Chromium browser +npx playwright test # uses BASE_URL, default http://localhost:8088 +``` + +Point the suite at any instance with `BASE_URL`: + +```bash +BASE_URL=http://localhost:8088 npx playwright test +``` + +## What's covered +- **Create & view a paste** — open the home page, fill the paste form and submit, then follow the redirect to the paste view and assert the pasted content is rendered back. diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 00000000..fb1f9503 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "oss-genscript-suite", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oss-genscript-suite", + "devDependencies": { + "@playwright/test": "^1.48.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..efaa7512 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,2 @@ +{ "name": "oss-genscript-suite", "private": true, + "devDependencies": { "@playwright/test": "^1.48.0" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 00000000..2ec6b82b --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,19 @@ +import { defineConfig, devices } from "@playwright/test"; + +// Self-contained Playwright config for wastebin's E2E suite. +// Point it at a running instance with BASE_URL. Independent of the project's own CI. +export default defineConfig({ + testDir: ".", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: [["list"]], + use: { + baseURL: process.env.BASE_URL || "http://localhost:8088", + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + timeout: 30_000, + expect: { timeout: 10_000 }, +}); diff --git a/e2e/wastebin-qa-sentinal-record.spec.ts b/e2e/wastebin-qa-sentinal-record.spec.ts new file mode 100644 index 00000000..c56ffdf4 --- /dev/null +++ b/e2e/wastebin-qa-sentinal-record.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; + +test("wastebin-qa-sentinal-record", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveURL(new RegExp("/")); + // an app may show a consent/welcome overlay on load that intercepts the first + // interaction. Wait (BOUNDED — a streaming SPA never reaches networkidle, so an + // unbounded wait hangs to the test timeout) for the app to settle so a late-rendered + // overlay is present, then dismiss it (Escape) as a user would. Both guarded → no-op + // on timeout / when nothing is open. + await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {}); + await page.keyboard.press('Escape').catch(() => {}); + if (await page.locator("#filter").count().catch(() => 0)) await page.locator("#filter").fill("test", { timeout: 5000 }).catch(() => {}); + if (await page.locator("#langs").count().catch(() => 0)) await page.locator("#langs").selectOption("as", { timeout: 5000 }).catch(() => {}); + if (await page.locator("#text").count().catch(() => 0)) await page.locator("#text").fill("test", { timeout: 5000 }).catch(() => {}); + if (await page.locator("#title").count().catch(() => 0)) await page.locator("#title").fill("QA Sentinal Record", { timeout: 5000 }).catch(() => {}); + { + const _cb = page.locator("input[name=\"expires\"][value=\"2592000\"]"); + if ((await _cb.count().catch(() => 0)) && (await _cb.isChecked().catch(() => null)) !== true) { + const _lbl = _cb.locator('xpath=ancestor::label[1]'); + if (await _lbl.count().catch(() => 0)) await _lbl.first().click({ timeout: 5000 }).catch(() => {}); + else await _cb.check({ force: true, timeout: 5000 }).catch(() => {}); + } + } + await page.getByRole("button", { name: "paste", exact: true }).click(); + await expect(page).toHaveURL(new RegExp("/[^/]+")); + await expect(page.locator("code").first()).not.toBeEmpty(); +});