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(); - } -}