Skip to content
Draft
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ htmlcov/

# playwright
client/test-results/
client/playwright-report/
client/playwright-report/

# e2e test database
server/e2e_test_dogshelter.db
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Pets workshop

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.
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/).

## Getting started

Expand Down
1 change: 1 addition & 0 deletions client/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @ts-check
process.env.ASTRO_TELEMETRY_DISABLED = '1';
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import node from '@astrojs/node';
Expand Down
28 changes: 22 additions & 6 deletions client/e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ Make sure you have installed dependencies:
npm install
```

You also need Python 3 with Flask dependencies installed:
```bash
pip install -r ../server/requirements.txt
```

### Running Tests

```bash
Expand All @@ -34,33 +39,44 @@ npm run test:e2e:headed
npm run test:e2e:debug
```

## Test Architecture

Tests run against the real Flask server with a separate test database seeded with deterministic data. When Playwright starts, it:

1. Seeds a test database (`server/e2e_test_dogshelter.db`) with known dogs and breeds
2. Starts the Flask server using the test database
3. Starts the Astro dev server pointing at the Flask server
4. Runs all e2e tests against the live application

The test data is defined in `server/utils/seed_test_database.py`.

## Test Coverage

The tests cover the following core functionality:

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

### About Page Tests
- About page content displays correctly
- Navigation back to homepage works

### Dog Details Tests
- Navigation from homepage to dog details
- Full dog details display correctly
- Navigation back from dog details to homepage
- Handling of invalid dog IDs

### API Integration Tests
- Successful API responses
- Empty dog list handling
- Network error handling
- Dogs render correctly on the homepage
- Dog details render correctly
- 404 handling for non-existent dogs
- Navigation from card to detail page

## Configuration

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

The tests run against:
- Client (Astro): http://localhost:4321
Expand Down
79 changes: 35 additions & 44 deletions client/e2e-tests/api-integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,47 @@
import { test, expect } from '@playwright/test';

test.describe('API Integration', () => {
test('should fetch dogs from API', async ({ page }) => {
// Mock successful API response
await page.route('/api/dogs', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Buddy', breed: 'Golden Retriever' },
{ id: 2, name: 'Luna', breed: 'Husky' },
{ id: 3, name: 'Max', breed: 'Labrador' }
])
});
});

test('should render dogs from the API on the homepage', async ({ page }) => {
await page.goto('/');

// Check that mocked dogs are displayed
await expect(page.getByText('Buddy')).toBeVisible();
await expect(page.getByText('Golden Retriever')).toBeVisible();
await expect(page.getByText('Luna')).toBeVisible();
await expect(page.getByText('Husky')).toBeVisible();
await expect(page.getByText('Max')).toBeVisible();
await expect(page.getByText('Labrador')).toBeVisible();

const dogCards = page.getByTestId('dog-card');
await expect(dogCards).toHaveCount(3);

await expect(page.getByTestId('dog-name').nth(0)).toHaveText('Buddy');
await expect(page.getByTestId('dog-breed').nth(0)).toHaveText('Golden Retriever');

await expect(page.getByTestId('dog-name').nth(1)).toHaveText('Luna');
await expect(page.getByTestId('dog-breed').nth(1)).toHaveText('Husky');

await expect(page.getByTestId('dog-name').nth(2)).toHaveText('Max');
await expect(page.getByTestId('dog-breed').nth(2)).toHaveText('German Shepherd');
});

test('should handle empty dog list', async ({ page }) => {
// Mock empty API response
await page.route('/api/dogs', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([])
});
});
test('should render dog details from the API', async ({ page }) => {
await page.goto('/dog/1');

await page.goto('/');

// Check that empty state message is displayed
await expect(page.getByText('No dogs available at the moment')).toBeVisible();
await expect(page.getByTestId('dog-details')).toBeVisible();
await expect(page.getByTestId('dog-name')).toHaveText('Buddy');
await expect(page.getByTestId('dog-breed')).toContainText('Golden Retriever');
await expect(page.getByTestId('dog-age')).toContainText('3');
await expect(page.getByTestId('dog-gender')).toContainText('Male');
await expect(page.getByTestId('dog-status')).toHaveText('Available');
});

test('should handle network errors', async ({ page }) => {
// Mock network error
await page.route('/api/dogs', route => {
route.abort('failed');
});
test('should return 404 details for non-existent dog', async ({ page }) => {
await page.goto('/dog/99999');

await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText('not found');
});

test('should link from dog card to detail page', async ({ page }) => {
await page.goto('/');

// Check that error message is displayed
await expect(page.getByText(/Error:/)).toBeVisible({ timeout: 10000 });

const firstCard = page.getByTestId('dog-card').first();
await firstCard.click();

await expect(page).toHaveURL(/\/dog\/1$/);
await expect(page.getByTestId('dog-details')).toBeVisible();
});
});
});
61 changes: 28 additions & 33 deletions client/e2e-tests/dog-details.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,44 @@ import { test, expect } from '@playwright/test';
test.describe('Dog Details', () => {
test('should navigate to dog details from homepage', async ({ page }) => {
await page.goto('/');

// Wait for dogs to load
await page.waitForSelector('.grid a[href^="/dog/"]', { timeout: 10000 });

// Get the first dog link
const firstDogLink = page.locator('.grid a[href^="/dog/"]').first();

// Get the dog name for verification
const dogName = await firstDogLink.locator('h3').textContent();

// Click on the first dog
await firstDogLink.click();

// Should be on a dog details page
await expect(page.url()).toMatch(/\/dog\/\d+/);

// Check that the page title is correct

const firstDogCard = page.getByTestId('dog-card').first();
const dogName = await page.getByTestId('dog-name').first().textContent();

await firstDogCard.click();

await expect(page).toHaveURL(/\/dog\/\d+/);
await expect(page).toHaveTitle(/Dog Details - Tailspin Shelter/);

// Check for back button
await expect(page.getByRole('link', { name: 'Back to All Dogs' })).toBeVisible();
await expect(page.getByTestId('dog-details')).toBeVisible();
await expect(page.getByTestId('dog-name')).toHaveText(dogName!);
});

test('should display full dog details for Buddy', async ({ page }) => {
await page.goto('/dog/1');

await expect(page.getByTestId('dog-details')).toBeVisible();
await expect(page.getByTestId('dog-name')).toHaveText('Buddy');
await expect(page.getByTestId('dog-breed')).toContainText('Golden Retriever');
await expect(page.getByTestId('dog-age')).toContainText('3');
await expect(page.getByTestId('dog-gender')).toContainText('Male');
await expect(page.getByTestId('dog-status')).toHaveText('Available');
await expect(page.getByTestId('dog-description')).toContainText('friendly and loyal');
});

test('should navigate back to homepage from dog details', async ({ page }) => {
// Go directly to a dog details page (assuming dog with ID 1 exists)
await page.goto('/dog/1');

// Click the back button
await page.getByRole('link', { name: 'Back to All Dogs' }).click();

// Should be redirected to homepage

await page.getByTestId('back-link').click();

await expect(page).toHaveURL('/');
await expect(page.getByRole('heading', { name: 'Welcome to Tailspin Shelter' })).toBeVisible();
});

test('should handle invalid dog ID gracefully', async ({ page }) => {
// Go to a dog page with an invalid ID
await page.goto('/dog/99999');

// The page should still load (even if no dog is found)

await expect(page).toHaveTitle(/Dog Details - Tailspin Shelter/);

// Back button should still be available
await expect(page.getByRole('link', { name: 'Back to All Dogs' })).toBeVisible();
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('back-link')).toBeVisible();
});
});
});
60 changes: 20 additions & 40 deletions client/e2e-tests/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,36 @@ import { test, expect } from '@playwright/test';
test.describe('Tailspin Shelter Homepage', () => {
test('should load homepage and display title', async ({ page }) => {
await page.goto('/');

// Check that the page title is correct

await expect(page).toHaveTitle(/Tailspin Shelter - Find Your Forever Friend/);

// Check that the main heading is visible

await expect(page.getByRole('heading', { name: 'Welcome to Tailspin Shelter' })).toBeVisible();

// Check that the description is visible

await expect(page.getByText('Find your perfect companion from our wonderful selection')).toBeVisible();
});

test('should display dog list section', async ({ page }) => {
test('should display dog list', async ({ page }) => {
await page.goto('/');

// Check that the "Available Dogs" heading is visible

await expect(page.getByRole('heading', { name: 'Available Dogs' })).toBeVisible();

// Wait for dogs to load (either loading state, error, or actual dogs)
await page.waitForSelector('.grid', { timeout: 10000 });

const dogList = page.getByTestId('dog-list');
await expect(dogList).toBeVisible();

const dogCards = page.getByTestId('dog-card');
await expect(dogCards).toHaveCount(3);
});

test('should show loading state initially', async ({ page }) => {
test('should display dog names and breeds', async ({ page }) => {
await page.goto('/');

// Check that loading animation is shown initially
// Look for the loading skeleton cards
const loadingElements = page.locator('.animate-pulse').first();

// Either loading should be visible initially, or dogs should load quickly
try {
await expect(loadingElements).toBeVisible({ timeout: 2000 });
} catch {
// If loading finishes too quickly, that's fine - check for dog content instead
await expect(page.locator('.grid')).toBeVisible();
}
});

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

await page.goto('/');

// Check that error message is displayed
await expect(page.getByText(/Failed to fetch data/)).toBeVisible({ timeout: 10000 });
await expect(page.getByTestId('dog-name').nth(1)).toHaveText('Luna');
await expect(page.getByTestId('dog-breed').nth(1)).toHaveText('Husky');

await expect(page.getByTestId('dog-name').nth(2)).toHaveText('Max');
await expect(page.getByTestId('dog-breed').nth(2)).toHaveText('German Shepherd');
});
});
});
Loading