temp changes
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m59s

This commit is contained in:
2026-03-09 10:16:39 -04:00
parent a8d7427a95
commit 2c65d3ecaf
34 changed files with 1023 additions and 217 deletions

View File

@@ -0,0 +1,41 @@
---
name: playwright-implementation
description: Converts plans into code and performs self-healing verification.
tools:
[
"read",
"write",
"terminal/*",
"playwright/*",
"edit",
"execute",
"vscode",
"web",
"search",
"todo",
]
---
# Role: Senior QA Automation Engineer
You are a Playwright expert. Your goal is to create robust, flake-free E2E tests.
# Test Implementation & Healing Workflow
When you receive a test plan:
1. **Implement**: Generate the `.spec.ts` files in `/tests` using standard Playwright patterns.
2. **Verify**: Once files are written, execute the following command in the terminal:
`npx playwright test --agent=healer`
3. **Analyze & Repair**:
- If the Healer Agent proposes a patch, review it.
- If the test still fails after healing, check the **Flask backend logs** to see if it's an API error rather than a UI error.
4. **Final Check**: Only mark the task as "Complete" once `npx playwright test` returns a clean pass.
## Rules of Engagement
1. **Locators:** Prioritize `getByRole`, `getByLabel`, and `getByText`. Avoid CSS selectors unless necessary.
2. **Page Objects:** Always use the Page Object Model (POM). Check `tests/pages/` for existing objects before creating new ones.
3. **Environment:** The app runs at `https://localhost:5173` (HTTPS — self-signed cert). The backend runs at `http://localhost:5000`.
4. **Authentication:** Auth is handled globally via `storageState`. Do NOT navigate to `/auth/login` in any test — you are already logged in. Never hardcode credentials; import `E2E_EMAIL` and `E2E_PASSWORD` from `tests/global-setup.ts` if needed.

View File

@@ -5,7 +5,7 @@ description: Scans codebase and explores URLs to create Playwright test plans.
tools: ["read", "search", "playwright/*", "web"] tools: ["read", "search", "playwright/*", "web"]
handoffs: handoffs:
- label: Start Implementation - label: Start Implementation
agent: agent agent: playwright-implementation
prompt: Implement the test plan prompt: Implement the test plan
send: true send: true
# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed. # tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.

View File

@@ -0,0 +1,87 @@
---
name: playwright-test-generator
description: 'Use this agent when you need to create automated browser tests using Playwright Examples: <example>Context: User wants to generate a test for the test plan item. <test-suite><!-- Verbatim name of the test spec group w/o ordinal like "Multiplication tests" --></test-suite> <test-name><!-- Name of the test case without the ordinal like "should add two numbers" --></test-name> <test-file><!-- Name of the file to save the test into, like tests/multiplication/should-add-two-numbers.spec.ts --></test-file> <seed-file><!-- Seed file path from test plan --></seed-file> <body><!-- Test case content including steps and expectations --></body></example>'
tools:
- search
- playwright-test/browser_click
- playwright-test/browser_drag
- playwright-test/browser_evaluate
- playwright-test/browser_file_upload
- playwright-test/browser_handle_dialog
- playwright-test/browser_hover
- playwright-test/browser_navigate
- playwright-test/browser_press_key
- playwright-test/browser_select_option
- playwright-test/browser_snapshot
- playwright-test/browser_type
- playwright-test/browser_verify_element_visible
- playwright-test/browser_verify_list_visible
- playwright-test/browser_verify_text_visible
- playwright-test/browser_verify_value
- playwright-test/browser_wait_for
- playwright-test/generator_read_log
- playwright-test/generator_setup_page
- playwright-test/generator_write_test
model: Claude Sonnet 4
mcp-servers:
playwright-test:
type: stdio
command: npx
args:
- playwright
- run-test-mcp-server
tools:
- "*"
---
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
application behavior.
# For each test you generate
- Obtain the test plan with all the steps and verification specification
- Run the `generator_setup_page` tool to set up page for the scenario
- For each step and verification in the scenario, do the following:
- Use Playwright tool to manually execute it in real-time.
- Use the step description as the intent for each Playwright tool call.
- Retrieve generator log via `generator_read_log`
- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
- File should contain single test
- File name must be fs-friendly scenario name
- Test must be placed in a describe matching the top-level test plan item
- Test title must match the scenario name
- Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
multiple actions.
- Always use best practices from the log when generating tests.
<example-generation>
For following plan:
```markdown file=specs/plan.md
### 1. Adding New Todos
**Seed:** `tests/seed.spec.ts`
#### 1.1 Add Valid Todo
**Steps:**
1. Click in the "What needs to be done?" input field
#### 1.2 Add Multiple Todos
...
```
Following file is generated:
```ts file=add-valid-todo.spec.ts
// spec: specs/plan.md
// seed: tests/seed.spec.ts
test.describe('Adding New Todos', () => {
test('Add Valid Todo', async { page } => {
// 1. Click in the "What needs to be done?" input field
await page.click(...);
...
});
});
```
</example-generation>

View File

@@ -0,0 +1,63 @@
---
name: playwright-test-healer
description: Use this agent when you need to debug and fix failing Playwright tests
tools:
- search
- edit
- playwright-test/browser_console_messages
- playwright-test/browser_evaluate
- playwright-test/browser_generate_locator
- playwright-test/browser_network_requests
- playwright-test/browser_snapshot
- playwright-test/test_debug
- playwright-test/test_list
- playwright-test/test_run
model: Claude Sonnet 4
mcp-servers:
playwright-test:
type: stdio
command: npx
args:
- playwright
- run-test-mcp-server
tools:
- "*"
---
You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and
resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
broken Playwright tests using a methodical approach.
Your workflow:
1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests
2. **Debug failed tests**: For each failing test run `test_debug`.
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
- Examine the error details
- Capture page snapshot to understand the context
- Analyze selectors, timing issues, or assertion failures
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
- Element selectors that may have changed
- Timing and synchronization issues
- Data dependencies or test environment problems
- Application changes that broke test assumptions
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
- Updating selectors to match current application state
- Fixing assertions and expected values
- Improving test reliability and maintainability
- For inherently dynamic data, utilize regular expressions to produce resilient locators
6. **Verification**: Restart the test after each fix to validate the changes
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
Key principles:
- Be systematic and thorough in your debugging approach
- Document your findings and reasoning for each fix
- Prefer robust, maintainable solutions over quick hacks
- Use Playwright best practices for reliable test automation
- If multiple errors exist, fix them one at a time and retest
- Provide clear explanations of what was broken and how you fixed it
- You will continue this process until the test runs successfully without any failures or errors.
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
of the expected behavior.
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
- Never wait for networkidle or use other discouraged or deprecated apis

View File

@@ -0,0 +1,81 @@
---
name: playwright-test-planner
description: Use this agent when you need to create comprehensive test plan for a web application or website
tools:
- search
- playwright-test/browser_click
- playwright-test/browser_close
- playwright-test/browser_console_messages
- playwright-test/browser_drag
- playwright-test/browser_evaluate
- playwright-test/browser_file_upload
- playwright-test/browser_handle_dialog
- playwright-test/browser_hover
- playwright-test/browser_navigate
- playwright-test/browser_navigate_back
- playwright-test/browser_network_requests
- playwright-test/browser_press_key
- playwright-test/browser_run_code
- playwright-test/browser_select_option
- playwright-test/browser_snapshot
- playwright-test/browser_take_screenshot
- playwright-test/browser_type
- playwright-test/browser_wait_for
- playwright-test/planner_setup_page
- playwright-test/planner_save_plan
model: Claude Sonnet 4
mcp-servers:
playwright-test:
type: stdio
command: npx
args:
- playwright
- run-test-mcp-server
tools:
- "*"
---
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage
planning.
You will:
1. **Navigate and Explore**
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
- Explore the browser snapshot
- Do not take screenshots unless absolutely necessary
- Use `browser_*` tools to navigate and discover interface
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
2. **Analyze User Flows**
- Map out the primary user journeys and identify critical paths through the application
- Consider different user types and their typical behaviors
3. **Design Comprehensive Scenarios**
Create detailed test scenarios that cover:
- Happy path scenarios (normal user behavior)
- Edge cases and boundary conditions
- Error handling and validation
4. **Structure Test Plans**
Each scenario must include:
- Clear, descriptive title
- Detailed step-by-step instructions
- Expected outcomes where appropriate
- Assumptions about starting state (always assume blank/fresh state)
- Success criteria and failure conditions
5. **Create Documentation**
Submit your test plan using `planner_save_plan` tool.
**Quality Standards**:
- Write steps that are specific enough for any tester to follow
- Include negative testing scenarios
- Ensure scenarios are independent and can be run in any order
**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
professional formatting suitable for sharing with development and QA teams.

View File

@@ -1,27 +0,0 @@
---
name: playwright
description: Expert in end-to-end testing using Playwright and TypeScript.
tools: [create, edit, view, delete, terminal, read_file, edit_file, web_search]
---
# Role: Senior QA Automation Engineer
You are a Playwright expert. Your goal is to create robust, flake-free E2E tests.
## Rules of Engagement
1. **Locators:** Prioritize `getByRole`, `getByLabel`, and `getByText`. Avoid CSS selectors unless necessary.
2. **Page Objects:** Always use the Page Object Model (POM). Check `tests/pages/` for existing objects before creating new ones.
3. **Execution:** After writing a test, run it using `npx playwright test` from `frontend/vue-app/`. If it fails, read the trace and fix the test immediately.
4. **Environment:** The app runs at `https://localhost:5173` (HTTPS — self-signed cert). The backend runs at `http://localhost:5000`. Both must be running before tests execute. Use the `flask-backend` skill (sets `DB_ENV=e2e DATA_ENV=e2e`) and `vue-frontend` skill to start them.
5. **Authentication:** Auth is handled globally via `storageState`. Do NOT navigate to `/auth/login` in any test — you are already logged in. Never hardcode credentials; import `E2E_EMAIL` and `E2E_PASSWORD` from `tests/global-setup.ts` if needed.
6. **Test Naming:** Test files must match the pattern `*.smoke.spec.ts` to be picked up by the `smoke` project in `playwright.config.ts`.
## Workflow
- **Step 1:** Read the relevant source code or component file.
- **Step 2:** Generate a plan for the test steps.
- **Step 3:** Create/Update Page Objects in `tests/pages/`.
- **Step 4:** Write the test file in `tests/` using the `*.smoke.spec.ts` naming convention.
- **Step 5:** Run the test and verify success.

View File

@@ -0,0 +1,34 @@
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
# Customize this step as needed
- name: Build application
run: npx run build

13
.vscode/mcp.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"servers": {
"playwright-test": {
"type": "stdio",
"command": "npx",
"args": [
"playwright",
"run-test-mcp-server"
]
}
},
"inputs": []
}

View File

@@ -0,0 +1,34 @@
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
# Customize this step as needed
- name: Build application
run: npx run build

View File

@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View File

@@ -34,3 +34,12 @@ coverage
# Vitest # Vitest
__screenshots__/ __screenshots__/
*.old
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

View File

@@ -0,0 +1,35 @@
{
"cookies": [
{
"name": "refresh_token",
"value": "exz9voXnacTUkQGnKkc2QHLZA1DB3-7neit29Gtan5w",
"domain": "localhost",
"path": "/api/auth",
"expires": 1780801137.642288,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
},
{
"name": "access_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI2NmQ5Yzk0NC05MzFmLTQyODktOWYxZS1kNzZhODQyZTM0MzIiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjYwMzd9.gjcizOIYTbdX6B-AobROaoJtMczY-7EnoyUco-b-xE8",
"domain": "localhost",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
}
],
"origins": [
{
"origin": "https://localhost:5173",
"localStorage": [
{
"name": "authSyncEvent",
"value": "{\"type\":\"logout\",\"at\":1773025137442}"
}
]
}
]
}

View File

@@ -2,17 +2,17 @@
"cookies": [ "cookies": [
{ {
"name": "refresh_token", "name": "refresh_token",
"value": "eGf3kkzCP5BSOTDLz-_lkzfeBD_j5mzKfWFrJbPD6CY", "value": "aQ7Hdjmxefq4F6nLro-Sz0d2qO_3XN3v_tO4ioHOH6w",
"domain": "localhost", "domain": "localhost",
"path": "/api/auth", "path": "/api/auth",
"expires": 1780671228.638382, "expires": 1780799347.476442,
"httpOnly": true, "httpOnly": true,
"secure": true, "secure": true,
"sameSite": "Strict" "sameSite": "Strict"
}, },
{ {
"name": "access_token", "name": "access_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzI4OTYxMjh9.zGwBu1uhcEs_aH5MTDQFYKNWb9bjfgdIgSO9YnS3ez8", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjQyNDd9.g3audbmZ_S-Bc5ZfgwvpfoQuJEjCS2vd3dF8baExFEA",
"domain": "localhost", "domain": "localhost",
"path": "/", "path": "/",
"expires": -1, "expires": -1,
@@ -27,11 +27,7 @@
"localStorage": [ "localStorage": [
{ {
"name": "authSyncEvent", "name": "authSyncEvent",
"value": "{\"type\":\"logout\",\"at\":1772895228428}" "value": "{\"type\":\"parent_logout\",\"at\":1773023350687}"
},
{
"name": "parentAuth",
"value": "{\"expiresAt\":1773068028652}"
} }
] ]
} }

View File

@@ -0,0 +1,18 @@
import { test as setup } from '@playwright/test'
import { STORAGE_STATE_NO_PIN, E2E_EMAIL, E2E_PASSWORD } from './e2e-constants'
setup('authenticate without parent pin', async ({ page }) => {
await page.goto('/auth/login')
await page.getByLabel('Email address').fill(E2E_EMAIL)
await page.getByLabel('Password').fill(E2E_PASSWORD)
await page.getByRole('button', { name: 'Sign in' }).click()
// Wait for redirect to the authenticated area
await page.waitForURL(/\/(parent|child)/)
// Remove parent auth from localStorage so the PIN prompt appears
await page.evaluate(() => localStorage.removeItem('parentAuth'))
await page.context().storageState({ path: STORAGE_STATE_NO_PIN })
})

View File

@@ -0,0 +1,43 @@
import { test as setup } from '@playwright/test'
import { STORAGE_STATE, E2E_EMAIL, E2E_PASSWORD, E2E_PIN } from './e2e-constants'
setup('authenticate', async ({ page }) => {
// Seed backend test data
const backendUrl = 'http://localhost:5000'
const seedRes = await page.request.post(`${backendUrl}/auth/e2e-seed`)
if (!seedRes.ok()) {
throw new Error(`e2e-seed failed: ${seedRes.status()} ${await seedRes.text()}`)
}
await page.goto('/auth/login')
await page.getByLabel('Email address').fill(E2E_EMAIL)
await page.getByLabel('Password').fill(E2E_PASSWORD)
await page.getByRole('button', { name: 'Sign in' }).click()
// After login the router redirects to /child (not parent-authenticated yet)
await page.waitForURL(/\/(parent|child)/)
// Click the LoginButton in the header to open the PIN modal
await page.getByRole('button', { name: 'Parent login' }).click()
// Fill in the PIN and submit
const pinInput = page.getByLabel('PIN').or(page.getByPlaceholder('Enter PIN'))
await pinInput.waitFor({ timeout: 5000 })
await page.screenshot({ path: 'auth-setup-before-pin.png' })
await pinInput.fill(E2E_PIN)
await page.screenshot({ path: 'auth-setup-after-pin.png' })
await page.getByRole('button', { name: 'Verify' }).click()
// LoginButton does router.push('/parent') after PIN - wait for it
await page.waitForURL(/\/parent(\/|$)/)
// Confirm parent mode is active by waiting for the Add Child FAB at /parent
try {
await page.getByRole('button', { name: 'Add Child' }).waitFor({ timeout: 5000 })
} catch (e) {
await page.screenshot({ path: 'auth-setup-parent-fail.png' })
throw new Error('Parent mode not reached after PIN entry. See auth-setup-parent-fail.png for details.')
}
await page.context().storageState({ path: STORAGE_STATE })
})

View File

@@ -0,0 +1,16 @@
// spec: e2e/plans/create-child.plan.md
import { test, expect } from '@playwright/test'
import { STORAGE_STATE_NO_PIN } from '../e2e-constants'
test.use({ storageState: STORAGE_STATE_NO_PIN })
test.describe('Create Child', () => {
test('Add Child FAB is hidden when parent auth is expired', async ({ page }) => {
// Navigate to app root - with no parent auth, router redirects to /child
await page.goto('/')
// expect: the 'Add Child' FAB is NOT visible (not in parent mode)
await expect(page.getByRole('button', { name: 'Add Child' })).not.toBeVisible()
})
})

View File

@@ -0,0 +1,104 @@
// spec: e2e/plans/create-child.plan.md
import { test, expect } from '@playwright/test'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const TEST_IMAGE = path.join(__dirname, '../../../../resources/logo/star_only.png')
async function deleteNamedChildren(request: any, names: string[]) {
const res = await request.get('/api/child/list')
const data = await res.json()
for (const child of data.children ?? []) {
if (names.includes(child.name)) {
await request.delete(`/api/child/${child.id}`)
}
}
}
async function deleteAllChildren(request: any) {
const res = await request.get('/api/child/list')
const data = await res.json()
for (const child of data.children ?? []) {
await request.delete(`/api/child/${child.id}`)
}
}
test.describe('Create Child', () => {
test('Create a child with name and age only', async ({ page, request }) => {
await deleteNamedChildren(request, ['Alice'])
// 1. Navigate to app root - router redirects to /parent (children list) when parent-authenticated
await page.goto('/')
await expect(page).toHaveURL('/parent')
// 2. Click the 'Add Child' FAB
await page.getByRole('button', { name: 'Add Child' }).click()
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
await expect(page.getByLabel('Name')).toBeVisible()
await expect(page.getByLabel('Age')).toBeVisible()
// 3. Enter 'Alice' in the Name field
await page.getByLabel('Name').fill('Alice')
await expect(page.getByLabel('Name')).toHaveValue('Alice')
// 4. Enter '8' in the Age field
await page.getByLabel('Age').fill('8')
await expect(page.getByLabel('Age')).toHaveValue('8')
// 5. Leave Image as default and click Create
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.locator('.error')).not.toBeVisible()
await expect(page).toHaveURL('/parent')
await expect(page.getByText('Alice')).toBeVisible()
})
test('Create a child via the inline Create button in empty state', async ({ page, request }) => {
await deleteAllChildren(request)
// 1. Navigate to app root - router redirects to /parent (children list)
await page.goto('/')
await expect(page.getByText('No children')).toBeVisible()
await expect(page.getByRole('button', { name: 'Create' })).toBeVisible()
// 2. Click the inline 'Create' button
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
// 3. Enter 'Bob' and '10', then submit
await page.getByLabel('Name').fill('Bob')
await page.getByLabel('Age').fill('10')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.locator('.error')).not.toBeVisible()
await expect(page).toHaveURL('/parent')
await expect(page.getByText('Bob')).toBeVisible()
})
test('Create a child with a custom uploaded image', async ({ page, request }) => {
await deleteNamedChildren(request, ['Grace'])
// 1. Navigate to app root - router redirects to /parent (children list)
await page.goto('/')
await page.getByRole('button', { name: 'Add Child' }).click()
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
// 2. Enter 'Grace' and '6'
await page.getByLabel('Name').fill('Grace')
await page.getByLabel('Age').fill('6')
await expect(page.getByLabel('Name')).toHaveValue('Grace')
await expect(page.getByLabel('Age')).toHaveValue('6')
// 3. Upload a local image file via 'Add from device'
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByRole('button', { name: 'Add from device' }).click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(TEST_IMAGE)
// 4. Submit the form
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.locator('.error')).not.toBeVisible()
await expect(page).toHaveURL('/parent')
await expect(page.getByText('Grace')).toBeVisible()
})
})

View File

@@ -0,0 +1,21 @@
// spec: e2e/plans/create-child.plan.md
import { test, expect } from '@playwright/test'
test.describe('Create Child', () => {
test('Cancel navigates back without saving', async ({ page }) => {
// 1. Navigate to app root - router redirects to /parent (children list)
await page.goto('/')
await page.getByRole('button', { name: 'Add Child' }).click()
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
// 2. Fill in 'Frank' and '9', then click Cancel
await page.getByLabel('Name').fill('Frank')
await page.getByLabel('Age').fill('9')
await page.getByRole('button', { name: 'Cancel' }).click()
// expect: back on /parent and 'Frank' is NOT listed
await expect(page).toHaveURL('/parent')
await expect(page.getByText('Frank')).not.toBeVisible()
})
})

View File

@@ -0,0 +1,37 @@
// spec: e2e/plans/create-child.plan.md
import { test, expect } from '@playwright/test'
test.describe('Create Child', () => {
test('New child appears in list without page reload', async ({ page, context, request }) => {
// Clean up 'Hannah' before test
const res = await request.get('/api/child/list')
const data = await res.json()
for (const child of data.children ?? []) {
if (child.name === 'Hannah') {
await request.delete(`/api/child/${child.id}`)
}
}
// 1. Open two browser tabs both on /parent (children list)
await page.goto('/')
await expect(page).toHaveURL('/parent')
const tab2 = await context.newPage()
await tab2.goto('/')
await expect(tab2).toHaveURL('/parent')
// 2. In Tab 1, create child 'Hannah' age '4'
await page.getByRole('button', { name: 'Add Child' }).click()
await page.getByLabel('Name').fill('Hannah')
await page.getByLabel('Age').fill('4')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page).toHaveURL('/parent')
await expect(page.getByText('Hannah')).toBeVisible()
// 3. Tab 2 should show 'Hannah' via SSE without a manual refresh
await expect(tab2.getByText('Hannah')).toBeVisible()
await tab2.close()
})
})

View File

@@ -0,0 +1,74 @@
// spec: e2e/plans/create-child.plan.md
import { test, expect } from '@playwright/test'
test.describe('Create Child', () => {
test.beforeEach(async ({ page }) => {
// Navigate to app root - router redirects to /parent (children list) when parent-authenticated
await page.goto('/')
await page.getByRole('button', { name: 'Add Child' }).click()
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
})
test('Reject submission when Name is empty', async ({ page }) => {
// 2. Leave Name empty, enter '7' in Age, click Create
await page.getByLabel('Age').fill('7')
await page.getByRole('button', { name: 'Create' }).click()
// expect: error message and still on create form
await expect(page.locator('.error')).toHaveText('Child name is required.')
await expect(page).toHaveURL('/parent/children/create')
})
test('Reject submission when Name is whitespace only', async ({ page }) => {
// 2. Enter only spaces in Name, enter '7' in Age, click Create
await page.getByLabel('Name').fill(' ')
await page.getByLabel('Age').fill('7')
await page.getByRole('button', { name: 'Create' }).click()
// expect: error message and still on create form
await expect(page.locator('.error')).toHaveText('Child name is required.')
await expect(page).toHaveURL('/parent/children/create')
})
test('Reject submission when Age is empty', async ({ page }) => {
// 2. Enter 'Charlie', clear Age - Create button should be disabled
await page.getByLabel('Name').fill('Charlie')
await page.getByLabel('Age').clear()
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
await expect(page).toHaveURL('/parent/children/create')
})
test('Reject negative age', async ({ page }) => {
// 2. Enter 'Dave', enter '-1', click Create
await page.getByLabel('Name').fill('Dave')
await page.getByLabel('Age').fill('-1')
await page.getByRole('button', { name: 'Create' }).click()
// expect: error message and still on create form
await expect(page.locator('.error')).toHaveText('Age must be a non-negative number.')
await expect(page).toHaveURL('/parent/children/create')
})
test('Enforce maximum Name length of 64 characters', async ({ page }) => {
// 2. Type a 65-character name - HTML maxlength caps it at 64
const longName = 'A'.repeat(65)
await page.getByLabel('Name').fill(longName)
await expect(page.getByLabel('Name')).toHaveValue('A'.repeat(64))
// 3. Enter '5' in Age and submit successfully
await page.getByLabel('Age').fill('5')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page).toHaveURL('/parent')
})
test('Reject age greater than 120', async ({ page }) => {
// 2. Enter 'Eve', enter '121' in Age - Create button should be disabled
await page.getByLabel('Name').fill('Eve')
await page.getByLabel('Age').fill('121')
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
await expect(page).toHaveURL('/parent/children/create')
})
})

View File

@@ -0,0 +1,5 @@
export const STORAGE_STATE = 'e2e/.auth/user.json'
export const STORAGE_STATE_NO_PIN = 'e2e/.auth/user-no-pin.json'
export const E2E_EMAIL = 'e2e@test.com'
export const E2E_PASSWORD = 'E2eTestPass1!'
export const E2E_PIN = '1234'

View File

@@ -0,0 +1,18 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

View File

@@ -0,0 +1,174 @@
# Create Child
## Application Overview
Tests for creating a new child from the Parent dashboard. The user is authenticated and in parent mode via stored auth state (tests/.auth/user.json). All tests start at /parent/children (Children List view).
## Test Scenarios
### 1. Happy Path
**Seed:** `frontend/vue-app/seed.ts`
#### 1.1. Create a child with name and age only
**File:** `tests/create-child/happy-path.spec.ts`
**Steps:**
1. Navigate to /parent/children
- expect: The Children List page is displayed
2. Click the 'Add Child' floating action button (FAB) in the bottom-right corner
- expect: The Create Child form is displayed with Name, Age, and Image fields
3. Enter 'Alice' in the Name field
- expect: The Name field shows 'Alice'
4. Enter '8' in the Age field
- expect: The Age field shows '8'
5. Leave the Image field as the default pre-selected value and click the Save/Submit button
- expect: No error messages are displayed
- expect: Navigation returns to /parent/children
- expect: The child 'Alice' appears in the children list
#### 1.2. Create a child via the inline 'Create' button in empty state
**File:** `tests/create-child/happy-path.spec.ts`
**Steps:**
1. Navigate to /parent/children with no children in the account
- expect: An empty state message is shown with an inline 'Create' button
2. Click the inline 'Create' button
- expect: The Create Child form is displayed
3. Enter 'Bob' in the Name field and '10' in the Age field, then click Save/Submit
- expect: No errors are displayed
- expect: 'Bob' appears in the children list
#### 1.3. Create a child with a custom uploaded image
**File:** `tests/create-child/happy-path.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Enter 'Grace' in the Name field and '6' in the Age field
- expect: Name and Age fields are populated
3. In the Image field, select the option to upload a local file and choose a valid PNG or JPEG
- expect: The image is selected and shown as a preview
4. Click the Save/Submit button
- expect: No error messages are displayed
- expect: 'Grace' appears in the children list with the uploaded image
### 2. Validation - Required Fields
**Seed:** `frontend/vue-app/seed.ts`
#### 2.1. Reject submission when Name is empty
**File:** `tests/create-child/validation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Leave the Name field empty, enter '7' in the Age field, and click Save/Submit
- expect: Error message 'Child name is required.' is displayed
- expect: The form does not navigate away
#### 2.2. Reject submission when Name is whitespace only
**File:** `tests/create-child/validation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Enter only spaces in the Name field, enter '7' in the Age field, and click Save/Submit
- expect: Error message 'Child name is required.' is displayed
- expect: The form does not navigate away
#### 2.3. Reject submission when Age is empty
**File:** `tests/create-child/validation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Enter 'Charlie' in the Name field, clear the Age field, and click Save/Submit
- expect: An age validation error is displayed
- expect: The form does not navigate away
### 3. Validation - Boundary Conditions
**Seed:** `frontend/vue-app/seed.ts`
#### 3.1. Reject negative age
**File:** `tests/create-child/validation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Enter 'Dave' in the Name field, enter '-1' in the Age field, and click Save/Submit
- expect: Error message 'Age must be a non-negative number.' is displayed
- expect: The form does not navigate away
#### 3.2. Enforce maximum Name length of 64 characters
**File:** `tests/create-child/validation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Attempt to type a name with 65 or more characters in the Name field
- expect: The input is capped at 64 characters
3. Enter '5' in the Age field and click Save/Submit
- expect: The form submits successfully with the 64-character name
#### 3.3. Reject age greater than 120
**File:** `tests/create-child/validation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Enter 'Eve' in the Name field, enter '121' in the Age field, and click Save/Submit
- expect: A validation error is shown or the value is capped at 120
### 4. Navigation
**Seed:** `frontend/vue-app/seed.ts`
#### 4.1. Cancel navigates back without saving
**File:** `tests/create-child/navigation.spec.ts`
**Steps:**
1. Navigate to /parent/children and click the 'Add Child' FAB
- expect: The Create Child form is displayed
2. Enter 'Frank' in the Name field and '9' in the Age field, then click the Cancel button
- expect: Navigation returns to /parent/children
- expect: 'Frank' does NOT appear in the children list
### 5. Real-Time Updates via SSE
**Seed:** `frontend/vue-app/seed.ts`
#### 5.1. New child appears in list without page reload
**File:** `tests/create-child/sse.spec.ts`
**Steps:**
1. Open two browser tabs both on /parent/children
- expect: Both tabs show the Children List page
2. In Tab 1, click 'Add Child' FAB, fill in name 'Hannah' and age '4', then submit
- expect: Tab 1 navigates to /parent/children and 'Hannah' is visible
3. Switch to Tab 2 which is still on /parent/children
- expect: 'Hannah' appears in Tab 2 without a manual refresh (SSE real-time update)
### 6. Authorization
**Seed:** `frontend/vue-app/seed.ts`
#### 6.1. Add Child FAB is hidden when parent auth is expired
**File:** `tests/create-child/authorization.spec.ts`
**Steps:**
1. Clear 'parentAuth' from localStorage to simulate expired parent authentication and navigate to /parent/children
- expect: The 'Add Child' FAB is NOT visible on the page

View File

@@ -0,0 +1,3 @@
export default async function seed(page: any): Promise<void> {
// no-op seed
}

View File

@@ -8,11 +8,11 @@
"name": "chore-app-frontend", "name": "chore-app-frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@playwright/test": "^1.58.2",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2",
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/jsdom": "^27.0.0", "@types/jsdom": "^27.0.0",
"@types/node": "^22.18.11", "@types/node": "^22.18.11",
@@ -1527,6 +1527,7 @@
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.58.2" "playwright": "1.58.2"
@@ -4944,6 +4945,7 @@
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.58.2" "playwright-core": "1.58.2"
@@ -4962,6 +4964,7 @@
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@@ -4974,6 +4977,7 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,

View File

@@ -18,11 +18,11 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@playwright/test": "^1.58.2",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2",
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/jsdom": "^27.0.0", "@types/jsdom": "^27.0.0",
"@types/node": "^22.18.11", "@types/node": "^22.18.11",

View File

@@ -1,34 +1,86 @@
import { defineConfig, devices } from '@playwright/test' import { defineConfig, devices } from '@playwright/test'
import { STORAGE_STATE, STORAGE_STATE_NO_PIN } from './e2e/e2e-constants'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({ export default defineConfig({
testDir: './tests', testDir: './e2e',
globalSetup: './tests/global-setup.ts', /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 1, // Retries help AI "healer" skills see if a failure is flaky /* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: 'https://localhost:5173', baseURL: 'https://localhost:5173',
trace: 'retain-on-failure', // AI needs this to "see" why a test failed
video: 'on-first-retry', // Great for visual debugging
screenshot: 'only-on-failure',
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
}, },
/* Configure projects for different environments */ /* Configure projects for major browsers */
projects: [ projects: [
{ { name: 'setup', testMatch: /auth\.setup\.ts/ },
name: 'smoke', { name: 'setup-no-pin', testMatch: /auth-no-pin\.setup\.ts/ },
testMatch: /.*smoke.spec.ts/,
use: {
...devices['Desktop Chrome'],
storageState: 'tests/.auth/user.json',
},
},
],
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE },
dependencies: ['setup'],
},
{
name: 'chromium-no-pin',
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE_NO_PIN },
dependencies: ['setup-no-pin'],
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting tests */ /* Run your local dev server before starting tests */
webServer: [ webServer: [
{ {
@@ -57,4 +109,10 @@ export default defineConfig({
}, },
}, },
], ],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
}) })

View File

@@ -1,40 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,13 +0,0 @@
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();
}
}