Skip to content

Commit e69a484

Browse files
authored
Merge pull request #2115 from blockful/test/dashboard-synthetic-tests
test(dashboard): deflake e2e suite and add checkly synthetic monitoring
2 parents 8013fab + d4eacd6 commit e69a484

20 files changed

Lines changed: 2164 additions & 523 deletions

.changeset/common-wombats-cheat.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Checkly deploy
2+
3+
# Keeps the Checkly monitors in sync with the repo. `checkly deploy` is the
4+
# only sanctioned write path for monitor config: edits made in the Checkly UI
5+
# are overwritten by the next run of this workflow.
6+
on:
7+
push:
8+
branches: [main]
9+
paths:
10+
- "apps/dashboard/e2e/**"
11+
- "apps/dashboard/checkly.config.ts"
12+
- "apps/dashboard/playwright.config.ts"
13+
- "apps/dashboard/package.json"
14+
- ".github/workflows/checkly-deploy.yaml"
15+
workflow_dispatch:
16+
17+
jobs:
18+
checkly-deploy:
19+
name: Deploy Checkly monitors
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 10
22+
env:
23+
CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
24+
CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@v4
28+
29+
- name: Setup pnpm
30+
uses: pnpm/action-setup@v4
31+
with:
32+
version: 10.10.0
33+
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: "20"
38+
cache: "pnpm"
39+
40+
- name: Install dependencies
41+
run: pnpm install --frozen-lockfile
42+
43+
# Guarded so the workflow is a no-op until the Checkly account secrets
44+
# are configured on the repo.
45+
- name: Deploy checks
46+
if: env.CHECKLY_API_KEY != ''
47+
working-directory: apps/dashboard
48+
run: pnpm exec checkly deploy --force

.github/workflows/tests.yaml

Lines changed: 89 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -456,65 +456,92 @@ jobs:
456456
flags: api
457457
fail_ci_if_error: false
458458

459-
# dashboard-e2e:
460-
# name: Dashboard E2E
461-
# needs: wait-for-gateful
462-
# if: ${{ !(startsWith(github.head_ref, 'changeset-release/') && github.actor == 'github-actions[bot]' && github.event.pull_request.user.login == 'github-actions[bot]' && github.event.pull_request.head.repo.full_name == github.repository) }}
463-
# runs-on: ubuntu-latest
464-
# timeout-minutes: 20
465-
466-
# steps:
467-
# - name: Checkout repository
468-
# uses: actions/checkout@v4
469-
# with:
470-
# fetch-depth: 0
471-
472-
# - name: Setup pnpm
473-
# uses: pnpm/action-setup@v4
474-
# with:
475-
# version: 10.10.0
476-
477-
# - name: Setup Node.js
478-
# uses: actions/setup-node@v4
479-
# with:
480-
# node-version: "20"
481-
# cache: "pnpm"
482-
483-
# - name: Install dependencies
484-
# run: pnpm install --frozen-lockfile
485-
486-
# - name: Cache Playwright browsers
487-
# uses: actions/cache@v4
488-
# with:
489-
# path: ~/.cache/ms-playwright
490-
# key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
491-
# restore-keys: |
492-
# playwright-${{ runner.os }}-
493-
494-
# - name: Install Playwright browsers
495-
# run: pnpm --filter @anticapture/dashboard exec playwright install --with-deps chromium
496-
497-
# - name: Run dashboard E2E tests
498-
# run: pnpm dashboard test:e2e
499-
# env:
500-
# CI: "true"
501-
# NEXT_PUBLIC_WC_PROJECT_ID: ${{ secrets.E2E_NEXT_PUBLIC_WC_PROJECT_ID }}
502-
# BLOCKFUL_API_TOKEN: ${{ secrets.E2E_BLOCKFUL_API_TOKEN }}
503-
# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
504-
# TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
505-
506-
# - name: Upload Playwright report
507-
# if: failure()
508-
# uses: actions/upload-artifact@v4
509-
# with:
510-
# name: playwright-report
511-
# path: apps/dashboard/playwright-report
512-
# retention-days: 14
513-
514-
# - name: Upload Playwright traces
515-
# if: failure()
516-
# uses: actions/upload-artifact@v4
517-
# with:
518-
# name: playwright-test-results
519-
# path: apps/dashboard/test-results
520-
# retention-days: 14
459+
dashboard-e2e:
460+
name: Dashboard E2E
461+
needs: [configure-vercel-preview, wait-for-gateful]
462+
if: ${{ !(startsWith(github.head_ref, 'changeset-release/') && github.actor == 'github-actions[bot]' && github.event.pull_request.user.login == 'github-actions[bot]' && github.event.pull_request.head.repo.full_name == github.repository) }}
463+
runs-on: ubuntu-latest
464+
timeout-minutes: 20
465+
466+
steps:
467+
- name: Checkout repository
468+
uses: actions/checkout@v4
469+
with:
470+
fetch-depth: 0
471+
472+
- name: Setup pnpm
473+
uses: pnpm/action-setup@v4
474+
with:
475+
version: 10.10.0
476+
477+
- name: Setup Node.js
478+
uses: actions/setup-node@v4
479+
with:
480+
node-version: "20"
481+
cache: "pnpm"
482+
483+
- name: Install dependencies
484+
run: pnpm install --frozen-lockfile
485+
486+
- name: Cache Playwright browsers
487+
uses: actions/cache@v4
488+
with:
489+
path: ~/.cache/ms-playwright
490+
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
491+
restore-keys: |
492+
playwright-${{ runner.os }}-
493+
494+
- name: Install Playwright browsers
495+
run: pnpm --filter @anticapture/dashboard exec playwright install --with-deps chromium
496+
497+
# Gateful tokens are revocable server-side, so a stale secret used to
498+
# surface as 9 minutes of data specs dying on 401s. Probe once up front
499+
# and fail in seconds with the actual fix instead. (Reading the token
500+
# from Vercel is not an option: the env API returns ciphertext for
501+
# encrypted values; only deployments receive plaintext.)
502+
- name: Check the gateful token still authenticates
503+
shell: bash
504+
env:
505+
E2E_TOKEN: ${{ secrets.E2E_BLOCKFUL_API_TOKEN }}
506+
GATEFUL_URL: https://dev-gateful.up.railway.app
507+
run: |
508+
set -euo pipefail
509+
code=$(curl --silent --output /dev/null --write-out "%{http_code}" \
510+
--header "Authorization: Bearer ${E2E_TOKEN}" \
511+
"${GATEFUL_URL}/ens/proposals?limit=1&skip=0" || echo "000")
512+
echo "${GATEFUL_URL}: HTTP ${code}"
513+
if [ "${code}" != "200" ]; then
514+
echo "::error::E2E_BLOCKFUL_API_TOKEN no longer authenticates against ${GATEFUL_URL}. Update it with a valid dev token: gh secret set E2E_BLOCKFUL_API_TOKEN"
515+
exit 1
516+
fi
517+
518+
- name: Run dashboard E2E tests
519+
run: pnpm dashboard test:e2e
520+
env:
521+
CI: "true"
522+
# Shared dev services, NOT the PR's own preview stack: previews run
523+
# their own authful, so the dev-issued e2e token gets 401s there.
524+
ANTICAPTURE_API_URL: https://dev-gateful.up.railway.app
525+
# The /api/user auth/session proxy throws on boot without this, and
526+
# the resulting same-origin 500s trip the suite's 5xx watcher.
527+
USER_API_URL: https://user-api-dev-9002.up.railway.app
528+
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ${{ secrets.E2E_NEXT_PUBLIC_WC_PROJECT_ID }}
529+
BLOCKFUL_API_TOKEN: ${{ secrets.E2E_BLOCKFUL_API_TOKEN }}
530+
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
531+
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
532+
533+
- name: Upload Playwright report
534+
if: failure()
535+
uses: actions/upload-artifact@v4
536+
with:
537+
name: playwright-report
538+
path: apps/dashboard/playwright-report
539+
retention-days: 14
540+
541+
- name: Upload Playwright traces
542+
if: failure()
543+
uses: actions/upload-artifact@v4
544+
with:
545+
name: playwright-test-results
546+
path: apps/dashboard/test-results
547+
retention-days: 14

apps/api/vitest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export default defineConfig({
55
test: {
66
globals: true,
77
environment: "node",
8+
// PGlite-backed suites boot a WASM Postgres in beforeAll; cold CI runners
9+
// regularly blow through the 10s default.
10+
hookTimeout: 30_000,
811
setupFiles: ["./vitest.setup.ts"],
912
coverage: {
1013
provider: "v8",

apps/dashboard/checkly.config.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { defineConfig } from "checkly";
2+
import { AlertChannel, Frequency } from "checkly/constructs";
3+
4+
// Channels are created/authorized in the Checkly UI (Slack needs an OAuth
5+
// grant); the config subscribes to them by ID so a deploy never drops them.
6+
const SLACK_ALERT_CHANNEL = AlertChannel.fromId(318090); // #anticapture-alerts-prod
7+
const EMAIL_ALERT_CHANNEL = AlertChannel.fromId(318084); // shared@blockful.io
8+
9+
/**
10+
* Synthetic monitoring for the production dashboard (DEV-1132).
11+
*
12+
* The @smoke-tagged subset of the Playwright E2E suite runs hourly against
13+
* https://app.anticapture.com (the base URL switches automatically when
14+
* CHECKLY=1 — see playwright.config.ts). Hourly keeps the month at ~720 of
15+
* the free tier's 1,000 browser check runs.
16+
*
17+
* `checkly deploy` is the only sanctioned write path for this monitor; edits
18+
* made in the Checkly UI are overwritten on the next deploy. Alert channels
19+
* (Slack + email) are account-level settings configured in Checkly itself.
20+
*/
21+
export default defineConfig({
22+
projectName: "Anticapture Dashboard",
23+
logicalId: "anticapture-dashboard",
24+
checks: {
25+
playwrightConfigPath: "./playwright.config.ts",
26+
alertChannels: [SLACK_ALERT_CHANNEL, EMAIL_ALERT_CHANNEL],
27+
playwrightChecks: [
28+
{
29+
name: "anticapture-dashboard",
30+
logicalId: "anticapture-dashboard",
31+
pwTags: ["@smoke"],
32+
frequency: Frequency.EVERY_1H,
33+
locations: ["us-east-1", "eu-west-1"],
34+
},
35+
],
36+
},
37+
cli: {
38+
runLocation: "us-east-1",
39+
},
40+
});

apps/dashboard/e2e/activity-feed.spec.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { test, expect } from "./fixtures";
22

33
test.describe("Activity Feed page (/ens/activity-feed)", () => {
4-
test("renders Activity Feed heading and description", async ({
5-
goto,
6-
page,
7-
}) => {
8-
await goto("/ens/activity-feed");
9-
await expect(
10-
page.locator("h4").filter({ hasText: "Activity Feed" }),
11-
).toBeVisible();
12-
await expect(
13-
page.locator("text=Surfaces governance activity"),
14-
).toBeVisible();
15-
});
4+
test(
5+
"renders Activity Feed heading and description",
6+
{ tag: "@smoke" },
7+
async ({ goto, page }) => {
8+
await goto("/ens/activity-feed");
9+
await expect(
10+
page.locator("h4").filter({ hasText: "Activity Feed" }),
11+
).toBeVisible();
12+
await expect(
13+
page.locator("text=Surfaces governance activity"),
14+
).toBeVisible();
15+
},
16+
);
1617

1718
test("shows Filters button", async ({ goto, page }) => {
1819
await goto("/ens/activity-feed");

apps/dashboard/e2e/dao-overview.spec.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import { test, expect } from "./fixtures";
22

33
test.describe("DAO Overview page (/ens)", () => {
4-
test("renders DAO Overview page", async ({ goto, page }) => {
5-
await goto("/ens");
6-
await expect(page).toHaveURL(/\/ens(\?.*)?$/);
7-
await expect(page.locator("h3").filter({ hasText: "ENS" })).toBeVisible({
8-
timeout: 15_000,
9-
});
10-
});
4+
// exact: true — proposal cards on the page can also render h3s containing
5+
// "ENS", so a substring match hits a strict mode violation with live data.
6+
test(
7+
"renders DAO Overview page",
8+
{ tag: "@smoke" },
9+
async ({ goto, page }) => {
10+
await goto("/ens");
11+
await expect(page).toHaveURL(/\/ens(\?.*)?$/);
12+
await expect(
13+
page.getByRole("heading", { name: "ENS", exact: true }),
14+
).toBeVisible({ timeout: 15_000 });
15+
},
16+
);
1117

1218
test("renders ENS DAO header with name and metrics", async ({
1319
goto,
1420
page,
1521
}) => {
1622
await goto("/ens");
17-
// ENS name heading
18-
await expect(page.locator("h3").filter({ hasText: "ENS" })).toBeVisible({
19-
timeout: 15_000,
20-
});
23+
await expect(
24+
page.getByRole("heading", { name: "ENS", exact: true }),
25+
).toBeVisible({ timeout: 15_000 });
2126
});
2227

2328
test("shows header metric cards", async ({ goto, page }) => {
@@ -107,9 +112,9 @@ test.describe("DAO Overview page (/ens)", () => {
107112
page,
108113
}) => {
109114
await goto("/ens");
110-
await expect(page.locator("h3").filter({ hasText: "ENS" })).toBeVisible({
111-
timeout: 15_000,
112-
});
115+
await expect(
116+
page.getByRole("heading", { name: "ENS", exact: true }),
117+
).toBeVisible({ timeout: 15_000 });
113118
// recharts renders into a wrapper div
114119
const charts = page.locator(
115120
".recharts-wrapper, .recharts-responsive-container, svg.recharts-surface",

0 commit comments

Comments
 (0)