feat: enhance Playwright testing setup with E2E tests, new skills, and improved documentation
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m44s

- Added E2E test setup in `auth_api.py` with `/e2e-seed` endpoint for database reset and test user creation.
- Integrated Playwright for end-to-end testing in the frontend with necessary dependencies in `package.json` and `package-lock.json`.
- Created Playwright configuration in `playwright.config.ts` to manage test execution and server setup.
- Developed new skills for Playwright best practices, visual regression, smoke test generation, and self-healing tests.
- Implemented new test cases for chore creation in `chores-create.smoke.spec.ts` and `chores-create.spec.ts`.
- Added page object models for `ChildEditPage` and `LandingPage` to streamline test interactions.
- Updated `.gitignore` to exclude Playwright reports and test results.
- Enhanced documentation in `copilot-instructions.md` for testing and E2E setup.
This commit is contained in:
2026-03-07 10:13:21 -05:00
parent b2618361e4
commit a8d7427a95
22 changed files with 607 additions and 2 deletions

View File

@@ -0,0 +1,39 @@
{
"cookies": [
{
"name": "refresh_token",
"value": "eGf3kkzCP5BSOTDLz-_lkzfeBD_j5mzKfWFrJbPD6CY",
"domain": "localhost",
"path": "/api/auth",
"expires": 1780671228.638382,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
},
{
"name": "access_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzI4OTYxMjh9.zGwBu1uhcEs_aH5MTDQFYKNWb9bjfgdIgSO9YnS3ez8",
"domain": "localhost",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
}
],
"origins": [
{
"origin": "https://localhost:5173",
"localStorage": [
{
"name": "authSyncEvent",
"value": "{\"type\":\"logout\",\"at\":1772895228428}"
},
{
"name": "parentAuth",
"value": "{\"expiresAt\":1773068028652}"
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test'
// Test: Create a Chore
test.describe('Chores Tab - Create Chore', () => {
test.beforeEach(async ({ page }) => {
// Log in and navigate to Chores tab
await page.goto('/')
// Assume login is handled by globalSetup and storageState
await page.click('button:has-text("Chores")')
await expect(page).toHaveURL(/chores/)
})
test('should open create chore form and validate fields', async ({ page }) => {
await page.click('button:has-text("Add Chore")')
await expect(page.locator('form')).toBeVisible()
// Try submitting empty form
await page.click('button:has-text("Create")')
await expect(page.locator('.error')).toBeVisible()
// Fill valid fields
await page.fill('input[name="name"]', 'Take out trash')
await page.fill('textarea[name="description"]', 'Take out trash before dinner')
await page.fill('input[name="due_date"]', '2099-12-31')
await page.selectOption('select[name="assigned_child"]', { index: 0 })
await page.selectOption('select[name="reward"]', { index: 0 })
// Submit
await page.click('button:has-text("Create")')
await expect(page.locator('.success')).toBeVisible()
await expect(page.locator('.chore-list')).toContainText('Take out trash')
})
test('should cancel chore creation', async ({ page }) => {
await page.click('button:has-text("Add Chore")')
await page.click('button:has-text("Cancel")')
await expect(page.locator('form')).not.toBeVisible()
})
})

View File

@@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test'
// Test: Create a Chore
test.describe('Chores Tab - Create Chore', () => {
test.beforeEach(async ({ page }) => {
// Log in and navigate to Chores tab
await page.goto('/')
// Assume login is handled by globalSetup and storageState
await page.click('button:has-text("Chores")')
await expect(page).toHaveURL(/chores/)
})
test('should open create chore form and validate fields', async ({ page }) => {
await page.click('button:has-text("Add Chore")')
await expect(page.locator('form')).toBeVisible()
// Try submitting empty form
await page.click('button:has-text("Create")')
await expect(page.locator('.error')).toBeVisible()
// Fill valid fields
await page.fill('input[name="name"]', 'Take out trash')
await page.fill('textarea[name="description"]', 'Take out trash before dinner')
await page.fill('input[name="due_date"]', '2099-12-31')
await page.selectOption('select[name="assigned_child"]', { index: 0 })
await page.selectOption('select[name="reward"]', { index: 0 })
// Submit
await page.click('button:has-text("Create")')
await expect(page.locator('.success')).toBeVisible()
await expect(page.locator('.chore-list')).toContainText('Take out trash')
})
test('should cancel chore creation', async ({ page }) => {
await page.click('button:has-text("Add Chore")')
await page.click('button:has-text("Cancel")')
await expect(page.locator('form')).not.toBeVisible()
})
})

View File

@@ -0,0 +1,44 @@
import { chromium } from '@playwright/test'
const BACKEND_URL = 'http://localhost:5000'
const BASE_URL = 'https://localhost:5173'
export const E2E_EMAIL = 'e2e@test.com'
export const E2E_PASSWORD = 'E2eTestPass1!'
export const E2E_PIN = '1234'
export const STORAGE_STATE = 'tests/.auth/user.json'
// Matches PARENT_AUTH_KEY and PARENT_AUTH_EXPIRY_PERSISTENT in src/stores/auth.ts
const PARENT_AUTH_KEY = 'parentAuth'
const TWO_DAYS_MS = 172_800_000
export default async function globalSetup() {
// Reset all tables and insert a verified test user directly via the backend
const seedRes = await fetch(`${BACKEND_URL}/auth/e2e-seed`, { method: 'POST' })
if (!seedRes.ok) {
throw new Error(`e2e-seed failed: ${seedRes.status} ${await seedRes.text()}`)
}
// Use a real browser to log in so that HttpOnly auth cookies are captured correctly
const browser = await chromium.launch()
const context = await browser.newContext({ ignoreHTTPSErrors: true })
const page = await context.newPage()
await page.goto(`${BASE_URL}/auth/login`)
await page.fill('#email', E2E_EMAIL)
await page.fill('#password', E2E_PASSWORD)
await page.click('button[type="submit"]')
// After login the router redirects away from /auth — wait for that navigation
await page.waitForURL(/\/(child|parent)/)
// Inject persistent parent auth into localStorage so tests can access /parent routes
// without navigating through the PIN prompt UI
await page.evaluate(
({ key, expiresAt }) => localStorage.setItem(key, JSON.stringify({ expiresAt })),
{ key: PARENT_AUTH_KEY, expiresAt: Date.now() + TWO_DAYS_MS },
)
await context.storageState({ path: STORAGE_STATE })
await browser.close()
}

View File

@@ -0,0 +1,25 @@
import { Page } from '@playwright/test';
export class ChildEditPage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async fillName(name: string) {
await this.page.getByRole('textbox', { name: 'Name' }).fill(name);
}
async fillAge(age: string) {
await this.page.getByLabel('Age').fill(age);
}
async selectImage(imageAlt: string) {
await this.page.getByRole('img', { name: imageAlt }).click();
}
async submit() {
await this.page.getByRole('button', { name: 'Save' }).click();
}
}

View File

@@ -0,0 +1,13 @@
import { Page } from '@playwright/test';
export class LandingPage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async clickGetStartedFree() {
await this.page.getByRole('button', { name: 'Get Started Free' }).click();
}
}