Skip to content

Commit c940d17

Browse files
authored
Merge pull request #35 from github-samples/add-playwright
Add playwright
2 parents f40f8a6 + 51b4f70 commit c940d17

File tree

10 files changed

+378
-17
lines changed

10 files changed

+378
-17
lines changed

.github/workflows/run-tests.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: "Run tests"
2+
3+
# Run tests on pull requests to main and on pushes to main
4+
on:
5+
pull_request:
6+
branches: [main]
7+
push:
8+
branches: [main]
9+
workflow_dispatch:
10+
11+
jobs:
12+
backend-tests:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: "3.13"
25+
cache: "pip"
26+
27+
- name: Install Python dependencies
28+
run: bash ./scripts/setup-env.sh
29+
30+
31+
- name: Run Flask unit tests
32+
run: bash ./scripts/run-server-tests.sh
33+
34+
frontend-tests:
35+
runs-on: ubuntu-latest
36+
permissions:
37+
contents: read
38+
39+
steps:
40+
- name: Checkout code
41+
uses: actions/checkout@v4
42+
43+
- name: Set up Node.js
44+
uses: actions/setup-node@v4
45+
with:
46+
node-version: "22"
47+
cache: "npm"
48+
cache-dependency-path: "./client/package.json"
49+
50+
- name: Install JavaScript dependencies
51+
working-directory: ./client
52+
run: npm ci
53+
54+
- name: Install Playwright browsers
55+
working-directory: ./client
56+
run: npx playwright install --with-deps
57+
58+
- name: Run environment setup
59+
run: bash ./scripts/setup-env.sh
60+
61+
- name: Run Playwright e2e tests
62+
working-directory: ./client
63+
run: npm run test:e2e
64+
env:
65+
CI: true
66+
67+
- name: Upload Playwright report
68+
uses: actions/upload-artifact@v4
69+
if: failure()
70+
with:
71+
name: playwright-report
72+
path: client/playwright-report/
73+
retention-days: 30

client/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
# Playwright
3+
node_modules/
4+
/test-results/
5+
/playwright-report/
6+
/blob-report/
7+
/playwright/.cache/

client/e2e-tests/games.spec.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Game Listing and Navigation', () => {
4+
test('should display games with titles on index page', async ({ page }) => {
5+
await page.goto('/');
6+
7+
// Wait for the games to load
8+
await page.waitForSelector('[data-testid="games-grid"]', { timeout: 10000 });
9+
10+
// Check that games are displayed
11+
const gameCards = page.locator('[data-testid="game-card"]');
12+
13+
// Wait for at least one game card to be visible
14+
await expect(gameCards.first()).toBeVisible();
15+
16+
// Check that we have at least one game
17+
const gameCount = await gameCards.count();
18+
expect(gameCount).toBeGreaterThan(0);
19+
20+
// Check that each game card has a title
21+
const firstGameCard = gameCards.first();
22+
await expect(firstGameCard.locator('[data-testid="game-title"]')).toBeVisible();
23+
24+
// Verify that game titles are not empty
25+
const gameTitle = await firstGameCard.locator('[data-testid="game-title"]').textContent();
26+
expect(gameTitle?.trim()).toBeTruthy();
27+
});
28+
29+
test('should navigate to correct game details page when clicking on a game', async ({ page }) => {
30+
await page.goto('/');
31+
32+
// Wait for games to load
33+
await page.waitForSelector('[data-testid="games-grid"]', { timeout: 10000 });
34+
35+
// Get the first game card and its data attributes
36+
const firstGameCard = page.locator('[data-testid="game-card"]').first();
37+
const gameId = await firstGameCard.getAttribute('data-game-id');
38+
const gameTitle = await firstGameCard.getAttribute('data-game-title');
39+
40+
// Click on the first game
41+
await firstGameCard.click();
42+
43+
// Verify we're on the correct game details page
44+
await expect(page).toHaveURL(`/game/${gameId}`);
45+
46+
// Verify the game details page loads
47+
await page.waitForSelector('[data-testid="game-details"]', { timeout: 10000 });
48+
49+
// Verify the title matches what we clicked on
50+
const detailsTitle = page.locator('[data-testid="game-details-title"]');
51+
await expect(detailsTitle).toHaveText(gameTitle || '');
52+
});
53+
54+
test('should display game details with all required information', async ({ page }) => {
55+
// Navigate to a specific game (we'll use game ID 1 as an example)
56+
await page.goto('/game/1');
57+
58+
// Wait for game details to load
59+
await page.waitForSelector('[data-testid="game-details"]', { timeout: 10000 });
60+
61+
// Check that the game title is present and not empty
62+
const gameTitle = page.locator('[data-testid="game-details-title"]');
63+
await expect(gameTitle).toBeVisible();
64+
const titleText = await gameTitle.textContent();
65+
expect(titleText?.trim()).toBeTruthy();
66+
67+
// Check that the game description is present and not empty
68+
const gameDescription = page.locator('[data-testid="game-details-description"]');
69+
await expect(gameDescription).toBeVisible();
70+
const descriptionText = await gameDescription.textContent();
71+
expect(descriptionText?.trim()).toBeTruthy();
72+
73+
// Check that either publisher or category (or both) are present
74+
const publisherExists = await page.locator('[data-testid="game-details-publisher"]').isVisible();
75+
const categoryExists = await page.locator('[data-testid="game-details-category"]').isVisible();
76+
expect(publisherExists && categoryExists).toBeTruthy();
77+
78+
// If publisher exists, check it has content
79+
if (publisherExists) {
80+
const publisherText = await page.locator('[data-testid="game-details-publisher"]').textContent();
81+
expect(publisherText?.trim()).toBeTruthy();
82+
}
83+
84+
// If category exists, check it has content
85+
if (categoryExists) {
86+
const categoryText = await page.locator('[data-testid="game-details-category"]').textContent();
87+
expect(categoryText?.trim()).toBeTruthy();
88+
}
89+
});
90+
91+
test('should display a button to back the game', async ({ page }) => {
92+
await page.goto('/game/1');
93+
94+
// Wait for game details to load
95+
await page.waitForSelector('[data-testid="game-details"]', { timeout: 10000 });
96+
97+
// Check that the back game button is present
98+
const backButton = page.locator('[data-testid="back-game-button"]');
99+
await expect(backButton).toBeVisible();
100+
await expect(backButton).toContainText('Support This Game');
101+
102+
// Verify the button is clickable
103+
await expect(backButton).toBeEnabled();
104+
});
105+
106+
test('should be able to navigate back to home from game details', async ({ page }) => {
107+
await page.goto('/game/1');
108+
109+
// Wait for the page to load
110+
await page.waitForSelector('[data-testid="game-details"]', { timeout: 10000 });
111+
112+
// Find and click the back to all games link
113+
const backLink = page.locator('a:has-text("Back to all games")');
114+
await expect(backLink).toBeVisible();
115+
await backLink.click();
116+
117+
// Verify we're back on the home page
118+
await expect(page).toHaveURL('/');
119+
await page.waitForSelector('[data-testid="games-grid"]', { timeout: 10000 });
120+
});
121+
122+
test('should handle navigation to non-existent game gracefully', async ({ page }) => {
123+
// Navigate to a game that doesn't exist
124+
await page.goto('/game/99999');
125+
126+
// The page should load without crashing
127+
// Check if there's an error message or if it handles gracefully
128+
await page.waitForTimeout(3000);
129+
130+
// The page should either show an error or handle it gracefully
131+
// We expect the page to not crash and still have a valid title
132+
await expect(page).toHaveTitle(/Game Details - Tailspin Toys/);
133+
});
134+
});

client/e2e-tests/home.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Home Page', () => {
4+
test('should display the correct title', async ({ page }) => {
5+
await page.goto('/');
6+
7+
// Check that the page title is correct
8+
await expect(page).toHaveTitle('Tailspin Toys - Crowdfunding your new favorite game!');
9+
});
10+
11+
test('should display the main heading', async ({ page }) => {
12+
await page.goto('/');
13+
14+
// Check that the main heading is present
15+
const mainHeading = page.locator('h1').first();
16+
await expect(mainHeading).toHaveText('Tailspin Toys');
17+
});
18+
19+
test('should display the welcome message', async ({ page }) => {
20+
await page.goto('/');
21+
22+
// Check that the welcome message is present
23+
const welcomeMessage = page.locator('p').first();
24+
await expect(welcomeMessage).toHaveText('Find your next game! And maybe even back one! Explore our collection!');
25+
});
26+
});

client/package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "astro dev",
77
"build": "astro build",
88
"preview": "astro preview",
9-
"astro": "astro"
9+
"astro": "astro",
10+
"test:e2e": "npx playwright test"
1011
},
1112
"dependencies": {
1213
"@astrojs/node": "^9.2.2",
@@ -17,6 +18,7 @@
1718
"typescript": "^5.8.3"
1819
},
1920
"devDependencies": {
21+
"@playwright/test": "^1.52.0",
2022
"@types/node": "^22.15.21",
2123
"autoprefixer": "^10.4.21",
2224
"postcss": "^8.5.3",

client/playwright.config.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
/**
4+
* Read environment variables from file.
5+
* https://github.com/motdotla/dotenv
6+
*/
7+
// import dotenv from 'dotenv';
8+
// import path from 'path';
9+
// dotenv.config({ path: path.resolve(__dirname, '.env') });
10+
11+
/**
12+
* See https://playwright.dev/docs/test-configuration.
13+
*/
14+
export default defineConfig({
15+
testDir: './e2e-tests',
16+
/* Run tests in files in parallel */
17+
fullyParallel: true,
18+
/* Fail the build on CI if you accidentally left test.only in the source code. */
19+
forbidOnly: !!process.env.CI,
20+
/* Retry on CI only */
21+
retries: process.env.CI ? 2 : 0,
22+
/* Opt out of parallel tests on CI. */
23+
workers: process.env.CI ? 1 : undefined,
24+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
25+
reporter: 'list',
26+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
27+
use: {
28+
/* Base URL to use in actions like `await page.goto('/')`. */
29+
baseURL: 'http://localhost:4321',
30+
31+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
32+
trace: 'on-first-retry',
33+
},
34+
35+
/* Configure projects for major browsers */
36+
projects: [
37+
{
38+
name: 'chromium',
39+
use: { ...devices['Desktop Chrome'] },
40+
},
41+
],
42+
43+
/* Run your local dev server before starting the tests */
44+
webServer: {
45+
command: '../scripts/start-app.sh',
46+
url: 'http://localhost:4321',
47+
reuseExistingServer: !process.env.CI,
48+
timeout: 120 * 1000, // 2 minutes to allow for setup
49+
},
50+
});

0 commit comments

Comments
 (0)