Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase Playwright test reliability #341

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
19 changes: 14 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,27 @@ jobs:
needs: tests_e2e_netlify_prepare
name: Run end-to-end tests on Netlify PR preview
runs-on: ubuntu-latest
timeout-minutes: 3
container:
image: mcr.microsoft.com/playwright:v1.40.0-jammy
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Verify network connectivity
run: |
echo "Testing connection to Netlify preview..."
curl -v https://deploy-preview-${{ env.GITHUB_PR_NUMBER }}--eventua11y.netlify.app/
echo "Testing DNS resolution..."
nslookup deploy-preview-${{ env.GITHUB_PR_NUMBER }}--eventua11y.netlify.app
- name: Run tests
run: npx playwright test
env:
PLAYWRIGHT_TEST_BASE_URL: 'https://deploy-preview-${{ env.GITHUB_PR_NUMBER }}--eventua11y.netlify.app/'
DEBUG: pw:api
DEBUG: 'pw:api,pw:browser*,pw:protocol*'
CI: 'true'
run: |
echo "Starting Playwright tests..."
npx playwright test --project=chromium --retries=5 --reporter=list
55 changes: 52 additions & 3 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export default defineConfig({
// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!process.env.CI,

// Retry on CI only.
retries: process.env.CI ? 2 : 0,
// Increase retries for better reliability
retries: process.env.CI ? 3 : 1,

// Opt out of parallel tests on CI.
workers: process.env.CI ? 1 : undefined,
Expand All @@ -25,12 +25,60 @@ export default defineConfig({

// Collect trace when retrying the failed test.
trace: 'on-first-retry',

// More reasonable timeouts for CI
actionTimeout: process.env.CI ? 45000 : 30000,
navigationTimeout: process.env.CI ? 45000 : 30000,

// Add explicit test timeout
testTimeout: process.env.CI ? 90000 : 60000,

// Enable screenshot on failure
screenshot: 'only-on-failure',

// Enable video recording for failed tests
video: 'retain-on-failure',

// Add viewport size
viewport: { width: 1280, height: 720 },

// Add automatic waiting
waitForNavigation: 'networkidle',

// CI-specific browser launch options
launchOptions: process.env.CI
? {
args: ['--no-sandbox', '--disable-setuid-sandbox'],
}
: undefined,

// Use persistent context in CI to reduce browser startup overhead
contextOptions: process.env.CI
? {
acceptDownloads: false,
strictSelectors: true,
}
: undefined,
},
// Configure projects for major browsers.
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
...devices['Desktop Chrome'],
// Additional chromium-specific settings for CI
launchOptions: process.env.CI
? {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
],
headless: true,
}
: undefined,
},
},
{
name: 'firefox',
Expand All @@ -48,6 +96,7 @@ export default defineConfig({
command: 'netlify dev',
port: 8888,
reuseExistingServer: !process.env.CI,
timeout: 60000, // Reduced server startup timeout
}
: undefined,
});
86 changes: 55 additions & 31 deletions tests/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,65 @@
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.beforeEach(async ({ page, baseURL }) => {
await page.goto(baseURL);
// await page.waitForLoadState('networkidle');
const filterDrawer = page.locator('#filter-drawer');
const isVisible = await filterDrawer.isVisible();
if (isVisible) {
await page.keyboard.press('Escape');
await expect(filterDrawer).not.toBeVisible();
}
await page.waitForSelector('#upcoming-events');
});
test.describe('Events page', () => {
test.beforeEach(async ({ page, baseURL }) => {
await page.goto(baseURL);
await page.waitForLoadState('networkidle');

test('has title', async ({ page }) => {
await expect(page).toHaveTitle(/Eventua11y/);
});
// Clear any open drawers
const filterDrawer = page.locator('#filter-drawer');
await filterDrawer.waitFor({ state: 'attached', timeout: 5000 });
const isVisible = await filterDrawer.isVisible();
if (isVisible) {
await page.keyboard.press('Escape');
await expect(filterDrawer).not.toBeVisible();
}

test('header is visible', async ({ page }) => {
await expect(page.locator('#global-header')).toBeVisible();
});
// Wait for main content
await Promise.all([
page.waitForSelector('#upcoming-events', { state: 'visible' }),
page.waitForSelector('#global-header', { state: 'visible' }),
page.waitForSelector('#global-footer', { state: 'visible' }),
]);
});

test('Today heading is visible', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Today' })).toBeVisible();
});
test('has title', async ({ page }) => {
await expect(page).toHaveTitle(/Eventua11y/, { timeout: 10000 });
});

test('footer is visible', async ({ page }) => {
await expect(page.locator('#global-footer')).toBeVisible();
});
test('header is visible', async ({ page }) => {
const header = page.locator('#global-header');
await expect(header).toBeVisible();
await expect(header).toBeInViewport();
});

test('has no accessibility violations', async ({ page }) => {
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('Today heading is visible', async ({ page }) => {
const heading = page.getByRole('heading', { name: 'Today' });
await expect(heading).toBeVisible();
await expect(heading).toBeInViewport();
});

test('footer is visible', async ({ page }) => {
const footer = page.locator('#global-footer');
await expect(footer).toBeVisible();
});

test('has no accessibility violations', async ({ page }) => {
// Wait for dynamic content to load
await page.waitForLoadState('networkidle');
await page.waitForSelector('#upcoming-events .event', { state: 'visible' });

const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#filter-drawer') // Exclude drawer to avoid false positives
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

test('has at least one upcoming event', async ({ page }) => {
const upcomingEvents = page.locator('.event');
const countOfEvents = await upcomingEvents.count();
await expect(countOfEvents).toBeGreaterThan(0);
test('has at least one upcoming event', async ({ page }) => {
const upcomingEvents = page.locator('.event');
await upcomingEvents.first().waitFor({ state: 'visible' });
const countOfEvents = await upcomingEvents.count();
await expect(countOfEvents).toBeGreaterThan(0);
});
});
Loading
Loading