diff --git a/.github/agents/playwright-research.agent.md b/.github/agents/playwright-research.agent.md new file mode 100644 index 0000000..fb209e0 --- /dev/null +++ b/.github/agents/playwright-research.agent.md @@ -0,0 +1,30 @@ +--- +name: playwright-research +description: Scans codebase and explores URLs to create Playwright test plans. +#argument-hint: The inputs this agent expects, e.g., "a task to implement" or "a question to answer". +tools: ["read", "search", "playwright/*", "web"] +handoffs: + - label: Start Implementation + agent: agent + 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. +--- + +# Test Architect Persona + +You are a Senior QA Architect. Your goal is to analyze the user's codebase and +live application to identify high-value test cases. + +### Your Workflow: + +1. **Scan**: Use `read` and `search` to understand existing project structure and components. +2. **Explore**: Use `playwright/navigate` and `playwright/screenshot` to explore the live UI. +3. **Analyze**: Identify edge cases, happy paths, and critical user journeys. +4. **Present**: Output a structured Markdown Test Plan. + +### Hard Constraints: + +- **DO NOT** write any `.spec.ts` or `.js` files. +- **DO NOT** modify existing code. +- **ONLY** present the plan and wait for feedback. diff --git a/.github/agents/playwright.agent.md b/.github/agents/playwright.agent.md new file mode 100644 index 0000000..4382896 --- /dev/null +++ b/.github/agents/playwright.agent.md @@ -0,0 +1,27 @@ +--- +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/agents/playwright.agent.md.old b/.github/agents/playwright.agent.md.old new file mode 100644 index 0000000..154ac37 --- /dev/null +++ b/.github/agents/playwright.agent.md.old @@ -0,0 +1,25 @@ +--- +name: playwright +description: Expert in end-to-end testing using Playwright and TypeScript. +tools: [runCommands, readFile, editFiles, fetchWebpage, codebase, findTestFiles] +--- + +# 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. **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. +5. **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/agents/playwrighter.agent.md.old b/.github/agents/playwrighter.agent.md.old new file mode 100644 index 0000000..39da307 --- /dev/null +++ b/.github/agents/playwrighter.agent.md.old @@ -0,0 +1,25 @@ +--- +name: playwright +description: Expert in end-to-end testing using Playwright and TypeScript. +--- + +# 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/copilot-instructions.md b/.github/copilot-instructions.md index f5397a2..26dcccc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,7 +38,8 @@ - **Backend**: Run Flask with `python -m flask run --host=0.0.0.0 --port=5000` from the `backend/` directory. Main entry: `backend/main.py`. - **Virtual Env**: Python is running from a virtual environment located at `backend/.venv/`. - **Frontend**: From `frontend/vue-app/`, run `npm install` then `npm run dev`. -- **Tests**: Run backend tests with `pytest` in `backend/tests/`. Frontend component tests: `npm run test` in `frontend/vue-app/components/__tests__/`. +- **Tests**: Run backend tests with `pytest` in `backend/tests/`. Frontend component tests: `npm run test` in `frontend/vue-app/components/__tests__/`. E2E tests: `npx playwright test` from `frontend/vue-app/` — requires both servers running (use the `flask-backend` and `vue-frontend` skills). +- **E2E Setup**: Playwright config is at `frontend/vue-app/playwright.config.ts`. Tests live in `frontend/vue-app/tests/`. The `globalSetup` in `playwright.config.ts` seeds the database and logs in once; all tests receive a pre-authenticated session via `storageState` — do NOT navigate to `/auth/login` in tests. Import `E2E_EMAIL` and `E2E_PASSWORD` from `tests/global-setup.ts` rather than hardcoding credentials. The backend must be started with `DB_ENV=e2e DATA_ENV=e2e` (the `flask-backend` skill does this) so test data goes to `backend/test_data/` and never touches production data. - **Debugging**: Use VS Code launch configs or run Flask/Vue dev servers directly. For SSE, use browser dev tools to inspect event streams. ## 📁 Key Files & Directories diff --git a/.github/skills/flask-backend/SKILL.md b/.github/skills/flask-backend/SKILL.md new file mode 100644 index 0000000..a7e6741 --- /dev/null +++ b/.github/skills/flask-backend/SKILL.md @@ -0,0 +1,24 @@ +--- +name: flask-backend +description: Starts the Flask backend using the local virtual environment. +disable-model-invocation: true +--- + +# Instructions + +1. **Locate Environment:** Check for a virtual environment folder (usually `.venv` or `venv`) inside the `/backend` directory. +2. **Activation Logic:** + - If on **Windows**: Use `backend\.venv\Scripts\activate` + - If on **macOS/Linux**: Use `source backend/.venv/bin/activate` +3. **Set Environment Variables:** + - `FLASK_APP`: `main.py` + - `FLASK_DEBUG`: `1` + - `DB_ENV`: `e2e` + - `DATA_ENV`: `e2e` + - `SECRET_KEY`: `dev-secret-key-change-in-production` + - `REFRESH_TOKEN_EXPIRY_DAYS`: `90` +4. **Command:** Execute the following via the `terminal` tool: + `flask run --host=0.0.0.0 --port=5000 --no-debugger --no-reload` +5. **Execution:** Run `python -m flask run --host=0.0.0.0 --port=5000` + _Note: Using `python -m flask` ensures the version inside the venv is used._ +6. **Verification:** After running, check the terminal output for "Running on http://0.0.0.0:5000". If it fails, check if port 5000 is already in use. diff --git a/.github/skills/playwright-best-practices/SKILL.md b/.github/skills/playwright-best-practices/SKILL.md new file mode 100644 index 0000000..820514d --- /dev/null +++ b/.github/skills/playwright-best-practices/SKILL.md @@ -0,0 +1,12 @@ +--- +name: playwright-best-practices +description: Enforces stable, maintainable, and high-performance Playwright test code. +--- +# Playwright Best Practices +When generating or refactoring tests, you must adhere to these standards: + +1. **User-Visible Locators**: Prioritize `page.getByRole()`, `page.getByText()`, and `page.getByLabel()`. Never use fragile CSS selectors (e.g., `.btn-primary`) or XPath unless no other option exists. +2. **Web-First Assertions**: Use the `expect(locator).to...` pattern (e.g., `toBeVisible()`, `toHaveText()`) to leverage Playwright's built-in auto-waiting and retry logic. +3. **Test Isolation**: Every test must be independent. Use `test.beforeEach` for setup (like navigation or login) rather than relying on the state of a previous test. +4. **Avoid Logic in Tests**: Keep tests declarative. Use Page Object Models (POM) if the logic for finding an element requires more than one line of code. +5. **Network Reliability**: Mock third-party APIs using `page.route()` to prevent external flakiness. \ No newline at end of file diff --git a/.github/skills/playwright-healer/SKILL.md b/.github/skills/playwright-healer/SKILL.md new file mode 100644 index 0000000..c832784 --- /dev/null +++ b/.github/skills/playwright-healer/SKILL.md @@ -0,0 +1,14 @@ +--- +name: playwright-healer +description: Analyzes failing Playwright tests and suggests automated fixes based on UI changes. +--- +# Playwright Self-Healing Instructions +When a user asks to "fix" or "heal" a test: + +1. **Analyze the Trace**: Use the Playwright MCP or CLI to open the latest trace file in `.playwright-cli/traces/`. +2. **Compare Snapshots**: If a locator failed, take a fresh `snapshot` of the page. Identify if the element moved, changed its ARIA role, or had its text updated. +3. **Propose the Patch**: + - If the UI changed, suggest the updated locator. + - If it's a timing issue, suggest adding an `expect(locator).toBeVisible()` wait. + - If it's a data issue, check the mock definitions. +4. **Verify**: Run the patched test once before presenting the final code to the user. \ No newline at end of file diff --git a/.github/skills/playwright-smoke-gen/SKILL.md b/.github/skills/playwright-smoke-gen/SKILL.md new file mode 100644 index 0000000..4b9ca48 --- /dev/null +++ b/.github/skills/playwright-smoke-gen/SKILL.md @@ -0,0 +1,11 @@ +--- +name: playwright-smoke-gen +description: Generates high-level smoke tests by exploring a running web application. +--- +# Playwright Smoke Test Instructions +When this skill is active, follow these rules: +1. **Explore First**: Use the Playwright MCP `snapshot` tool to understand the page structure before writing code. +2. **Web-First Assertions**: Always use `expect(locator).toBeVisible()` or `toBeEnabled()`. +3. **Naming Convention**: Save tests in `tests/smoke/[feature].spec.ts`. +4. **Setup/Teardown**: Use `test.beforeEach` for repeated actions like navigating to the base URL. +5. **No Hardcoded Secrets**: If a login is required, use `process.env.TEST_USER` placeholders. \ No newline at end of file diff --git a/.github/skills/playwright-visual-reg/SKILL.md b/.github/skills/playwright-visual-reg/SKILL.md new file mode 100644 index 0000000..9a232db --- /dev/null +++ b/.github/skills/playwright-visual-reg/SKILL.md @@ -0,0 +1,12 @@ +--- +name: playwright-visual-reg +description: Generates and manages visual regression snapshots for UI consistency. +--- +# Playwright Visual Regression Standards +When creating visual tests: + +1. **Standard Assertion**: Use `await expect(page).toHaveScreenshot('name.png');`. +2. **Masking**: Automatically mask dynamic content (dates, usernames, or random IDs) using the `mask` option: + `await expect(page).toHaveScreenshot({ mask: [page.locator('.dynamic-id')] });` +3. **Consistency**: Set `animations: 'disabled'` and `timezoneId: 'UTC'` in the generated test metadata to prevent false positives. +4. **Update Strategy**: Instruct the user to run `npx playwright test --update-snapshots` if they intentionally changed the UI. \ No newline at end of file diff --git a/.github/skills/vue-frontend/SKILL.md b/.github/skills/vue-frontend/SKILL.md new file mode 100644 index 0000000..c18f365 --- /dev/null +++ b/.github/skills/vue-frontend/SKILL.md @@ -0,0 +1,21 @@ +--- +name: vue-frontend +description: Starts the Vue development server for the frontend application. +disable-model-invocation: true +--- + +# Instructions + +Use this skill when the user wants to "start the frontend," "run vue," or "launch the dev server." + +1. **Verify Directory:** Navigate to `./frontend/vue-app`. + - _Self-Correction:_ If the directory doesn't exist, search the workspace for `package.json` files and ask for clarification. + +2. **Check Dependencies:** - Before running, check if `node_modules` exists in `./frontend/vue-app`. + - If missing, ask the user: "Should I run `npm install` first?" + +3. **Execution:** - Run the command: `npm run dev` + - This script is configured in `package.json` to start the Vite/Vue dev server. + +4. **Success Criteria:** - Monitor the terminal output for a local URL (typically `http://localhost:5173` or similar). + - Once the server is "Ready," notify the user. diff --git a/.gitignore b/.gitignore index 204f7f1..73501d9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,43 @@ backend/test_data/db/users.json logs/account_deletion.log backend/test_data/db/tracking_events.json resources/ +frontend/vue-app/playwright-report/index.html +frontend/vue-app/playwright-report/data/7d9e8e8c2aa259dc9ad2c4715b0cf596bf05b8ad.webm +frontend/vue-app/playwright-report/data/53a9d25a05f8605aaf31a0831a88d4d108e65031.png +frontend/vue-app/playwright-report/data/97a02a9005e1cf49de3250991c2dfc24e1845eda.zip +frontend/vue-app/playwright-report/data/278ccf1fa441cc9997d89650beac0252c6bd72c7.zip +frontend/vue-app/playwright-report/data/831e7a25fc01d2aea65ef4b9590141615157afa6.webm +frontend/vue-app/playwright-report/data/12526c507e79af2d09d56299c0bf7a147d27f0c3.md +frontend/vue-app/playwright-report/data/a0602f28b9051f5191f9f9c04caf6e2be1fcf939.zip +frontend/vue-app/playwright-report/data/e8146067f1903cc173e1cc4f5c59b4796fcbb901.zip +frontend/vue-app/playwright-report/trace/codeMirrorModule.DYBRYzYX.css +frontend/vue-app/playwright-report/trace/codicon.DCmgc-ay.ttf +frontend/vue-app/playwright-report/trace/defaultSettingsView.7ch9cixO.css +frontend/vue-app/playwright-report/trace/index.BDwrLSGN.js +frontend/vue-app/playwright-report/trace/index.BVu7tZDe.css +frontend/vue-app/playwright-report/trace/index.html +frontend/vue-app/playwright-report/trace/manifest.webmanifest +frontend/vue-app/playwright-report/trace/playwright-logo.svg +frontend/vue-app/playwright-report/trace/snapshot.html +frontend/vue-app/playwright-report/trace/sw.bundle.js +frontend/vue-app/playwright-report/trace/uiMode.Btcz36p_.css +frontend/vue-app/playwright-report/trace/uiMode.CQJ9SCIQ.js +frontend/vue-app/playwright-report/trace/uiMode.html +frontend/vue-app/playwright-report/trace/xtermModule.DYP7pi_n.css +frontend/vue-app/playwright-report/trace/assets/codeMirrorModule-a5XoALAZ.js +frontend/vue-app/playwright-report/trace/assets/defaultSettingsView-CJSZINFr.js +frontend/vue-app/test-results/.last-run.json +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke/error-context.md +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke/test-failed-1.png +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke/trace.zip +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke-retry1/error-context.md +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke-retry1/test-failed-1.png +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke-retry1/trace.zip +frontend/vue-app/test-results/chores-create.smoke-Chores-127ec-re-form-and-validate-fields-smoke-retry1/video.webm +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke/error-context.md +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke/test-failed-1.png +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke/trace.zip +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke-retry1/error-context.md +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke-retry1/test-failed-1.png +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke-retry1/trace.zip +frontend/vue-app/test-results/chores-create.smoke-Chores-f8e2f-hould-cancel-chore-creation-smoke-retry1/video.webm diff --git a/backend/api/auth_api.py b/backend/api/auth_api.py index 3a9dca4..0050735 100644 --- a/backend/api/auth_api.py +++ b/backend/api/auth_api.py @@ -25,7 +25,11 @@ from api.error_codes import ( INVALID_CREDENTIALS, NOT_VERIFIED, ACCOUNT_MARKED_FOR_DELETION, REFRESH_TOKEN_REUSE, REFRESH_TOKEN_EXPIRED, MISSING_REFRESH_TOKEN, ) -from db.db import users_db, refresh_tokens_db +from db.db import ( + users_db, refresh_tokens_db, child_db, task_db, reward_db, image_db, + pending_reward_db, pending_confirmations_db, tracking_events_db, + child_overrides_db, chore_schedules_db, task_extensions_db, +) from api.utils import normalize_email logger = logging.getLogger(__name__) @@ -35,6 +39,9 @@ TokenQuery = Query() TOKEN_EXPIRY_MINUTES = 60 * 4 RESET_PASSWORD_TOKEN_EXPIRY_MINUTES = 10 ACCESS_TOKEN_EXPIRY_MINUTES = 15 +E2E_TEST_EMAIL = 'e2e@test.com' +E2E_TEST_PASSWORD = 'E2eTestPass1!' +E2E_TEST_PIN = '1234' def send_verification_email(to_email, token): @@ -460,3 +467,37 @@ def logout(): resp = jsonify({'message': 'Logged out'}) _clear_auth_cookies(resp) return resp, 200 + + +@auth_api.route('/e2e-seed', methods=['POST']) +def e2e_seed(): + """Reset the database and insert a verified test user. Only available outside production.""" + if os.environ.get('DB_ENV', 'prod') == 'prod': + return jsonify({'error': 'Not available in production'}), 403 + + child_db.truncate() + task_db.truncate() + reward_db.truncate() + image_db.truncate() + pending_reward_db.truncate() + pending_confirmations_db.truncate() + users_db.truncate() + tracking_events_db.truncate() + child_overrides_db.truncate() + chore_schedules_db.truncate() + task_extensions_db.truncate() + refresh_tokens_db.truncate() + + norm_email = normalize_email(E2E_TEST_EMAIL) + user = User( + first_name='E2E', + last_name='Tester', + email=norm_email, + password=generate_password_hash(E2E_TEST_PASSWORD), + verified=True, + role='user', + pin=E2E_TEST_PIN, + ) + users_db.insert(user.to_dict()) + + return jsonify({'email': norm_email}), 201 diff --git a/frontend/vue-app/package-lock.json b/frontend/vue-app/package-lock.json index ce15722..3ebc113 100644 --- a/frontend/vue-app/package-lock.json +++ b/frontend/vue-app/package-lock.json @@ -8,6 +8,7 @@ "name": "chore-app-frontend", "version": "0.0.0", "dependencies": { + "@playwright/test": "^1.58.2", "vue": "^3.5.22", "vue-router": "^4.6.3" }, @@ -1522,6 +1523,21 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -4924,6 +4940,50 @@ "node": ">=0.10" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/frontend/vue-app/package.json b/frontend/vue-app/package.json index a2a1863..0e10879 100644 --- a/frontend/vue-app/package.json +++ b/frontend/vue-app/package.json @@ -18,6 +18,7 @@ "format": "prettier --write src/" }, "dependencies": { + "@playwright/test": "^1.58.2", "vue": "^3.5.22", "vue-router": "^4.6.3" }, diff --git a/frontend/vue-app/playwright.config.ts b/frontend/vue-app/playwright.config.ts new file mode 100644 index 0000000..2140ccc --- /dev/null +++ b/frontend/vue-app/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', + globalSetup: './tests/global-setup.ts', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 1, // Retries help AI "healer" skills see if a failure is flaky + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + + use: { + 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, + }, + + /* Configure projects for different environments */ + projects: [ + { + name: 'smoke', + testMatch: /.*smoke.spec.ts/, + use: { + ...devices['Desktop Chrome'], + storageState: 'tests/.auth/user.json', + }, + }, + ], + + /* Run your local dev server before starting tests */ + webServer: [ + { + command: 'npm run dev', + url: 'https://localhost:5173', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + ignoreHTTPSErrors: true, + }, + { + command: + '.venv\\Scripts\\python.exe -m flask run --host=0.0.0.0 --port=5000 --no-debugger --no-reload', + url: 'http://localhost:5000/version', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + cwd: '../../backend', + env: { + FLASK_APP: 'main.py', + FLASK_DEBUG: '1', + DB_ENV: 'e2e', + DATA_ENV: 'e2e', + SECRET_KEY: 'dev-secret-key-change-in-production', + REFRESH_TOKEN_EXPIRY_DAYS: '90', + }, + }, + ], +}) diff --git a/frontend/vue-app/tests/.auth/user.json b/frontend/vue-app/tests/.auth/user.json new file mode 100644 index 0000000..0cbe8a0 --- /dev/null +++ b/frontend/vue-app/tests/.auth/user.json @@ -0,0 +1,39 @@ +{ + "cookies": [ + { + "name": "refresh_token", + "value": "eGf3kkzCP5BSOTDLz-_lkzfeBD_j5mzKfWFrJbPD6CY", + "domain": "localhost", + "path": "/api/auth", + "expires": 1780671228.638382, + "httpOnly": true, + "secure": true, + "sameSite": "Strict" + }, + { + "name": "access_token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzI4OTYxMjh9.zGwBu1uhcEs_aH5MTDQFYKNWb9bjfgdIgSO9YnS3ez8", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": true, + "sameSite": "Strict" + } + ], + "origins": [ + { + "origin": "https://localhost:5173", + "localStorage": [ + { + "name": "authSyncEvent", + "value": "{\"type\":\"logout\",\"at\":1772895228428}" + }, + { + "name": "parentAuth", + "value": "{\"expiresAt\":1773068028652}" + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/vue-app/tests/chores-create.smoke.spec.ts b/frontend/vue-app/tests/chores-create.smoke.spec.ts new file mode 100644 index 0000000..0b6c2cd --- /dev/null +++ b/frontend/vue-app/tests/chores-create.smoke.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test' + +// Test: Create a Chore + +test.describe('Chores Tab - Create Chore', () => { + test.beforeEach(async ({ page }) => { + // Log in and navigate to Chores tab + await page.goto('/') + // Assume login is handled by globalSetup and storageState + await page.click('button:has-text("Chores")') + await expect(page).toHaveURL(/chores/) + }) + + test('should open create chore form and validate fields', async ({ page }) => { + await page.click('button:has-text("Add Chore")') + await expect(page.locator('form')).toBeVisible() + + // Try submitting empty form + await page.click('button:has-text("Create")') + await expect(page.locator('.error')).toBeVisible() + + // Fill valid fields + await page.fill('input[name="name"]', 'Take out trash') + await page.fill('textarea[name="description"]', 'Take out trash before dinner') + await page.fill('input[name="due_date"]', '2099-12-31') + await page.selectOption('select[name="assigned_child"]', { index: 0 }) + await page.selectOption('select[name="reward"]', { index: 0 }) + + // Submit + await page.click('button:has-text("Create")') + await expect(page.locator('.success')).toBeVisible() + await expect(page.locator('.chore-list')).toContainText('Take out trash') + }) + + test('should cancel chore creation', async ({ page }) => { + await page.click('button:has-text("Add Chore")') + await page.click('button:has-text("Cancel")') + await expect(page.locator('form')).not.toBeVisible() + }) +}) diff --git a/frontend/vue-app/tests/chores-create.spec.ts b/frontend/vue-app/tests/chores-create.spec.ts new file mode 100644 index 0000000..0b6c2cd --- /dev/null +++ b/frontend/vue-app/tests/chores-create.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test' + +// Test: Create a Chore + +test.describe('Chores Tab - Create Chore', () => { + test.beforeEach(async ({ page }) => { + // Log in and navigate to Chores tab + await page.goto('/') + // Assume login is handled by globalSetup and storageState + await page.click('button:has-text("Chores")') + await expect(page).toHaveURL(/chores/) + }) + + test('should open create chore form and validate fields', async ({ page }) => { + await page.click('button:has-text("Add Chore")') + await expect(page.locator('form')).toBeVisible() + + // Try submitting empty form + await page.click('button:has-text("Create")') + await expect(page.locator('.error')).toBeVisible() + + // Fill valid fields + await page.fill('input[name="name"]', 'Take out trash') + await page.fill('textarea[name="description"]', 'Take out trash before dinner') + await page.fill('input[name="due_date"]', '2099-12-31') + await page.selectOption('select[name="assigned_child"]', { index: 0 }) + await page.selectOption('select[name="reward"]', { index: 0 }) + + // Submit + await page.click('button:has-text("Create")') + await expect(page.locator('.success')).toBeVisible() + await expect(page.locator('.chore-list')).toContainText('Take out trash') + }) + + test('should cancel chore creation', async ({ page }) => { + await page.click('button:has-text("Add Chore")') + await page.click('button:has-text("Cancel")') + await expect(page.locator('form')).not.toBeVisible() + }) +}) diff --git a/frontend/vue-app/tests/global-setup.ts b/frontend/vue-app/tests/global-setup.ts new file mode 100644 index 0000000..77db30b --- /dev/null +++ b/frontend/vue-app/tests/global-setup.ts @@ -0,0 +1,44 @@ +import { chromium } from '@playwright/test' + +const BACKEND_URL = 'http://localhost:5000' +const BASE_URL = 'https://localhost:5173' + +export const E2E_EMAIL = 'e2e@test.com' +export const E2E_PASSWORD = 'E2eTestPass1!' +export const E2E_PIN = '1234' +export const STORAGE_STATE = 'tests/.auth/user.json' + +// Matches PARENT_AUTH_KEY and PARENT_AUTH_EXPIRY_PERSISTENT in src/stores/auth.ts +const PARENT_AUTH_KEY = 'parentAuth' +const TWO_DAYS_MS = 172_800_000 + +export default async function globalSetup() { + // Reset all tables and insert a verified test user directly via the backend + const seedRes = await fetch(`${BACKEND_URL}/auth/e2e-seed`, { method: 'POST' }) + if (!seedRes.ok) { + throw new Error(`e2e-seed failed: ${seedRes.status} ${await seedRes.text()}`) + } + + // Use a real browser to log in so that HttpOnly auth cookies are captured correctly + const browser = await chromium.launch() + const context = await browser.newContext({ ignoreHTTPSErrors: true }) + const page = await context.newPage() + + await page.goto(`${BASE_URL}/auth/login`) + await page.fill('#email', E2E_EMAIL) + await page.fill('#password', E2E_PASSWORD) + await page.click('button[type="submit"]') + + // After login the router redirects away from /auth — wait for that navigation + await page.waitForURL(/\/(child|parent)/) + + // Inject persistent parent auth into localStorage so tests can access /parent routes + // without navigating through the PIN prompt UI + await page.evaluate( + ({ key, expiresAt }) => localStorage.setItem(key, JSON.stringify({ expiresAt })), + { key: PARENT_AUTH_KEY, expiresAt: Date.now() + TWO_DAYS_MS }, + ) + + await context.storageState({ path: STORAGE_STATE }) + await browser.close() +} diff --git a/frontend/vue-app/tests/pages/ChildEditPage.ts b/frontend/vue-app/tests/pages/ChildEditPage.ts new file mode 100644 index 0000000..81c1e03 --- /dev/null +++ b/frontend/vue-app/tests/pages/ChildEditPage.ts @@ -0,0 +1,25 @@ +import { Page } from '@playwright/test'; + +export class ChildEditPage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async fillName(name: string) { + await this.page.getByRole('textbox', { name: 'Name' }).fill(name); + } + + async fillAge(age: string) { + await this.page.getByLabel('Age').fill(age); + } + + async selectImage(imageAlt: string) { + await this.page.getByRole('img', { name: imageAlt }).click(); + } + + async submit() { + await this.page.getByRole('button', { name: 'Save' }).click(); + } +} diff --git a/frontend/vue-app/tests/pages/LandingPage.ts b/frontend/vue-app/tests/pages/LandingPage.ts new file mode 100644 index 0000000..7398ba3 --- /dev/null +++ b/frontend/vue-app/tests/pages/LandingPage.ts @@ -0,0 +1,13 @@ +import { Page } from '@playwright/test'; + +export class LandingPage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async clickGetStartedFree() { + await this.page.getByRole('button', { name: 'Get Started Free' }).click(); + } +}