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
73 changes: 73 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,76 @@ jobs:
- name: Build
working-directory: packages/mcp
run: npm run build

e2e:
# Required gate on any `refs/tags/2026.*` push. PRs and branch pushes
# also run the suite so regressions surface before tag cuts.
runs-on: ubuntu-latest
timeout-minutes: 40

steps:
- uses: actions/checkout@v4
with:
submodules: false

- name: Checkout submodules (shallow)
run: git submodule update --init --depth=1 community

- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'
cache-dependency-path: package-lock.json

- name: Install workspace dependencies
run: npm ci

- name: Start stack (api + dashboard + stubbed carrier + db)
run: docker compose -f docker-compose.e2e.yml up -d --wait
env:
KARRIO_TAG: ${{ github.ref_name == 'main' && 'latest' || 'latest' }}

- name: Install Playwright browsers
working-directory: packages/e2e
run: npx playwright install --with-deps chromium firefox

- name: Seed test tenant
working-directory: packages/e2e
run: npx tsx scripts/seed.ts
env:
KARRIO_API_URL: http://localhost:5002
KARRIO_DASHBOARD_URL: http://localhost:3002

- name: Run Playwright smoke suite
working-directory: packages/e2e
run: npx playwright test specs/
env:
CI: "true"
KARRIO_API_URL: http://localhost:5002
KARRIO_DASHBOARD_URL: http://localhost:3002

- name: Collect docker logs on failure
if: failure()
run: docker compose -f docker-compose.e2e.yml logs --no-color > docker-logs.txt || true

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ github.run_attempt }}
path: packages/e2e/playwright-report
retention-days: 14

- name: Upload traces + videos on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts-${{ github.run_attempt }}
path: |
packages/e2e/test-results
docker-logs.txt
retention-days: 14

- name: Tear down stack
if: always()
run: docker compose -f docker-compose.e2e.yml down -v
96 changes: 96 additions & 0 deletions docker-compose.e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Minimised compose for the Playwright smoke suite.
#
# Goals:
# - Fastest possible cold-start: Postgres + API + dashboard + carrier stub.
# - No Redis: API runs the worker in-process (DETACHED_WORKER=false).
# - Carrier stub: WireMock serving canned rate/label/tracking JSON so
# specs never touch real carrier endpoints.
# - Healthchecks drive `docker compose up --wait` in CI.
#
# Image tags default to the `latest` published build on Scarf; override with
# `KARRIO_TAG=<tag>` to pin a commit-specific image.
#
# Usage:
# docker compose -f docker-compose.e2e.yml up -d --wait
# docker compose -f docker-compose.e2e.yml down -v

services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: karrio
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d karrio"]
interval: 3s
timeout: 3s
retries: 20

carrier-stub:
# WireMock stands in for real carrier endpoints. Mappings live in
# packages/e2e/fixtures/wiremock and are mounted read-only.
image: wiremock/wiremock:3.9.1
command: ["--port", "8080", "--disable-banner"]
volumes:
- ./packages/e2e/fixtures/wiremock:/home/wiremock/mappings:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/__admin/health || exit 1"]
interval: 3s
timeout: 3s
retries: 10

api:
image: karrio.docker.scarf.sh/karrio/server:${KARRIO_TAG:-latest}
depends_on:
db:
condition: service_healthy
carrier-stub:
condition: service_healthy
environment:
SECRET_KEY: "e2e-smoke-test-secret-key-not-for-production"
DEBUG_MODE: "True"
DETACHED_WORKER: "False"
DATABASE_ENGINE: "postgresql_psycopg2"
DATABASE_HOST: db
DATABASE_PORT: "5432"
DATABASE_NAME: karrio
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: postgres
KARRIO_HTTP_PORT: "5002"
ADMIN_EMAIL: admin@example.com
ADMIN_PASSWORD: demo
ENABLE_ALL_PLUGINS_BY_DEFAULT: "True"
LOG_LEVEL: "30"
KARRIO_E2E_CARRIER_STUB_URL: "http://carrier-stub:8080"
ports:
- "5002:5002"
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:5002/ || exit 1"]
interval: 5s
timeout: 5s
retries: 40
start_period: 30s

dashboard:
image: karrio.docker.scarf.sh/karrio/dashboard:${KARRIO_TAG:-latest}
depends_on:
api:
condition: service_healthy
environment:
AUTH_TRUST_HOST: "true"
NEXTAUTH_SECRET: "e2e-smoke-test-nextauth-secret"
DASHBOARD_PORT: "3002"
NEXT_PUBLIC_DASHBOARD_URL: "http://localhost:3002"
NEXT_PUBLIC_KARRIO_PUBLIC_URL: "http://localhost:5002"
KARRIO_URL: "http://api:5002"
ports:
- "3002:3002"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3002/signin || exit 1"]
interval: 5s
timeout: 5s
retries: 40
start_period: 30s
93 changes: 50 additions & 43 deletions packages/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,71 @@
# @karrio/e2e — Playwright E2E Tests

End-to-end tests for the Karrio dashboard at `localhost:3002`.

> **Note:** `node_modules` are not committed. Run `npm install` before executing
> tests, and `npx playwright install chromium` on first run.
End-to-end tests for the Karrio dashboard (default: `localhost:3002`) and
API (default: `localhost:5002`).

> **Note:** `node_modules` are not committed. Run `npm install` at the repo
> root before executing tests, and `npx playwright install chromium firefox`
> on first run.

## Layout

| Path | Purpose |
|------|---------|
| `playwright.config.ts` | Chromium + Firefox, retries on CI, trace/video capture |
| `fixtures/auth.ts` | Extended Playwright test with a REST `api` fixture |
| `fixtures/wiremock/` | Canned carrier JSON served by WireMock in CI |
| `helpers/env.ts` | Centralised env-var resolution |
| `helpers/api.ts` | Thin REST client (JWT bearer auth) |
| `helpers/selectors.ts` | Shared role-based locators |
| `helpers/wait-for-stack.ts` | Polls API + dashboard until healthy |
| `tests/auth.setup.ts` | Persists NextAuth session to storageState |
| `tests/rate-sheet-editor.spec.ts` | Legacy rate-sheet editor suite |
| `specs/*.spec.ts` | Golden-path smoke suite (auth, shipment, tracking, order, settings) |
| `scripts/seed.ts` | CI-only data seeding via REST |

## Setup

```bash
cd karrio/packages/e2e
cd karrio
npm install
npx playwright install chromium # first time only
cd packages/e2e
npx playwright install chromium firefox # first time only
```

## Run (requires dev server on localhost:3002)
## Run against a running dev stack

```bash
npm test # headless
npm run test:headed # headed (see browser)
npm run test:ui # Playwright UI / trace viewer
npm test # full suite (chromium + firefox)
npm run test:smoke # specs/ only (chromium)
npm run test:headed # see the browser
npm run test:ui # Playwright UI / trace viewer
```

## Auth
## Run against the CI compose stack

Tests log in once via `helpers/auth.ts` and reuse the session via
`playwright/.auth/user.json` (auto-created, git-ignored).
```bash
docker compose -f docker-compose.e2e.yml up -d --wait
npx tsx packages/e2e/scripts/seed.ts
npm --prefix packages/e2e test
docker compose -f docker-compose.e2e.yml down -v
```

Set credentials via env vars or use the defaults:
## Env overrides

| Env var | Default |
|-------------------|------------------------|
| `KARRIO_EMAIL` | `admin@example.com` |
| `KARRIO_PASSWORD` | `demo` |
| Env var | Default |
|---------|---------|
| `KARRIO_EMAIL` | `admin@example.com` |
| `KARRIO_PASSWORD` | `demo` |
| `KARRIO_DASHBOARD_URL` | `http://localhost:3002` |
| `KARRIO_API_URL` | `http://localhost:5002` |

## Carrier stubbing

## Test Targets

| Test # | Description | URL |
|--------|-------------|-----|
| 1 | Carrier Network page loads | `/admin/carriers` |
| 2 | Rate Sheets tab switch | `/admin/carriers` |
| 3 | Create Rate Sheet editor opens | `/admin/carriers` |
| 4 | Mode buttons (edit/import/export) visible | editor panel |
| 5 | Switch to import mode → file input | editor panel |
| 6 | Upload valid xlsx → diff preview | editor import panel |
| 7 | Upload error xlsx → validation errors | editor import panel |
| 8 | Cancel import → returns to edit mode | editor import panel |
| 9 | Export button triggers download | editor panel |
| 10 | Connections rate-sheets page loads | `/connections/rate-sheets` |
| 11 | Escape key closes editor | editor panel |
| 12 | Close button dismisses editor | editor panel |

## Fixtures

| File | Description |
|------|-------------|
| `fixtures/rate-sheet-valid.xlsx` | Valid rate sheet — dry-run succeeds, diff preview shown |
| `fixtures/rate-sheet-errors.xlsx` | Invalid data — validation errors shown |
| `fixtures/rate-sheet-updated.xlsx` | Updated rates — for confirm-import flow |
The smoke suite never touches real carrier APIs. In CI the compose stack
launches a `wiremock/wiremock` container with mappings from
`fixtures/wiremock/` returning canned rate/shipment/tracking JSON.

## CI

Set `CI=true` to enable GitHub reporter and 2 retries per test.
Runs on every push + PR in `.github/workflows/tests.yml` under the `e2e` job.
Reports, traces, and videos are uploaded as artifacts on failure.
25 changes: 25 additions & 0 deletions packages/e2e/fixtures/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { test as base, expect } from "@playwright/test";
import { KarrioApi } from "../helpers/api";

/**
* Extended Playwright test that provides a shared KarrioApi REST client
* authenticated as the default admin.
*
* Browser-side auth (NextAuth session cookies) is handled by the
* `setup` project in playwright.config.ts — this fixture is for
* REST-backed seeding (addresses, parcels, carrier connections, etc.).
*/
type Fixtures = {
api: KarrioApi;
};

export const test = base.extend<Fixtures>({
api: async ({}, use) => {
const api = new KarrioApi();
await api.login();
await use(api);
await api.dispose();
},
});

export { expect };
23 changes: 23 additions & 0 deletions packages/e2e/fixtures/wiremock/rate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"request": {
"method": "POST",
"urlPathPattern": "/(rate|rates|rating).*"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"rates": [
{
"service": "e2e_standard",
"service_name": "E2E Standard",
"total_charge": 12.5,
"currency": "USD",
"transit_days": 3,
"carrier_id": "e2e-stub",
"carrier_name": "e2e_stub"
}
]
}
}
}
18 changes: 18 additions & 0 deletions packages/e2e/fixtures/wiremock/shipment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"request": {
"method": "POST",
"urlPathPattern": "/(ship|shipment|labels).*"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"tracking_number": "E2ESMOKE123456",
"shipment_identification_number": "E2ESMOKE123456",
"label": "JVBERi0xLjQKJcOkw7zDtsOfCjI=",
"label_type": "PDF",
"service": "e2e_standard",
"total_charge": { "amount": 12.5, "currency": "USD" }
}
}
}
30 changes: 30 additions & 0 deletions packages/e2e/fixtures/wiremock/tracking.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"request": {
"method": "GET",
"urlPathPattern": "/(track|tracking|status).*"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"tracking_number": "E2ESMOKE123456",
"status": "in_transit",
"events": [
{
"date": "2026-04-18",
"time": "10:00",
"code": "IN_TRANSIT",
"description": "Package scanned at origin facility",
"location": "Los Angeles, CA"
},
{
"date": "2026-04-19",
"time": "08:00",
"code": "OUT_FOR_DELIVERY",
"description": "Out for delivery",
"location": "New York, NY"
}
]
}
}
}
Loading
Loading