Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5e2adb4
Simplify client: remove Svelte, use pure Astro with SSR data fetching
GeekTrainer Mar 1, 2026
7cfff68
Initial plan
Copilot Mar 1, 2026
5006a9b
Create mock API server, update e2e tests for SSR, remove unused esbui…
Copilot Mar 1, 2026
9a5fed4
Update workshop content to reflect pure Astro app (remove Svelte refe…
Copilot Mar 1, 2026
28e1ba4
Replace mock API with real Flask server using seeded test database fo…
Copilot Mar 1, 2026
a2e5bb1
Move DATABASE_PATH resolution into seed_test_database() function
Copilot Mar 1, 2026
dd43328
Remove Google Fonts, use system font stack instead
Copilot Mar 1, 2026
7b19f3b
Disable Astro telemetry via ASTRO_TELEMETRY_DISABLED env var in config
Copilot Mar 1, 2026
74bc88a
Merge pull request #178 from github-samples/copilot/simplify-client-app
GeekTrainer Mar 2, 2026
ab3e09f
Fix content alignment with simplified app
GeekTrainer Mar 2, 2026
6f610c0
Restructure project into app/ directory
GeekTrainer Mar 2, 2026
1d58a16
Add pagination to dogs API and homepage
GeekTrainer Mar 2, 2026
f2d26f5
Fix Vite serving errors for symlinked node_modules
GeekTrainer Mar 2, 2026
d84b9ae
Remove old client/, server/, scripts/ directories
GeekTrainer Mar 2, 2026
4d9a59c
Add dev tooling for Copilot sandbox environment
GeekTrainer Mar 2, 2026
042df81
Genericize sandbox script with external config
GeekTrainer Mar 3, 2026
1ec9322
Update content/1-hour/README.md
GeekTrainer Mar 9, 2026
314144e
Update PLAN.md
GeekTrainer Mar 9, 2026
f83547f
Initial plan
Copilot Mar 9, 2026
b091dc6
Merge main, resolve conflicts, fix content path references
Copilot Mar 9, 2026
d11f050
Reset actions-workshop content to main, remove .dev/ and PLAN.md
GeekTrainer Mar 9, 2026
b278230
Fix GitHub Actions workshop content issues
GeekTrainer Mar 10, 2026
b56c68a
Fix 1-hour and full-day workshop content issues
GeekTrainer Mar 10, 2026
decf271
Merge pull request #190 from github-samples/copilot/sub-pr-179
GeekTrainer Apr 13, 2026
368d84f
Update npm dependencies to latest major versions
GeekTrainer Apr 13, 2026
f304509
Remove adoption-site duplicate and .github/skills, gitignore .playwri…
GeekTrainer Apr 13, 2026
bb2575c
Address Copilot code review feedback
GeekTrainer Apr 14, 2026
ce14043
Merge main into simplify-client-app
GeekTrainer Apr 14, 2026
fa026fb
Fix working-directory paths in GitHub Actions content
GeekTrainer Apr 14, 2026
fdaa14a
Use platform-aware Python executable in test server script
GeekTrainer Apr 14, 2026
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
95 changes: 95 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Plan: Simplify Client App — Pure Astro, Remove Svelte

## Problem Statement

The client app is more complex than necessary:

1. **Svelte is unnecessary** — Both Svelte components (`DogList.svelte`, `DogDetails.svelte`) have zero client-side interactivity (no state changes, no event handlers, just data display and `<a>` links). They can be simple Astro components.
2. **Unnecessary client-side data fetching** — Components fetch data client-side via `fetch('/api/...')`, even though Astro is configured for SSR and can fetch server-side in page frontmatter
3. **Middleware API proxy adds complexity** — The middleware intercepts `/api/` requests and proxies them to Flask. With server-side fetching in Astro, this layer is unnecessary
4. **Dead dependencies** — `autoprefixer`, `postcss` (Tailwind v4 handles this), `flask-cors` (no cross-origin requests with SSR)
5. **Unused assets & redundant code** — Starter template leftovers, duplicate imports, placeholder comments

## Proposed Approach

Remove Svelte entirely. Convert Svelte components to Astro components. Fetch data server-side in Astro page frontmatter. Remove the middleware proxy, dead dependencies, and unused files.

## Todos

### 1. Convert `DogList.svelte` → `DogList.astro`
- **Delete**: `client/src/components/DogList.svelte`
- **Create**: `client/src/components/DogList.astro`
- Accept `dogs` array via `Astro.props`
- Render the same HTML grid of dog cards (pure template, no JS)

### 2. Convert `DogDetails.svelte` → `DogDetails.astro`
- **Delete**: `client/src/components/DogDetails.svelte`
- **Create**: `client/src/components/DogDetails.astro`
- Accept `dog` object via `Astro.props`
- Render the same HTML dog detail card (pure template, no JS)

### 3. Update `index.astro` — server-side data fetching
- **File**: `client/src/pages/index.astro`
- Fetch dogs list from Flask in frontmatter (`API_SERVER_URL/api/dogs`)
- Pass data to `DogList.astro` as props
- Handle error states in the page
- Remove `global.css` import (already in Layout)

### 4. Update `[id].astro` — server-side data fetching
- **File**: `client/src/pages/dog/[id].astro`
- Fetch dog details from Flask in frontmatter (`API_SERVER_URL/api/dogs/{id}`)
- Pass data to `DogDetails.astro` as props
- Handle 404 / error states
- Remove redundant `export const prerender = false` and unused `props` variable

### 5. Remove the API proxy middleware
- **Delete**: `client/src/middleware.ts`

### 6. Remove Svelte from the project
- **File**: `client/astro.config.mjs` — Remove Svelte integration and duplicate vite plugin
- **Delete**: `client/svelte.config.js`
- **File**: `client/package.json` — Remove `svelte`, `@astrojs/svelte`

### 7. Remove dead dependencies
- **File**: `client/package.json` — Remove `autoprefixer`, `postcss` (Tailwind v4 + Vite handles CSS natively)
- **File**: `server/requirements.txt` — Remove `flask-cors` (no cross-origin with SSR)

### 8. Remove unused assets
- **Delete**: `client/src/assets/astro.svg`, `client/src/assets/background.svg` (starter template leftovers, not referenced anywhere)

### 9. Clean up minor issues
- **Eliminate `global.css`** — It only contains `@import "tailwindcss"`. Move this into the `<style is:global>` block in `Layout.astro` and delete the file. Removes a file and 3 redundant imports.
- **Simplify Header to always-visible nav** — The hamburger menu with JS toggle is overkill for 2 nav links (Home, About). Replace with simple inline nav links. This eliminates the **only client-side JavaScript** in the entire app, making it truly zero-JS.
- **Remove unused `dark:` variants** — `<html>` has `class="dark"` hardcoded, so `dark:` prefixes are always active. The non-dark variants (`bg-blue-500`, `bg-white`, `text-slate-800`) never apply. Replace `bg-blue-500 dark:bg-blue-700` with just `bg-blue-700`, etc. Simpler for learners to read.
- **Remove `transition-colors duration-300`** — These transition classes appear on many elements but never trigger (no theme switching). Dead code.
- Remove `// about page` comment from `about.astro`

### 10. Update all libraries to latest versions
- **Client**: `astro`, `@astrojs/node`, `@tailwindcss/vite`, `tailwindcss`, `typescript`, `@playwright/test`, `@types/node`
- **Server**: Pin `flask`, `sqlalchemy`, `flask_sqlalchemy` to latest stable

### 11. Add data-testid attributes to all components
- Add `data-testid` attributes to key elements across all Astro components for stable test selectors
- Components: `DogList.astro`, `DogDetails.astro`, `index.astro`, `[id].astro`, `about.astro`, `Header.astro`
- Key attributes: `dog-list`, `dog-card`, `dog-name`, `dog-breed`, `dog-details`, `dog-age`, `dog-gender`, `dog-status`, `dog-description`, `error-message`, `empty-state`, `back-link`

### 12. Create mock API server for e2e tests
- **Create**: `client/e2e-tests/mock-api.ts` — A lightweight Node.js HTTP server that serves the same endpoints as Flask (`/api/dogs`, `/api/dogs/:id`) with hardcoded test data
- **Update**: `client/playwright.config.ts` — Use Playwright's multiple `webServer` config to start both the mock API and Astro dev server (with `API_SERVER_URL` pointing to the mock)
- This decouples e2e tests from the Flask server entirely

### 13. Update e2e tests to use data-testid selectors
- **All test files**: Replace brittle text selectors (`getByText('Buddy')`) and CSS selectors (`.grid a[href^="/dog/"]`) with `data-testid` locators
- **`api-integration.spec.ts`**: Rewrite to test against mock API server-rendered content (no more `page.route()` mocks)
- **`homepage.spec.ts`**: Remove the "loading state" test (no loading state with SSR) and the client-side API error test
- **`dog-details.spec.ts`**: Update selectors to use data-testid

### 14. Run `npm install` and verify build + e2e tests

## Notes

- The Flask server (`server/app.py`) is unchanged (logic-wise)
- `API_SERVER_URL` env var moves from middleware to a shared constant used by Astro pages
- The `Header.astro` component with its vanilla JS menu toggle is fine as-is
Comment thread
GeekTrainer marked this conversation as resolved.
Outdated
- E2e tests should pass since rendered HTML output is equivalent
- This eliminates the entire Svelte framework from the dependency tree
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
8 changes: 2 additions & 6 deletions client/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
// @ts-check
process.env.ASTRO_TELEMETRY_DISABLED = '1';
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import svelte from '@astrojs/svelte';

import node from '@astrojs/node';

// https://astro.build/config
export default defineConfig({
output: 'server',
integrations: [
svelte(),
],
vite: {
plugins: [tailwindcss(), svelte()]
plugins: [tailwindcss()]
},
adapter: node({
mode: 'standalone'
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();
});
});
});
Loading