Skip to content
Closed
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
33 changes: 24 additions & 9 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: copy backend env
run: cp .env.example .env
working-directory: backend
- name: copy frontend env
run: cp .env.example .env
working-directory: frontend
- name: copy env files
run: cp backend/.env.example backend/.env && cp frontend/.env.example frontend/.env
- run: bun install
- name: compile backend
run: bun compile
Expand Down Expand Up @@ -60,17 +56,36 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: cp backend/.env.example backend/.env
- run: cp backend/.env.example backend/.env && cp frontend/.env.example frontend/.env
- run: bun test-backend

test-frontend:
runs-on: ubuntu-latest

services:
redis:
image: redis
ports:
- 6379:6379
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: bun-test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: cp backend/.env.example backend/.env
- run: bun test-frontend
- run: cp backend/.env.example backend/.env && cp frontend/.env.example frontend/.env
- run: bun run test-frontend

complete:
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ next.sock
backend/.env
frontend/.env
types

# Playwright
test-results
playwright-report
blob-report
playwright/.cache
Binary file modified bun.lockb
Binary file not shown.
7 changes: 0 additions & 7 deletions frontend/__tests__/smoketest.test.ts

This file was deleted.

1 change: 1 addition & 0 deletions frontend/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default function App() {
<td className="text-start">Signed in as {user.name}</td>
<td className="text-end">
<Button
data-testid="signout-button"
size="sm"
variant="outline-secondary"
onClick={handleLogout}
Expand Down
17 changes: 13 additions & 4 deletions frontend/components/ChatCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,22 @@ export default function ChatCard({
<>
<Row>
<Col>
<Form onSubmit={sendMessage}>
<Form data-testid="chat-form" onSubmit={sendMessage}>
<Form.Group className="mb-3" controlId="body">
<Form.Label>Message</Form.Label>
<Form.Control type="text" placeholder="Message" />
<Form.Control
data-testid="chat-message"
type="text"
placeholder="Message"
/>
</Form.Group>

<Button variant="primary" type="submit" disabled={!connected}>
<Button
data-testid="chat-send"
variant="primary"
type="submit"
disabled={!connected}
>
Send
</Button>
</Form>
Expand All @@ -119,7 +128,7 @@ export default function ChatCard({
<hr />

<Row>
<Table striped bordered hover>
<Table data-testid="messages-table" striped bordered hover>
<thead>
<tr>
<th>Who</th>
Expand Down
16 changes: 12 additions & 4 deletions frontend/components/SessionCreateCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,26 @@ export const SessionCreateCard = ({

return (
<div>
<Form onSubmit={handleForm}>
<Form data-testid="signin-form" onSubmit={handleForm}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control type="text" placeholder="Enter Email Address" />
<Form.Control
data-testid="signin-email"
type="text"
placeholder="Enter Email Address"
/>
</Form.Group>

<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control type="password" placeholder="Enter Password" />
<Form.Control
data-testid="signin-password"
type="password"
placeholder="Enter Password"
/>
</Form.Group>

<Button variant="primary" type="submit">
<Button data-testid="signin-submit" variant="primary" type="submit">
Submit
</Button>
</Form>
Expand Down
22 changes: 17 additions & 5 deletions frontend/components/SignUpCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,35 @@ export const SignUpCard = ({

return (
<div>
<Form onSubmit={handleForm}>
<Form data-testid="signup-form" onSubmit={handleForm}>
<Form.Group className="mb-3" controlId="name">
<Form.Label>Name</Form.Label>
<Form.Control type="text" placeholder="Enter Name" />
<Form.Control
data-testid="signup-name"
type="text"
placeholder="Enter Name"
/>
</Form.Group>

<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control type="text" placeholder="Enter Email Address" />
<Form.Control
data-testid="signup-email"
type="text"
placeholder="Enter Email Address"
/>
</Form.Group>

<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control type="password" placeholder="Enter Password" />
<Form.Control
data-testid="signup-password"
type="password"
placeholder="Enter Password"
/>
</Form.Group>

<Button variant="primary" type="submit">
<Button data-testid="signup-submit" variant="primary" type="submit">
Submit
</Button>
</Form>
Expand Down
27 changes: 20 additions & 7 deletions frontend/components/StatusCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,36 @@ export const StatusCard = () => {
};

return (
<Card bg="primary">
<Card bg="primary" data-testid="status-card">
<Card.Header>Server Status</Card.Header>
<Card.Body>
<code>
<strong>Name</strong>: {status?.name}
<strong>Name</strong>:{" "}
<span data-testid="status-name">{status?.name ?? ""}</span>
<br />
<strong>PID</strong>: {status?.pid}
<strong>PID</strong>:{" "}
<span data-testid="status-pid">{status?.pid ?? ""}</span>
<br />
<strong>Version</strong>: {status?.version}
<strong>Version</strong>:{" "}
<span data-testid="status-version">{status?.version ?? ""}</span>
<br />
<strong>Uptime</strong>: {status?.uptime ? status.uptime / 1000 : 0}s
<strong>Uptime</strong>:{" "}
<span data-testid="status-uptime">
{status?.uptime ? status.uptime / 1000 : 0}
</span>
s
<br />
<strong>Memory Used</strong>: {status?.consumedMemoryMB}MB
<strong>Memory Used</strong>:{" "}
<span data-testid="status-memory">
{status?.consumedMemoryMB ?? ""}
</span>
MB
</code>
</Card.Body>

<Button onClick={loadStatus}>Refresh</Button>
<Button data-testid="status-refresh" onClick={loadStatus}>
Refresh
</Button>
</Card>
);
};
2 changes: 1 addition & 1 deletion frontend/components/SwaggerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ export const SwaggerCard = () => {
});
}, []);

return <div id="swaggerContainer" />;
return <div id="swaggerContainer" data-testid="swagger-container" />;
};
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "playwright test",
"test:ci": "playwright install --with-deps chromium && playwright test",
"lint": "prettier --check .",
"format": "prettier --write ."
},
Expand All @@ -20,6 +22,7 @@
"typescript": "^5.4.3"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "22.7.5",
"@types/react-dom": "^18.3.0",
Expand Down
69 changes: 69 additions & 0 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { defineConfig, devices } from "@playwright/test";
import path from "node:path";
import { fileURLToPath } from "node:url";

const isCI = !!process.env.CI;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function loadNumberFromEnvIfSet(key: string, defaultValue: number): number {
const raw = process.env[key];
if (!raw) return defaultValue;
const num = Number(raw);
return Number.isFinite(num) ? num : defaultValue;
}

// Prefer a dedicated test port; allow overrides via env (similar spirit to backend config).
const frontendPort = loadNumberFromEnvIfSet("PLAYWRIGHT_PORT", 3100);
const baseURL =
process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${frontendPort}`;
const backendPort = loadNumberFromEnvIfSet("WEB_SERVER_PORT_TEST", 8090);
const backendBaseUrl = `http://127.0.0.1:${backendPort}`;

export default defineConfig({
testDir: "./playwright",
timeout: 30_000,
expect: { timeout: 10_000 },
fullyParallel: true,
retries: isCI ? 1 : 0,
use: {
baseURL,
trace: "on-first-retry",
},
webServer: [
{
command: "bun run start",
cwd: path.resolve(__dirname, "../backend"),
url: `${backendBaseUrl}/api/status`,
env: {
NODE_ENV: "test",
WEB_SERVER_PORT_TEST: String(backendPort),
// Make CORS compatible with credentials: "include" in the frontend.
// Using "*" with Access-Control-Allow-Credentials breaks browser requests.
WEB_SERVER_ALLOWED_ORIGINS: baseURL,
},
reuseExistingServer: !isCI,
timeout: 120_000,
},
{
command: `bun run build && PORT=${frontendPort} bun run start`,
cwd: __dirname,
url: baseURL,
env: {
NODE_ENV: "test",
// Ensure the app can reach the backend during build/runtime.
// Must NOT include "/api" because the websocket appends it.
NEXT_PUBLIC_API_URL:
process.env.NEXT_PUBLIC_API_URL ?? backendBaseUrl,
},
reuseExistingServer: !isCI,
timeout: 120_000,
},
],
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
48 changes: 48 additions & 0 deletions frontend/playwright/happy-path.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, test } from "@playwright/test";

test("happy path: sign up, sign in, send a message", async ({ page }) => {
const runId = Date.now();
const name = `Playwright User ${runId}`;
const email = `pw_${runId}@example.com`;
const password = `password-${runId}!`;

await page.goto("/");
await expect(
page.getByRole("heading", { name: "Bun Actionhero" }),
).toBeVisible();

// Sign up (creates user account)
const signUp = page.getByTestId("signup-form");
await signUp.getByTestId("signup-name").fill(name);
await signUp.getByTestId("signup-email").fill(email);
await signUp.getByTestId("signup-password").fill(password);
await signUp.getByTestId("signup-submit").click();

await expect(page.getByText(`Hello ${name}!`)).toBeVisible({ timeout: 5000 });

// Reload so the app returns to the sign-in state (signup doesn't create a session)
await page.reload();

// Sign in (creates session + localStorage userId)
const signIn = page.getByTestId("signin-form");
await signIn.getByTestId("signin-email").fill(email);
await signIn.getByTestId("signin-password").fill(password);
await signIn.getByTestId("signin-submit").click();

await expect(page.getByText(`Welcome back, ${name}!`)).toBeVisible({
timeout: 5000,
});
await expect(page.getByText(`Signed in as ${name}`)).toBeVisible();

// Send a message via websocket and ensure it renders in the table
const message = `hello from playwright ${runId}`;
await expect(page.getByTestId("chat-send")).toBeEnabled({ timeout: 10_000 });
await page.getByTestId("chat-message").fill(message);
await page.getByTestId("chat-send").click();

await expect(
page
.getByTestId("messages-table")
.getByRole("cell", { name: message, exact: true }),
).toBeVisible({ timeout: 10_000 });
});
Loading
Loading