Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
766486f
first pass at playwright testing
buckhalt Mar 4, 2025
c73b497
working env.test.local setup for next and prisma
buckhalt Mar 5, 2025
c4dc8d9
working e2e testing setup process
buckhalt Mar 5, 2025
56440ab
move app setup to global setup
buckhalt Mar 5, 2025
7000120
upade playwright test command, ignore playwright in vitest
buckhalt Mar 5, 2025
4e2103b
add gh repository secrets
buckhalt Mar 5, 2025
9052f48
knip ignore setup and teardown
buckhalt Mar 5, 2025
9b7f85b
attempt at mocking uploadthing by intercepting request
buckhalt Mar 6, 2025
0e2a7af
revert intercepting uploadthing requests
buckhalt Mar 7, 2025
a318187
skip env validation in playwright ci
buckhalt Mar 7, 2025
ea56854
change command from docker-compose to docker compose
buckhalt Mar 7, 2025
94cbe9d
create test env file
buckhalt Mar 7, 2025
fef37fa
use dev dep dotenv-cli in db setup to ensure correct version is used …
buckhalt Mar 7, 2025
376a946
ci can use .env
buckhalt Mar 7, 2025
7140bb8
use test:e2e-ci
buckhalt Mar 7, 2025
d513d29
explicitly create .env file
buckhalt Mar 7, 2025
55fac92
try different env location
buckhalt Mar 7, 2025
6611d54
env at root .env
buckhalt Mar 7, 2025
c077b32
debug env
buckhalt Mar 10, 2025
5163372
set postgres vars directly
buckhalt Mar 10, 2025
ba53a30
working tests against preview deployment when run locally
buckhalt Mar 10, 2025
64734d6
reference UPLOADTHING_TOKEN
buckhalt Mar 10, 2025
83dc95a
timeout on expect dashboard
buckhalt Mar 10, 2025
ce5319e
run e2e after successful preview deployment, use preview url for base…
buckhalt Mar 11, 2025
daef884
log preview url
buckhalt Mar 11, 2025
874f262
remove unused code
buckhalt Mar 11, 2025
555729e
install specific browsers
buckhalt Mar 12, 2025
2897ce0
cleanup db branches when prs are closed
buckhalt Mar 12, 2025
140cdfd
install browsers with deps
buckhalt Mar 12, 2025
8aaf06a
change order
buckhalt Mar 12, 2025
18ee26f
revert installing specific browsers
buckhalt Mar 12, 2025
692da0b
test change
buckhalt Mar 12, 2025
571f252
directly use neonctl in workflow
buckhalt Mar 12, 2025
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 .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const config = {
'*.test.*',
'public',
'.eslintrc.cjs',
'test-results',
'playwright-report'
],
rules: {
'@next/next/no-img-element': 'off',
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/cleanup-db-branches.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Clean up branched databases
on:
pull_request:
types: [closed]

jobs:
delete-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
- run: npm i -g neonctl@latest
- name: Delete Neon Branch
run: neonctl branches delete preview/${{ github.event.pull_request.head.ref }} --project-id ${{ secrets.NEON_PROJECT_ID }}
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
22 changes: 22 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: ${{ github.event_name == 'deployment_status' && startsWith(github.event.deployment_status.environment, 'preview') && github.event.deployment_status.state == 'success' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run tests
run: pnpm run test:e2e-ci
env:
UPLOADTHING_TOKEN: ${{ secrets.UPLOADTHING_TOKEN }}
BASE_URL: ${{ github.event.deployment_status.environment_url }}
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@ yarn-error.log*
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
.env.test

# vercel
.vercel

# typescript
*.tsbuildinfo

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
28 changes: 28 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
postgres-test:
container_name: fresco-test-postgres
image: postgres:16-alpine
restart: always
ports:
- 5433:5432 # Avoid conflicts with dev
environment:
- POSTGRES_PASSWORD=test_postgres_password
- POSTGRES_USER=test_user
- POSTGRES_DB=test_db
volumes:
- test-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", '-U', 'test_user']
interval: 5s
timeout: 10s
retries: 5
networks:
- test-network

volumes:
test-postgres:
name: fresco-test-db-volume

networks:
test-network:
name: fresco-test-network
18 changes: 18 additions & 0 deletions e2e/app.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

import { expect, test } from '@playwright/test';


// app should be setup before this is run

test('should sign in', async ({ page }) => {

await page.goto("/"); // base url is set in playwright.config.ts
await expect(page).toHaveURL(/\/signin/);

// sign in using credentials
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'Administrator1!');
await page.click('button[type="submit"]');

await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 });
});
Binary file added e2e/files/SampleProtocol.netcanvas
Binary file not shown.
6 changes: 6 additions & 0 deletions e2e/files/participants.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
identifier,label
d8a1d0be-5f9f-4cf9-b89b-f15e6c6df19d,John Doe
4aefc585-18e6-40cb-ae04-2b511f0d007d,Jane Smith
b2d788bc-0305-4fd5-b512-8928d9b33a20,Michael Johnson
cce25123-d8c6-44cc-971b-2fc0f6cf9200,Emily Brown
657b0254-33de-4f7d-8e3a-c1057bdf4f1a,Christopher Lee
121 changes: 121 additions & 0 deletions e2e/global.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* eslint-disable no-console */
/* eslint-disable no-process-env */

import { expect, test } from '@playwright/test';
import { execSync } from 'child_process';

test('create test database and setup app', async ({ page }) => {
console.log('Running setup test');
test.slow(); // triple the default timeout

// Stop any existing test db to ensure clean state
if (!process.env.CI) {
try {
execSync('docker compose -f docker-compose.test.yml down -v', { stdio: 'inherit' });
} catch (error) {
// Ignore errors if no existing container
}

// Start test db
execSync('docker compose -f docker-compose.test.yml up -d', { stdio: 'inherit' });

// Optional: Wait for database to be ready
console.log('Waiting for database to be ready');
execSync('sleep 5', { stdio: 'inherit' });

// local dev, need to use .env.test.local
execSync('pnpm exec dotenv -e .env.test.local node ./setup-database.js && pnpm exec dotenv -e .env.test.local node ./initialize.js', { stdio: 'inherit' });
} else {
console.log('CI environment detected');
// we are in CI uiing the preview deployment
// sign in and reset database
await page.goto("/"); // base url is set in playwright.config.ts
await expect(page).toHaveURL(/\/signin/);

// sign in using credentials
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'Administrator1!');
await page.click('button[type="submit"]');

await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 });
console.log('✅ Signed in successfully');

// go to /settings
await page.goto("/dashboard/settings");
// click "reset all app data" button
const resetButton = page.getByRole('button', { name: 'Reset all app data' });
await resetButton.click({ timeout: 10000 });
const confirmButton = page.getByRole('button', { name: 'Delete all data' });
await confirmButton.click({ timeout: 10000 });

await expect(page).toHaveURL(/\/setup/, { timeout: 10000 });

console.log('✅ Reset app data with settings button')

}

// STEP 1
await page.goto("/setup");
// screenshot
await page.fill('input[name="username"]', 'admin', { timeout: 5000 });
await page.fill('input[name="password"]', 'Administrator1!', { timeout: 5000 });

await page.fill('input[name="confirmPassword"]', 'Administrator1!', { timeout: 5000 });
await page.click('button[type="submit"]', { timeout: 10000 });
await expect(page).toHaveURL(/\/setup\?step=2/);
console.log('✅ Step 1 completed: admin user created');

// STEP 2
// env var cannot be UPLOADTHING_TOKEN or this step will be skipped
// screenshot
await page.fill('input[name="uploadThingToken"]', process.env.UPLOADTHING_TOKEN ?? '', { timeout: 5000 });
await page.click('button[type="submit"]', { timeout: 10000 });

await expect(page).toHaveURL(/\/setup\?step=3/, { timeout: 20000 });
console.log('✅ Step 2 completed: uploadthing token set');

// STEP 3
const protocolHandle = page.locator('input[type="file"]');
await protocolHandle.setInputFiles('e2e/files/SampleProtocol.netcanvas');
// check for uploading assets toast
await expect(page.getByText('Uploading assets...')).toBeVisible({ timeout: 5000 });
await expect(page.getByText('Uploading assets...')).not.toBeVisible({ timeout: 120000 }); // long process if assets are large
await expect(page.getByText('Complete...')).toBeVisible({ timeout: 60000 });

await page.getByRole('button', { name: 'Continue' }).click({ timeout: 5000 });
await expect(page).toHaveURL(/\/setup\?step=4/);
console.log('✅ Step 3 completed: protocol uploaded');

// STEP 4
// import participants
await page.getByRole('button', { name: 'Import participants' }).click({ timeout: 5000 });

// dialog should be visible
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5000 });

const participantsHandle = page.locator('input[type="file"]');
await participantsHandle.setInputFiles('e2e/files/participants.csv');
await page.getByRole('button', { name: 'Import' }).click({ timeout: 5000 });

// participants imported toast
await expect(page.locator('div.text-sm.opacity-90', { hasText: 'Participants have been imported successfully' })).toBeVisible({ timeout: 5000 });

// toggle switches
const anonymousRecruitmentSwitch = page.getByRole('switch').first();
const limitInterviewsSwitch = page.getByRole('switch').last();
await anonymousRecruitmentSwitch.click({ timeout: 10000 });
await limitInterviewsSwitch.click({ timeout: 10000 });


await expect(anonymousRecruitmentSwitch).toBeChecked();
await expect(limitInterviewsSwitch).toBeChecked();

await page.getByRole('button', { name: 'Continue' }).click({ timeout: 10000 });
await expect(page).toHaveURL(/\/setup\?step=5/);
console.log('✅ Step 4 completed: participants imported and settings toggled');

// STEP 5 - documentation
await page.getByRole('button', { name: 'Go to the dashboard!' }).click({ timeout: 10000 });
await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 });
console.log('✅ Setup completed: dashboard reached');
});
25 changes: 25 additions & 0 deletions e2e/global.teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable no-process-env */
import { test as teardown } from '@playwright/test';
import { execSync } from 'child_process';
import { UTApi } from 'uploadthing/server';

teardown('delete test database', async () => {
if (!process.env.CI) {
// remove uploaded files from uploadthing
// eslint-disable-next-line no-console
console.log('🗑️ Deleting uploaded files from uploadthing');

const utapi = new UTApi({
// TODO: figure out why we cannot use getUTApi here
token: process.env.UPLOADTHING_TOKEN,
});

await utapi.listFiles({}).then(({ files }) => {
const keys = files.map((file) => file.key);
return utapi.deleteFiles(keys);
});

// Stop and remove test db
execSync('docker compose -f docker-compose.test.yml down -v', { stdio: 'inherit' });
}
});
2 changes: 1 addition & 1 deletion initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async function setInitializedAt() {
});

if (initializedAt) {
console.log('App already initialized. Skipping.');
console.log(`App already initialized at ${initializedAt}. Skipping.`);
return;
}

Expand Down
7 changes: 5 additions & 2 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"lib/interviewer/hooks/forceSimulation.worker.js",
"lib/ui/components/Sprites/ExportSprite.js",
"utils/auth.ts",
"load-test.js"
"load-test.js",
"e2e/global.setup.ts",
"e2e/global.teardown.ts"
],
"ignoreDependencies": [
"server-only",
Expand All @@ -18,7 +20,8 @@
"@tailwindcss/container-queries",
"@tailwindcss/forms",
"@tailwindcss/typography",
"tailwindcss-animate"
"tailwindcss-animate",
"dotenv-cli"
],
"ignoreBinaries": ["docker-compose"]
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"vercel-build": "node ./setup-database.js && node ./initialize.js && next build",
"knip": "knip",
"test": "vitest",
"load-test": "docker run -i grafana/k6 run - <load-test.js"
"load-test": "docker run -i grafana/k6 run - <load-test.js",
"test:e2e-dev": "rm -rf .next && NODE_ENV='test' next build && playwright test --trace on",
"test:e2e-ci": "playwright test"
},
"pnpm": {
"overrides": {
Expand Down Expand Up @@ -106,6 +108,7 @@
"zod-form-data": "^2.0.5"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@prisma/client": "^6.4.1",
"@t3-oss/env-nextjs": "^0.12.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
Expand All @@ -129,6 +132,7 @@
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-react": "^4.3.4",
"dotenv-cli": "^8.0.0",
"eslint": "^8.57.1",
"eslint-config-next": "^15.1.5",
"eslint-config-prettier": "^10.0.1",
Expand Down
68 changes: 68 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { defineConfig, devices } from '@playwright/test';

// Load environment variables
import dotenv from 'dotenv';
// const CI = process.env.CI;
const CI = true;
dotenv.config({
path: CI ? './.env' : './.env.test.local'
});

const PORT = 3001; // run on port 3001 to avoid conflicts with dev

const baseURL = CI
// eslint-disable-next-line no-process-env
? process.env.BASE_URL
: `http://localhost:${PORT}`;

const webServer = CI ?
undefined : {
command: `NODE_ENV=test next start -p ${PORT}`,
url: baseURL,
reuseExistingServer: true
};

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
// forbidOnly: !!process.env.CI,
// retries: process.env.CI ? 2 : 0,
// workers: process.env.CI ? 1 : undefined,
reporter: 'html',

use: {
baseURL,
trace: 'on-first-retry',
},

projects: [
{
name: 'setup db',
testMatch: /global\.setup\.ts/,
},
{
name: 'cleanup db',
testMatch: /global\.teardown\.ts/,
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup db'],
teardown: 'cleanup db',
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup db'],
teardown: 'cleanup db',
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup db'],
teardown: 'cleanup db',
},
],

webServer,
});
Loading
Loading