Skip to content

Commit 74bc88a

Browse files
authored
Merge pull request #178 from github-samples/copilot/simplify-client-app
E2e tests with real Flask + seeded test DB, fix platform-specific build failures, update workshop content, remove Google Fonts, disable telemetry
2 parents 5e2adb4 + 7b19f3b commit 74bc88a

23 files changed

Lines changed: 3469 additions & 3828 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ htmlcov/
4141

4242
# playwright
4343
client/test-results/
44-
client/playwright-report/
44+
client/playwright-report/
45+
46+
# e2e test database
47+
server/e2e_test_dogshelter.db

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Pets workshop
22

3-
This repository contains the project for two guided workshops to explore various GitHub features. The project is a website for a fictional dog shelter, with a [Flask](https://flask.palletsprojects.com/en/stable/) backend using [SQLAlchemy](https://www.sqlalchemy.org/) and [Astro](https://astro.build/) frontend using [Svelte](https://svelte.dev/) for dynamic pages.
3+
This repository contains the project for two guided workshops to explore various GitHub features. The project is a website for a fictional dog shelter, with a [Flask](https://flask.palletsprojects.com/en/stable/) backend using [SQLAlchemy](https://www.sqlalchemy.org/) and an [Astro](https://astro.build/) frontend using [Tailwind CSS](https://tailwindcss.com/).
44

55
## Getting started
66

client/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @ts-check
2+
process.env.ASTRO_TELEMETRY_DISABLED = '1';
23
import { defineConfig } from 'astro/config';
34
import tailwindcss from '@tailwindcss/vite';
45
import node from '@astrojs/node';

client/e2e-tests/README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ Make sure you have installed dependencies:
1818
npm install
1919
```
2020

21+
You also need Python 3 with Flask dependencies installed:
22+
```bash
23+
pip install -r ../server/requirements.txt
24+
```
25+
2126
### Running Tests
2227

2328
```bash
@@ -34,33 +39,44 @@ npm run test:e2e:headed
3439
npm run test:e2e:debug
3540
```
3641

42+
## Test Architecture
43+
44+
Tests run against the real Flask server with a separate test database seeded with deterministic data. When Playwright starts, it:
45+
46+
1. Seeds a test database (`server/e2e_test_dogshelter.db`) with known dogs and breeds
47+
2. Starts the Flask server using the test database
48+
3. Starts the Astro dev server pointing at the Flask server
49+
4. Runs all e2e tests against the live application
50+
51+
The test data is defined in `server/utils/seed_test_database.py`.
52+
3753
## Test Coverage
3854

3955
The tests cover the following core functionality:
4056

4157
### Homepage Tests
4258
- Page loads with correct title and content
4359
- Dog list displays properly
44-
- Loading states work correctly
45-
- Error handling for API failures
4660

4761
### About Page Tests
4862
- About page content displays correctly
4963
- Navigation back to homepage works
5064

5165
### Dog Details Tests
5266
- Navigation from homepage to dog details
67+
- Full dog details display correctly
5368
- Navigation back from dog details to homepage
5469
- Handling of invalid dog IDs
5570

5671
### API Integration Tests
57-
- Successful API responses
58-
- Empty dog list handling
59-
- Network error handling
72+
- Dogs render correctly on the homepage
73+
- Dog details render correctly
74+
- 404 handling for non-existent dogs
75+
- Navigation from card to detail page
6076

6177
## Configuration
6278

63-
Tests are configured in `../playwright.config.ts` and automatically start the application servers using the existing `scripts/start-app.sh` script before running tests.
79+
Tests are configured in `../playwright.config.ts` and automatically start the Flask and Astro servers before running tests.
6480

6581
The tests run against:
6682
- Client (Astro): http://localhost:4321
Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,47 @@
11
import { test, expect } from '@playwright/test';
22

33
test.describe('API Integration', () => {
4-
test('should fetch dogs from API', async ({ page }) => {
5-
// Mock successful API response
6-
await page.route('/api/dogs', route => {
7-
route.fulfill({
8-
status: 200,
9-
contentType: 'application/json',
10-
body: JSON.stringify([
11-
{ id: 1, name: 'Buddy', breed: 'Golden Retriever' },
12-
{ id: 2, name: 'Luna', breed: 'Husky' },
13-
{ id: 3, name: 'Max', breed: 'Labrador' }
14-
])
15-
});
16-
});
17-
4+
test('should render dogs from the API on the homepage', async ({ page }) => {
185
await page.goto('/');
19-
20-
// Check that mocked dogs are displayed
21-
await expect(page.getByText('Buddy')).toBeVisible();
22-
await expect(page.getByText('Golden Retriever')).toBeVisible();
23-
await expect(page.getByText('Luna')).toBeVisible();
24-
await expect(page.getByText('Husky')).toBeVisible();
25-
await expect(page.getByText('Max')).toBeVisible();
26-
await expect(page.getByText('Labrador')).toBeVisible();
6+
7+
const dogCards = page.getByTestId('dog-card');
8+
await expect(dogCards).toHaveCount(3);
9+
10+
await expect(page.getByTestId('dog-name').nth(0)).toHaveText('Buddy');
11+
await expect(page.getByTestId('dog-breed').nth(0)).toHaveText('Golden Retriever');
12+
13+
await expect(page.getByTestId('dog-name').nth(1)).toHaveText('Luna');
14+
await expect(page.getByTestId('dog-breed').nth(1)).toHaveText('Husky');
15+
16+
await expect(page.getByTestId('dog-name').nth(2)).toHaveText('Max');
17+
await expect(page.getByTestId('dog-breed').nth(2)).toHaveText('German Shepherd');
2718
});
2819

29-
test('should handle empty dog list', async ({ page }) => {
30-
// Mock empty API response
31-
await page.route('/api/dogs', route => {
32-
route.fulfill({
33-
status: 200,
34-
contentType: 'application/json',
35-
body: JSON.stringify([])
36-
});
37-
});
20+
test('should render dog details from the API', async ({ page }) => {
21+
await page.goto('/dog/1');
3822

39-
await page.goto('/');
40-
41-
// Check that empty state message is displayed
42-
await expect(page.getByText('No dogs available at the moment')).toBeVisible();
23+
await expect(page.getByTestId('dog-details')).toBeVisible();
24+
await expect(page.getByTestId('dog-name')).toHaveText('Buddy');
25+
await expect(page.getByTestId('dog-breed')).toContainText('Golden Retriever');
26+
await expect(page.getByTestId('dog-age')).toContainText('3');
27+
await expect(page.getByTestId('dog-gender')).toContainText('Male');
28+
await expect(page.getByTestId('dog-status')).toHaveText('Available');
4329
});
4430

45-
test('should handle network errors', async ({ page }) => {
46-
// Mock network error
47-
await page.route('/api/dogs', route => {
48-
route.abort('failed');
49-
});
31+
test('should return 404 details for non-existent dog', async ({ page }) => {
32+
await page.goto('/dog/99999');
33+
34+
await expect(page.getByTestId('error-message')).toBeVisible();
35+
await expect(page.getByTestId('error-message')).toContainText('not found');
36+
});
5037

38+
test('should link from dog card to detail page', async ({ page }) => {
5139
await page.goto('/');
52-
53-
// Check that error message is displayed
54-
await expect(page.getByText(/Error:/)).toBeVisible({ timeout: 10000 });
40+
41+
const firstCard = page.getByTestId('dog-card').first();
42+
await firstCard.click();
43+
44+
await expect(page).toHaveURL(/\/dog\/1$/);
45+
await expect(page.getByTestId('dog-details')).toBeVisible();
5546
});
56-
});
47+
});

client/e2e-tests/dog-details.spec.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,44 @@ import { test, expect } from '@playwright/test';
33
test.describe('Dog Details', () => {
44
test('should navigate to dog details from homepage', async ({ page }) => {
55
await page.goto('/');
6-
7-
// Wait for dogs to load
8-
await page.waitForSelector('.grid a[href^="/dog/"]', { timeout: 10000 });
9-
10-
// Get the first dog link
11-
const firstDogLink = page.locator('.grid a[href^="/dog/"]').first();
12-
13-
// Get the dog name for verification
14-
const dogName = await firstDogLink.locator('h3').textContent();
15-
16-
// Click on the first dog
17-
await firstDogLink.click();
18-
19-
// Should be on a dog details page
20-
await expect(page.url()).toMatch(/\/dog\/\d+/);
21-
22-
// Check that the page title is correct
6+
7+
const firstDogCard = page.getByTestId('dog-card').first();
8+
const dogName = await page.getByTestId('dog-name').first().textContent();
9+
10+
await firstDogCard.click();
11+
12+
await expect(page).toHaveURL(/\/dog\/\d+/);
2313
await expect(page).toHaveTitle(/Dog Details - Tailspin Shelter/);
24-
25-
// Check for back button
26-
await expect(page.getByRole('link', { name: 'Back to All Dogs' })).toBeVisible();
14+
await expect(page.getByTestId('dog-details')).toBeVisible();
15+
await expect(page.getByTestId('dog-name')).toHaveText(dogName!);
16+
});
17+
18+
test('should display full dog details for Buddy', async ({ page }) => {
19+
await page.goto('/dog/1');
20+
21+
await expect(page.getByTestId('dog-details')).toBeVisible();
22+
await expect(page.getByTestId('dog-name')).toHaveText('Buddy');
23+
await expect(page.getByTestId('dog-breed')).toContainText('Golden Retriever');
24+
await expect(page.getByTestId('dog-age')).toContainText('3');
25+
await expect(page.getByTestId('dog-gender')).toContainText('Male');
26+
await expect(page.getByTestId('dog-status')).toHaveText('Available');
27+
await expect(page.getByTestId('dog-description')).toContainText('friendly and loyal');
2728
});
2829

2930
test('should navigate back to homepage from dog details', async ({ page }) => {
30-
// Go directly to a dog details page (assuming dog with ID 1 exists)
3131
await page.goto('/dog/1');
32-
33-
// Click the back button
34-
await page.getByRole('link', { name: 'Back to All Dogs' }).click();
35-
36-
// Should be redirected to homepage
32+
33+
await page.getByTestId('back-link').click();
34+
3735
await expect(page).toHaveURL('/');
3836
await expect(page.getByRole('heading', { name: 'Welcome to Tailspin Shelter' })).toBeVisible();
3937
});
4038

4139
test('should handle invalid dog ID gracefully', async ({ page }) => {
42-
// Go to a dog page with an invalid ID
4340
await page.goto('/dog/99999');
44-
45-
// The page should still load (even if no dog is found)
41+
4642
await expect(page).toHaveTitle(/Dog Details - Tailspin Shelter/);
47-
48-
// Back button should still be available
49-
await expect(page.getByRole('link', { name: 'Back to All Dogs' })).toBeVisible();
43+
await expect(page.getByTestId('error-message')).toBeVisible();
44+
await expect(page.getByTestId('back-link')).toBeVisible();
5045
});
51-
});
46+
});

client/e2e-tests/homepage.spec.ts

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,36 @@ import { test, expect } from '@playwright/test';
33
test.describe('Tailspin Shelter Homepage', () => {
44
test('should load homepage and display title', async ({ page }) => {
55
await page.goto('/');
6-
7-
// Check that the page title is correct
6+
87
await expect(page).toHaveTitle(/Tailspin Shelter - Find Your Forever Friend/);
9-
10-
// Check that the main heading is visible
8+
119
await expect(page.getByRole('heading', { name: 'Welcome to Tailspin Shelter' })).toBeVisible();
12-
13-
// Check that the description is visible
10+
1411
await expect(page.getByText('Find your perfect companion from our wonderful selection')).toBeVisible();
1512
});
1613

17-
test('should display dog list section', async ({ page }) => {
14+
test('should display dog list', async ({ page }) => {
1815
await page.goto('/');
19-
20-
// Check that the "Available Dogs" heading is visible
16+
2117
await expect(page.getByRole('heading', { name: 'Available Dogs' })).toBeVisible();
22-
23-
// Wait for dogs to load (either loading state, error, or actual dogs)
24-
await page.waitForSelector('.grid', { timeout: 10000 });
18+
19+
const dogList = page.getByTestId('dog-list');
20+
await expect(dogList).toBeVisible();
21+
22+
const dogCards = page.getByTestId('dog-card');
23+
await expect(dogCards).toHaveCount(3);
2524
});
2625

27-
test('should show loading state initially', async ({ page }) => {
26+
test('should display dog names and breeds', async ({ page }) => {
2827
await page.goto('/');
29-
30-
// Check that loading animation is shown initially
31-
// Look for the loading skeleton cards
32-
const loadingElements = page.locator('.animate-pulse').first();
33-
34-
// Either loading should be visible initially, or dogs should load quickly
35-
try {
36-
await expect(loadingElements).toBeVisible({ timeout: 2000 });
37-
} catch {
38-
// If loading finishes too quickly, that's fine - check for dog content instead
39-
await expect(page.locator('.grid')).toBeVisible();
40-
}
41-
});
4228

43-
test('should handle API errors gracefully', async ({ page }) => {
44-
// Intercept the API call and make it fail
45-
await page.route('/api/dogs', route => {
46-
route.fulfill({
47-
status: 500,
48-
contentType: 'application/json',
49-
body: JSON.stringify({ error: 'Internal Server Error' })
50-
});
51-
});
29+
await expect(page.getByTestId('dog-name').nth(0)).toHaveText('Buddy');
30+
await expect(page.getByTestId('dog-breed').nth(0)).toHaveText('Golden Retriever');
5231

53-
await page.goto('/');
54-
55-
// Check that error message is displayed
56-
await expect(page.getByText(/Failed to fetch data/)).toBeVisible({ timeout: 10000 });
32+
await expect(page.getByTestId('dog-name').nth(1)).toHaveText('Luna');
33+
await expect(page.getByTestId('dog-breed').nth(1)).toHaveText('Husky');
34+
35+
await expect(page.getByTestId('dog-name').nth(2)).toHaveText('Max');
36+
await expect(page.getByTestId('dog-breed').nth(2)).toHaveText('German Shepherd');
5737
});
58-
});
38+
});

0 commit comments

Comments
 (0)