Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
test-results/
playwright-report/
blob-report/
playwright/.cache/
27 changes: 27 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 58 additions & 0 deletions e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{ "name": "oss-genscript-suite", "private": true,
"devDependencies": { "@playwright/test": "^1.48.0" } }
19 changes: 19 additions & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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 },
});
28 changes: 28 additions & 0 deletions e2e/wastebin-qa-sentinal-record.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});