Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Optional comma-separated hostnames for proxied Next.js dev server origins.
NEXT_ALLOWED_DEV_ORIGINS=
# Enables local Playwright-only mock sessions. Never set in production.
E2E_AUTH_ENABLED=false

# Server-only configuration
DATABASE_URL=
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

# testing
/coverage
/playwright-report
/test-results

# next.js
/.next/
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,43 @@ Keep machine-specific proxy hostnames and URLs out of committed source.
- `pnpm build`: create a production build.
- `pnpm start`: run the production build.
- `pnpm lint`: run ESLint.
- `pnpm test:e2e`: run Playwright responsive smoke tests.
- `pnpm test:e2e:install`: install the Playwright Chromium browser.
- `pnpm db:generate`: generate Drizzle SQL migrations from the schema.
- `pnpm db:migrate`: apply Drizzle migrations to `DATABASE_URL`.
- `pnpm db:reset:local`: drop and recreate the local `public` schema, then run migrations.
- `pnpm db:studio`: open Drizzle Studio for local database inspection.

## End-to-End QA

The Playwright suite checks public and gated dashboard routes across mobile,
tablet, and desktop viewports. It uses the local database state that is already
present. Detail-page checks skip with a clear message when the local database
does not contain a matching quarter or report.

On a fresh Ubuntu server, install browser system dependencies once:

```bash
pnpm exec playwright install-deps chromium
```

Then install the Chromium browser bundle:

```bash
pnpm test:e2e:install
```

Run the suite:

```bash
pnpm test:e2e
```

The tests start the Next.js dev server with `E2E_AUTH_ENABLED=true` and use a
local-only mock session endpoint for member, cleric, and admin route coverage.
That endpoint returns 404 unless `E2E_AUTH_ENABLED=true` and the app is not
running in production.

## Stack

- Next.js App Router.
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const eslintConfig = defineConfig([
".next/**",
"out/**",
"build/**",
"playwright-report/**",
"test-results/**",
"next-env.d.ts",
]),
]);
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"test:e2e": "playwright test",
"test:e2e:install": "playwright install chromium",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:reset:local": "node scripts/reset-local-db.mjs",
Expand Down Expand Up @@ -43,6 +45,7 @@
"wagmi": "^3.6.16"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pg": "^8.18.0",
Expand Down
56 changes: 56 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { defineConfig, devices } from "@playwright/test";

const port = Number(process.env.PLAYWRIGHT_PORT ?? 3131);
const host = "127.0.0.1";
const baseURL = `http://${host}:${port}`;

export default defineConfig({
expect: {
timeout: 10_000,
},
fullyParallel: false,
outputDir: "test-results/e2e",
reporter: [["list"], ["html", { open: "never" }]],
testDir: "./tests/e2e",
timeout: 45_000,
use: {
baseURL,
trace: "retain-on-failure",
},
webServer: {
command: `E2E_AUTH_ENABLED=true COREPACK_HOME=/tmp/corepack corepack pnpm exec next dev -H ${host} -p ${port}`,
reuseExistingServer: false,
timeout: 120_000,
url: baseURL,
},
projects: [
{
name: "mobile",
use: {
...devices["Pixel 5"],
viewport: { height: 844, width: 390 },
},
},
{
name: "small-mobile",
use: {
...devices["Pixel 5"],
viewport: { height: 740, width: 360 },
},
},
{
name: "tablet",
use: {
...devices["iPad (gen 7)"],
browserName: "chromium",
viewport: { height: 1024, width: 768 },
},
},
{
name: "desktop",
use: {
viewport: { height: 900, width: 1440 },
},
},
],
});
55 changes: 47 additions & 8 deletions pnpm-lock.yaml

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

22 changes: 14 additions & 8 deletions src/app/admin/providers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,8 @@ function ProviderRankingTable({
</span>
</div>
{providers.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full min-w-[680px] text-left text-sm">
<div className="rounded-lg border border-border bg-card p-3 md:p-0">
<table className="mobile-card-table">
<thead className="border-b border-border text-xs uppercase text-muted-foreground">
<tr>
<th className="px-3 py-3 font-medium">Provider</th>
Expand All @@ -501,7 +501,8 @@ function ProviderRankingTable({
key={provider.id}
className="transition-colors hover:bg-muted/50"
>
<td className="p-0">
<td data-label="Provider" data-full="true" className="p-0">
<span className="sr-only">Provider: </span>
<Link
href={href}
scroll={false}
Expand All @@ -510,7 +511,8 @@ function ProviderRankingTable({
{provider.name}
</Link>
</td>
<td className="p-0">
<td data-align="right" data-label="Spend" className="p-0">
<span className="sr-only">Spend: </span>
<Link
href={href}
scroll={false}
Expand All @@ -519,7 +521,8 @@ function ProviderRankingTable({
{formatCurrency(spend?.totalUsd ?? "0")}
</Link>
</td>
<td className="p-0">
<td data-align="right" data-label="Entries" className="p-0">
<span className="sr-only">Entries: </span>
<Link
href={href}
scroll={false}
Expand All @@ -528,7 +531,8 @@ function ProviderRankingTable({
{spend?.entryCount ?? 0}
</Link>
</td>
<td className="p-0">
<td data-label="Website" className="p-0">
<span className="sr-only">Website: </span>
<Link
href={href}
scroll={false}
Expand All @@ -537,7 +541,8 @@ function ProviderRankingTable({
{provider.website || "Not recorded"}
</Link>
</td>
<td className="p-0">
<td data-align="right" data-label="Addresses" className="p-0">
<span className="sr-only">Addresses: </span>
<Link
href={href}
scroll={false}
Expand All @@ -546,7 +551,8 @@ function ProviderRankingTable({
{provider.addresses.length}
</Link>
</td>
<td className="p-0">
<td data-label="Status" className="p-0">
<span className="sr-only">Status: </span>
<Link
href={href}
scroll={false}
Expand Down
Loading
Loading