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
33 changes: 22 additions & 11 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ on:

jobs:
e2e:
timeout-minutes: 30
timeout-minutes: 20
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-noble
Expand Down Expand Up @@ -59,23 +59,35 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Restore Next.js build cache
uses: actions/cache@v4
with:
path: packages/web/.next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('packages/web/**/*.ts', 'packages/web/**/*.tsx', 'packages/web/**/*.css') }}
restore-keys: |
nextjs-${{ runner.os }}-

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Build shared-schema
run: npm run build --workspace=@boardsesh/shared-schema
- name: Build shared-schema and crypto
run: |
npm run build --workspace=@boardsesh/shared-schema &
npm run build --workspace=@boardsesh/crypto &
wait

- name: Build db package
run: npm run build --workspace=@boardsesh/db

- name: Build backend
run: npm run build --workspace=boardsesh-backend

- name: Build web application
run: npm run build --workspace=@boardsesh/web
- name: Build backend and web application
run: |
npm run build --workspace=boardsesh-backend &
BACKEND_PID=$!
npm run build --workspace=@boardsesh/web
wait $BACKEND_PID
env:
# Provide minimal env vars needed for build
NEXTAUTH_SECRET: test-secret-for-ci
NEXTAUTH_URL: http://localhost:3000
DATABASE_URL: postgresql://postgres:password@db.localtest.me:5432/main
Expand All @@ -90,9 +102,8 @@ jobs:
- name: Start backend and web server, then run E2E tests
run: |
npm run start --workspace=boardsesh-backend &
npx wait-on http-get://localhost:8080/health --timeout 30000
npm run start --workspace=@boardsesh/web &
npx wait-on http://localhost:3000 --timeout 60000
npx wait-on http-get://localhost:8080/health http://localhost:3000 --timeout 60000
npm run test:e2e --workspace=@boardsesh/web
env:
PLAYWRIGHT_TEST_BASE_URL: http://localhost:3000
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ node_modules

# testing
coverage
**/e2e/.auth/

# next.js
.next/
Expand Down
47 changes: 47 additions & 0 deletions packages/web/e2e/activity-feed-authenticated.authenticated.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';

/**
* Authenticated E2E tests for the home page sessions feed.
*
* These tests use the pre-authenticated storageState from auth.setup.ts,
* so no login flow is needed in beforeEach.
*/

test.describe('Sessions Feed - Authenticated', () => {
test('renders personalized feed without sign-in alert', async ({ page }) => {
await page.goto('/');

// The Sessions tab should be active by default
const sessionsTab = page.getByRole('tab', { name: 'Sessions' });
await expect(sessionsTab).toBeVisible({ timeout: 15000 });
await expect(sessionsTab).toHaveAttribute('aria-selected', 'true');

// Wait for feed items to render
const feedItems = page.locator('[data-testid="activity-feed-item"]');
await expect(feedItems.first()).toBeVisible({ timeout: 30000 });

// Should NOT show the "Sign in" alert
await expect(page.getByText('Sign in to see a personalized feed')).not.toBeVisible();
});

test('infinite scroll pagination works with authenticated feed', async ({ page }) => {
await page.goto('/');

// Wait for initial items to render
const feedItems = page.locator('[data-testid="activity-feed-item"]');
await expect(feedItems.first()).toBeVisible({ timeout: 30000 });

const initialCount = await feedItems.count();
expect(initialCount).toBeGreaterThan(0);

// Scroll the sentinel element into view to trigger loading more
const sentinel = page.locator('[data-testid="activity-feed-sentinel"]');
await sentinel.scrollIntoViewIfNeeded();

// Wait for more items to appear
await expect(async () => {
const newCount = await feedItems.count();
expect(newCount).toBeGreaterThan(initialCount);
}).toPass({ timeout: 15000 });
});
});
47 changes: 2 additions & 45 deletions packages/web/e2e/activity-feed-infinite-scroll.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,48 +152,5 @@ test.describe('Tab Navigation', () => {
});
});

test.describe('Sessions Feed - Authenticated', () => {
test.beforeEach(async ({ page }) => {
// Log in via the auth login form
await page.goto('/auth/login');
await page.getByLabel('Email').fill('test@boardsesh.com');
await page.getByLabel('Password').fill('test');
await page.getByRole('button', { name: 'Login' }).click();

// Wait for redirect to home page after login
await page.waitForURL('/', { timeout: 15000 });
});

test('renders personalized feed without sign-in alert', async ({ page }) => {
// The Sessions tab should be active by default
const sessionsTab = page.getByRole('tab', { name: 'Sessions' });
await expect(sessionsTab).toBeVisible({ timeout: 15000 });
await expect(sessionsTab).toHaveAttribute('aria-selected', 'true');

// Wait for feed items to render
const feedItems = page.locator('[data-testid="activity-feed-item"]');
await expect(feedItems.first()).toBeVisible({ timeout: 30000 });

// Should NOT show the "Sign in" alert
await expect(page.getByText('Sign in to see a personalized feed')).not.toBeVisible();
});

test('infinite scroll pagination works with authenticated feed', async ({ page }) => {
// Wait for initial items to render
const feedItems = page.locator('[data-testid="activity-feed-item"]');
await expect(feedItems.first()).toBeVisible({ timeout: 30000 });

const initialCount = await feedItems.count();
expect(initialCount).toBeGreaterThan(0);

// Scroll the sentinel element into view to trigger loading more
const sentinel = page.locator('[data-testid="activity-feed-sentinel"]');
await sentinel.scrollIntoViewIfNeeded();

// Wait for more items to appear
await expect(async () => {
const newCount = await feedItems.count();
expect(newCount).toBeGreaterThan(initialCount);
}).toPass({ timeout: 15000 });
});
});
// Authenticated tests moved to activity-feed-authenticated.authenticated.spec.ts
// They use Playwright's storageState for pre-authenticated sessions.
22 changes: 22 additions & 0 deletions packages/web/e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test as setup, expect } from '@playwright/test';
import path from 'path';

const authFile = path.join(__dirname, '.auth', 'user.json');

setup('authenticate', async ({ page }) => {
const testEmail = process.env.TEST_USER_EMAIL;
const testPassword = process.env.TEST_USER_PASSWORD;

setup.skip(!testEmail || !testPassword, 'Set TEST_USER_EMAIL and TEST_USER_PASSWORD env vars');

await page.goto('/auth/login');
await page.getByLabel('Email').fill(testEmail!);
await page.getByLabel('Password').fill(testPassword!);
await page.getByRole('button', { name: 'Login' }).click();

// Wait for redirect to home page after login
await page.waitForURL('/', { timeout: 15000 });

// Save signed-in state
await page.context().storageState({ path: authFile });
});
101 changes: 101 additions & 0 deletions packages/web/e2e/help-screenshots-authenticated.authenticated.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Authenticated Help Page Screenshot Generation Tests
*
* These tests use the pre-authenticated storageState from auth.setup.ts,
* so no login flow is needed in beforeEach.
*
* Run with 1Password CLI:
* TEST_USER_EMAIL=$(op read "op://Boardsesh/Boardsesh local/username") \
* TEST_USER_PASSWORD=$(op read "op://Boardsesh/Boardsesh local/password") \
* npx playwright test e2e/help-screenshots-authenticated.authenticated.spec.ts
*/
import { test, expect } from '@playwright/test';

const SCREENSHOT_DIR = 'public/help';
const boardUrl = '/kilter/original/12x12-square/screw_bolt/40/list';

test.describe('Help Page Screenshots - Authenticated', () => {
test.use({ viewport: { width: 390, height: 844 } });

const testEmail = process.env.TEST_USER_EMAIL;
const testPassword = process.env.TEST_USER_PASSWORD;

test.skip(!testEmail || !testPassword, 'Set TEST_USER_EMAIL and TEST_USER_PASSWORD env vars to run authenticated tests');

test.beforeEach(async ({ page }) => {
await page.goto(boardUrl);
await page.waitForSelector('#onboarding-climb-card, [data-testid="climb-card"]', { timeout: 30000 })
.catch(() => page.waitForLoadState('domcontentloaded'));
});

test('personal progress filters', async ({ page }) => {
// Open search drawer to show filters including personal progress
await page.locator('#onboarding-search-button').click();
await page.getByText('Grade').first().waitFor({ state: 'visible' });

// Expand the Progress section
await page.getByText('Progress').click();

await page.screenshot({ path: `${SCREENSHOT_DIR}/personal-progress.png` });
});

test('party mode active session', async ({ page, context }) => {
test.slow(); // WebSocket connection setup can be slow in CI

// Grant geolocation permission so session creation doesn't wait for permission prompt
await context.grantPermissions(['geolocation']);

// First add a climb to queue so the queue bar appears
const climbCard = page.locator('#onboarding-climb-card');
await climbCard.dblclick();

// Wait for queue bar
const queueBar = page.locator('[data-testid="queue-control-bar"]');
await expect(queueBar).toBeVisible({ timeout: 10000 });

// Open party mode drawer
await page.locator('[data-testid="queue-control-bar"]').getByLabel('Party Mode').click();
await page.locator('[data-swipeable-drawer="true"]:visible').first().waitFor({ timeout: 10000 });

// Start a party session
await page.getByRole('button', { name: 'Start Party Mode' }).click();

// Wait for session to be active - WebSocket connection needs time to establish
await page.waitForSelector('button:has-text("Leave")', { state: 'visible', timeout: 30000 });

await page.screenshot({ path: `${SCREENSHOT_DIR}/party-mode-active.png` });

// Leave the session to clean up
await page.getByRole('button', { name: 'Leave' }).click();
});

test('hold classification wizard', async ({ page }) => {
// Open user drawer and click Classify Holds
await page.getByLabel('User menu').click();
await page.getByText('Classify Holds').waitFor({ state: 'visible' });
await page.getByText('Classify Holds').click();

// Wait for wizard content to load
await page.waitForSelector('.MuiRating-root, .MuiLinearProgress-root', { state: 'visible', timeout: 10000 });

await page.screenshot({ path: `${SCREENSHOT_DIR}/hold-classification.png` });
});

test('settings aurora sync', async ({ page }) => {
// Navigate to settings page
await page.goto('/settings');
// Wait for settings page content to load
await page.waitForSelector('.MuiCard-root', { state: 'visible' });

// Scroll to Board Accounts section
await page.evaluate(() => {
const heading = Array.from(document.querySelectorAll('h4, .MuiCardHeader-title'))
.find(el => el.textContent?.includes('Board Accounts'));
if (heading) {
heading.scrollIntoView({ behavior: 'instant', block: 'start' });
}
});

await page.screenshot({ path: `${SCREENSHOT_DIR}/settings-aurora.png` });
});
});
Loading
Loading