diff --git a/.github/agents/playwright-implement.agent.md b/.github/agents/playwright-implement.agent.md
new file mode 100644
index 0000000..ed88f30
--- /dev/null
+++ b/.github/agents/playwright-implement.agent.md
@@ -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.
diff --git a/.github/agents/playwright-research.agent.md b/.github/agents/playwright-research.agent.md
index fb209e0..a019d50 100644
--- a/.github/agents/playwright-research.agent.md
+++ b/.github/agents/playwright-research.agent.md
@@ -5,7 +5,7 @@ description: Scans codebase and explores URLs to create Playwright test plans.
tools: ["read", "search", "playwright/*", "web"]
handoffs:
- label: Start Implementation
- agent: agent
+ agent: playwright-implementation
prompt: Implement the test plan
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.
diff --git a/.github/agents/playwright-test-generator.agent.md b/.github/agents/playwright-test-generator.agent.md
new file mode 100644
index 0000000..fb15e6e
--- /dev/null
+++ b/.github/agents/playwright-test-generator.agent.md
@@ -0,0 +1,87 @@
+---
+name: playwright-test-generator
+description: 'Use this agent when you need to create automated browser tests using Playwright Examples: Context: User wants to generate a test for the test plan item. '
+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.
+
+
+ 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(...);
+
+ ...
+ });
+ });
+ ```
+
diff --git a/.github/agents/playwright-test-healer.agent.md b/.github/agents/playwright-test-healer.agent.md
new file mode 100644
index 0000000..2e04486
--- /dev/null
+++ b/.github/agents/playwright-test-healer.agent.md
@@ -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
diff --git a/.github/agents/playwright-test-planner.agent.md b/.github/agents/playwright-test-planner.agent.md
new file mode 100644
index 0000000..1f2e788
--- /dev/null
+++ b/.github/agents/playwright-test-planner.agent.md
@@ -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.
diff --git a/.github/agents/playwright.agent.md b/.github/agents/playwright.agent.md
deleted file mode 100644
index 4382896..0000000
--- a/.github/agents/playwright.agent.md
+++ /dev/null
@@ -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.
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
new file mode 100644
index 0000000..d9b5b71
--- /dev/null
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -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
diff --git a/.vscode/mcp.json b/.vscode/mcp.json
new file mode 100644
index 0000000..f0c78a8
--- /dev/null
+++ b/.vscode/mcp.json
@@ -0,0 +1,13 @@
+{
+ "servers": {
+ "playwright-test": {
+ "type": "stdio",
+ "command": "npx",
+ "args": [
+ "playwright",
+ "run-test-mcp-server"
+ ]
+ }
+ },
+ "inputs": []
+}
\ No newline at end of file
diff --git a/frontend/vue-app/.github/workflows/copilot-setup-steps.yml b/frontend/vue-app/.github/workflows/copilot-setup-steps.yml
new file mode 100644
index 0000000..d9b5b71
--- /dev/null
+++ b/frontend/vue-app/.github/workflows/copilot-setup-steps.yml
@@ -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
diff --git a/frontend/vue-app/.github/workflows/playwright.yml b/frontend/vue-app/.github/workflows/playwright.yml
new file mode 100644
index 0000000..3eb1314
--- /dev/null
+++ b/frontend/vue-app/.github/workflows/playwright.yml
@@ -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
diff --git a/frontend/vue-app/.gitignore b/frontend/vue-app/.gitignore
index a3f7a51..9d423c3 100644
--- a/frontend/vue-app/.gitignore
+++ b/frontend/vue-app/.gitignore
@@ -34,3 +34,12 @@ coverage
# Vitest
__screenshots__/
+
+*.old
+
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+/playwright/.auth/
diff --git a/frontend/vue-app/auth-setup-before-signin.png b/frontend/vue-app/auth-setup-before-signin.png
new file mode 100644
index 0000000..c85213e
Binary files /dev/null and b/frontend/vue-app/auth-setup-before-signin.png differ
diff --git a/frontend/vue-app/auth-setup-parent-fail.png b/frontend/vue-app/auth-setup-parent-fail.png
new file mode 100644
index 0000000..cfd0e04
Binary files /dev/null and b/frontend/vue-app/auth-setup-parent-fail.png differ
diff --git a/frontend/vue-app/e2e/.auth/user-no-pin.json b/frontend/vue-app/e2e/.auth/user-no-pin.json
new file mode 100644
index 0000000..768de9e
--- /dev/null
+++ b/frontend/vue-app/e2e/.auth/user-no-pin.json
@@ -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}"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/vue-app/tests/.auth/user.json b/frontend/vue-app/e2e/.auth/user.json
similarity index 63%
rename from frontend/vue-app/tests/.auth/user.json
rename to frontend/vue-app/e2e/.auth/user.json
index 0cbe8a0..f764683 100644
--- a/frontend/vue-app/tests/.auth/user.json
+++ b/frontend/vue-app/e2e/.auth/user.json
@@ -2,17 +2,17 @@
"cookies": [
{
"name": "refresh_token",
- "value": "eGf3kkzCP5BSOTDLz-_lkzfeBD_j5mzKfWFrJbPD6CY",
+ "value": "aQ7Hdjmxefq4F6nLro-Sz0d2qO_3XN3v_tO4ioHOH6w",
"domain": "localhost",
"path": "/api/auth",
- "expires": 1780671228.638382,
+ "expires": 1780799347.476442,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
},
{
"name": "access_token",
- "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzI4OTYxMjh9.zGwBu1uhcEs_aH5MTDQFYKNWb9bjfgdIgSO9YnS3ez8",
+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjQyNDd9.g3audbmZ_S-Bc5ZfgwvpfoQuJEjCS2vd3dF8baExFEA",
"domain": "localhost",
"path": "/",
"expires": -1,
@@ -27,11 +27,7 @@
"localStorage": [
{
"name": "authSyncEvent",
- "value": "{\"type\":\"logout\",\"at\":1772895228428}"
- },
- {
- "name": "parentAuth",
- "value": "{\"expiresAt\":1773068028652}"
+ "value": "{\"type\":\"parent_logout\",\"at\":1773023350687}"
}
]
}
diff --git a/frontend/vue-app/e2e/auth-no-pin.setup.ts b/frontend/vue-app/e2e/auth-no-pin.setup.ts
new file mode 100644
index 0000000..6a966e2
--- /dev/null
+++ b/frontend/vue-app/e2e/auth-no-pin.setup.ts
@@ -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 })
+})
diff --git a/frontend/vue-app/e2e/auth.setup.ts b/frontend/vue-app/e2e/auth.setup.ts
new file mode 100644
index 0000000..ff90af8
--- /dev/null
+++ b/frontend/vue-app/e2e/auth.setup.ts
@@ -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 })
+})
diff --git a/frontend/vue-app/e2e/create-child/authorization.spec.ts b/frontend/vue-app/e2e/create-child/authorization.spec.ts
new file mode 100644
index 0000000..a76c331
--- /dev/null
+++ b/frontend/vue-app/e2e/create-child/authorization.spec.ts
@@ -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()
+ })
+})
diff --git a/frontend/vue-app/e2e/create-child/happy-path.spec.ts b/frontend/vue-app/e2e/create-child/happy-path.spec.ts
new file mode 100644
index 0000000..80034bb
--- /dev/null
+++ b/frontend/vue-app/e2e/create-child/happy-path.spec.ts
@@ -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()
+ })
+})
diff --git a/frontend/vue-app/e2e/create-child/navigation.spec.ts b/frontend/vue-app/e2e/create-child/navigation.spec.ts
new file mode 100644
index 0000000..775a276
--- /dev/null
+++ b/frontend/vue-app/e2e/create-child/navigation.spec.ts
@@ -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()
+ })
+})
diff --git a/frontend/vue-app/e2e/create-child/sse.spec.ts b/frontend/vue-app/e2e/create-child/sse.spec.ts
new file mode 100644
index 0000000..e629241
--- /dev/null
+++ b/frontend/vue-app/e2e/create-child/sse.spec.ts
@@ -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()
+ })
+})
diff --git a/frontend/vue-app/e2e/create-child/validation.spec.ts b/frontend/vue-app/e2e/create-child/validation.spec.ts
new file mode 100644
index 0000000..5d90bea
--- /dev/null
+++ b/frontend/vue-app/e2e/create-child/validation.spec.ts
@@ -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')
+ })
+})
diff --git a/frontend/vue-app/e2e/e2e-constants.ts b/frontend/vue-app/e2e/e2e-constants.ts
new file mode 100644
index 0000000..1b86535
--- /dev/null
+++ b/frontend/vue-app/e2e/e2e-constants.ts
@@ -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'
diff --git a/frontend/vue-app/e2e/example.spec.ts b/frontend/vue-app/e2e/example.spec.ts
new file mode 100644
index 0000000..54a906a
--- /dev/null
+++ b/frontend/vue-app/e2e/example.spec.ts
@@ -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();
+});
diff --git a/frontend/vue-app/e2e/plans/create-child.plan.md b/frontend/vue-app/e2e/plans/create-child.plan.md
new file mode 100644
index 0000000..a42c056
--- /dev/null
+++ b/frontend/vue-app/e2e/plans/create-child.plan.md
@@ -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
diff --git a/frontend/vue-app/e2e/seed.spec.ts b/frontend/vue-app/e2e/seed.spec.ts
new file mode 100644
index 0000000..6cc7068
--- /dev/null
+++ b/frontend/vue-app/e2e/seed.spec.ts
@@ -0,0 +1,3 @@
+export default async function seed(page: any): Promise {
+ // no-op seed
+}
diff --git a/frontend/vue-app/package-lock.json b/frontend/vue-app/package-lock.json
index 3ebc113..b589a09 100644
--- a/frontend/vue-app/package-lock.json
+++ b/frontend/vue-app/package-lock.json
@@ -8,11 +8,11 @@
"name": "chore-app-frontend",
"version": "0.0.0",
"dependencies": {
- "@playwright/test": "^1.58.2",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
+ "@playwright/test": "^1.58.2",
"@tsconfig/node22": "^22.0.2",
"@types/jsdom": "^27.0.0",
"@types/node": "^22.18.11",
@@ -1527,6 +1527,7 @@
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
@@ -4944,6 +4945,7 @@
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
@@ -4962,6 +4964,7 @@
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -4974,6 +4977,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
diff --git a/frontend/vue-app/package.json b/frontend/vue-app/package.json
index 0e10879..756b55e 100644
--- a/frontend/vue-app/package.json
+++ b/frontend/vue-app/package.json
@@ -18,11 +18,11 @@
"format": "prettier --write src/"
},
"dependencies": {
- "@playwright/test": "^1.58.2",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
+ "@playwright/test": "^1.58.2",
"@tsconfig/node22": "^22.0.2",
"@types/jsdom": "^27.0.0",
"@types/node": "^22.18.11",
diff --git a/frontend/vue-app/playwright.config.ts b/frontend/vue-app/playwright.config.ts
index 2140ccc..502de96 100644
--- a/frontend/vue-app/playwright.config.ts
+++ b/frontend/vue-app/playwright.config.ts
@@ -1,34 +1,86 @@
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({
- testDir: './tests',
- globalSetup: './tests/global-setup.ts',
+ testDir: './e2e',
+ /* Run tests in files in parallel */
fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
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,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
-
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
+ /* Base URL to use in actions like `await page.goto('')`. */
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,
+
+ /* 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: [
- {
- name: 'smoke',
- testMatch: /.*smoke.spec.ts/,
- use: {
- ...devices['Desktop Chrome'],
- storageState: 'tests/.auth/user.json',
- },
- },
- ],
+ { name: 'setup', testMatch: /auth\.setup\.ts/ },
+ { name: 'setup-no-pin', testMatch: /auth-no-pin\.setup\.ts/ },
+ {
+ 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 */
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,
+ // },
})
diff --git a/frontend/vue-app/tests/chores-create.smoke.spec.ts b/frontend/vue-app/tests/chores-create.smoke.spec.ts
deleted file mode 100644
index 0b6c2cd..0000000
--- a/frontend/vue-app/tests/chores-create.smoke.spec.ts
+++ /dev/null
@@ -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()
- })
-})
diff --git a/frontend/vue-app/tests/chores-create.spec.ts b/frontend/vue-app/tests/chores-create.spec.ts
deleted file mode 100644
index 0b6c2cd..0000000
--- a/frontend/vue-app/tests/chores-create.spec.ts
+++ /dev/null
@@ -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()
- })
-})
diff --git a/frontend/vue-app/tests/global-setup.ts b/frontend/vue-app/tests/global-setup.ts
deleted file mode 100644
index 77db30b..0000000
--- a/frontend/vue-app/tests/global-setup.ts
+++ /dev/null
@@ -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()
-}
diff --git a/frontend/vue-app/tests/pages/ChildEditPage.ts b/frontend/vue-app/tests/pages/ChildEditPage.ts
deleted file mode 100644
index 81c1e03..0000000
--- a/frontend/vue-app/tests/pages/ChildEditPage.ts
+++ /dev/null
@@ -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();
- }
-}
diff --git a/frontend/vue-app/tests/pages/LandingPage.ts b/frontend/vue-app/tests/pages/LandingPage.ts
deleted file mode 100644
index 7398ba3..0000000
--- a/frontend/vue-app/tests/pages/LandingPage.ts
+++ /dev/null
@@ -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();
- }
-}