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 microbin
run: |
cargo build --release
./target/release/microbin &
for i in $(seq 1 60); do
if curl -sSf http://localhost:8080/ >/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:8080
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/
34 changes: 34 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# microbin end-to-end tests

Functional browser tests for microbin'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
microbin instance over HTTP, so they add coverage without changing anything about how microbin
is built or tested today.

## Run locally

```bash
# 1. Start microbin, e.g.:
docker run -p 8080:8080 danielszabo99/microbin

# 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:8080
```

Point the suite at any instance with `BASE_URL`:

```bash
BASE_URL=http://localhost:8080 npx playwright test
```

## What's covered
- **Create & view a paste** — fill the upload form and submit, then follow the redirect to the paste permalink and assert the content is rendered back.
- **Create after visiting a paste permalink** — load a paste permalink first, then create and view a paste.
- **Create from the paste list** — open the `/list` view, then create and view a paste.
- **Create from the guide page** — open the `/guide` page, then create and view a paste.
- **Create via list -> guide** — visit the list then the guide, then create and view a paste.
- **Create via guide -> list** — visit the guide then the list, then create and view a paste.
- **Create after the remove route** — hit the `/remove/{id}` route, then create and view a paste.
- **Create after the edit route** — hit the `/edit/{id}` route, then create and view a paste.
20 changes: 20 additions & 0 deletions e2e/microbin-2.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test';

test("microbin-2", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
22 changes: 22 additions & 0 deletions e2e/microbin-3.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';

test("microbin-3", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/list");
await expect(page).toHaveURL(new RegExp("/list"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
22 changes: 22 additions & 0 deletions e2e/microbin-4.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';

test("microbin-4", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/guide");
await expect(page).toHaveURL(new RegExp("/guide"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
24 changes: 24 additions & 0 deletions e2e/microbin-5.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test';

test("microbin-5", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/list");
await expect(page).toHaveURL(new RegExp("/list"));
await page.goto("/guide");
await expect(page).toHaveURL(new RegExp("/guide"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
24 changes: 24 additions & 0 deletions e2e/microbin-6.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test';

test("microbin-6", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/guide");
await expect(page).toHaveURL(new RegExp("/guide"));
await page.goto("/list");
await expect(page).toHaveURL(new RegExp("/list"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
22 changes: 22 additions & 0 deletions e2e/microbin-7.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';

test("microbin-7", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/remove/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/remove/monkey\\-dog\\-bat"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
22 changes: 22 additions & 0 deletions e2e/microbin-8.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';

test("microbin-8", async ({ page }) => {
await page.goto("/upload/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/upload/monkey\\-dog\\-bat"));
await page.goto("/edit/monkey-dog-bat");
await expect(page).toHaveURL(new RegExp("/edit/monkey\\-dog\\-bat"));
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
18 changes: 18 additions & 0 deletions e2e/microbin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test, expect } from '@playwright/test';

test("microbin", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveURL(new RegExp("/"));
// Let the page settle, then dismiss any welcome/consent overlay before interacting.
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await page.locator("#burn_after").selectOption("1");
if (await page.locator("#content-input").count().catch(() => 0)) await page.locator("#content-input").fill("test", { timeout: 5000 }).catch(() => {});
await page.locator("#expiration").selectOption("1min");
if (await page.locator("#password_field").count().catch(() => 0)) await page.locator("#password_field").fill("Test1234!Qa", { timeout: 5000 }).catch(() => {});
await page.locator("#privacy").selectOption("unlisted");
await page.locator("#syntax_highlight").selectOption("auto");
await page.locator("#submit-button").click();
await expect(page).toHaveURL(new RegExp("/upload/[^/]+"));
await expect(page.locator("code").first()).not.toBeEmpty();
});
76 changes: 76 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" } }
8 changes: 8 additions & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: ".",
use: { baseURL: process.env.BASE_URL || "http://localhost:8080", trace: "on-first-retry" },
reporter: [["list"]],
timeout: 30000,
expect: { timeout: 10000 },
});