feat: enhance Playwright testing setup with E2E tests, new skills, and improved documentation
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m44s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m44s
- Added E2E test setup in `auth_api.py` with `/e2e-seed` endpoint for database reset and test user creation. - Integrated Playwright for end-to-end testing in the frontend with necessary dependencies in `package.json` and `package-lock.json`. - Created Playwright configuration in `playwright.config.ts` to manage test execution and server setup. - Developed new skills for Playwright best practices, visual regression, smoke test generation, and self-healing tests. - Implemented new test cases for chore creation in `chores-create.smoke.spec.ts` and `chores-create.spec.ts`. - Added page object models for `ChildEditPage` and `LandingPage` to streamline test interactions. - Updated `.gitignore` to exclude Playwright reports and test results. - Enhanced documentation in `copilot-instructions.md` for testing and E2E setup.
This commit is contained in:
30
.github/agents/playwright-research.agent.md
vendored
Normal file
30
.github/agents/playwright-research.agent.md
vendored
Normal file
@@ -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.
|
||||||
27
.github/agents/playwright.agent.md
vendored
Normal file
27
.github/agents/playwright.agent.md
vendored
Normal file
@@ -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.
|
||||||
25
.github/agents/playwright.agent.md.old
vendored
Normal file
25
.github/agents/playwright.agent.md.old
vendored
Normal file
@@ -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.
|
||||||
25
.github/agents/playwrighter.agent.md.old
vendored
Normal file
25
.github/agents/playwrighter.agent.md.old
vendored
Normal file
@@ -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.
|
||||||
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -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`.
|
- **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/`.
|
- **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`.
|
- **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.
|
- **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
|
## 📁 Key Files & Directories
|
||||||
|
|||||||
24
.github/skills/flask-backend/SKILL.md
vendored
Normal file
24
.github/skills/flask-backend/SKILL.md
vendored
Normal file
@@ -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.
|
||||||
12
.github/skills/playwright-best-practices/SKILL.md
vendored
Normal file
12
.github/skills/playwright-best-practices/SKILL.md
vendored
Normal file
@@ -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.
|
||||||
14
.github/skills/playwright-healer/SKILL.md
vendored
Normal file
14
.github/skills/playwright-healer/SKILL.md
vendored
Normal file
@@ -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.
|
||||||
11
.github/skills/playwright-smoke-gen/SKILL.md
vendored
Normal file
11
.github/skills/playwright-smoke-gen/SKILL.md
vendored
Normal file
@@ -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.
|
||||||
12
.github/skills/playwright-visual-reg/SKILL.md
vendored
Normal file
12
.github/skills/playwright-visual-reg/SKILL.md
vendored
Normal file
@@ -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.
|
||||||
21
.github/skills/vue-frontend/SKILL.md
vendored
Normal file
21
.github/skills/vue-frontend/SKILL.md
vendored
Normal file
@@ -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.
|
||||||
40
.gitignore
vendored
40
.gitignore
vendored
@@ -8,3 +8,43 @@ backend/test_data/db/users.json
|
|||||||
logs/account_deletion.log
|
logs/account_deletion.log
|
||||||
backend/test_data/db/tracking_events.json
|
backend/test_data/db/tracking_events.json
|
||||||
resources/
|
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
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ from api.error_codes import (
|
|||||||
INVALID_CREDENTIALS, NOT_VERIFIED, ACCOUNT_MARKED_FOR_DELETION,
|
INVALID_CREDENTIALS, NOT_VERIFIED, ACCOUNT_MARKED_FOR_DELETION,
|
||||||
REFRESH_TOKEN_REUSE, REFRESH_TOKEN_EXPIRED, MISSING_REFRESH_TOKEN,
|
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
|
from api.utils import normalize_email
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -35,6 +39,9 @@ TokenQuery = Query()
|
|||||||
TOKEN_EXPIRY_MINUTES = 60 * 4
|
TOKEN_EXPIRY_MINUTES = 60 * 4
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES = 10
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES = 10
|
||||||
ACCESS_TOKEN_EXPIRY_MINUTES = 15
|
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):
|
def send_verification_email(to_email, token):
|
||||||
@@ -460,3 +467,37 @@ def logout():
|
|||||||
resp = jsonify({'message': 'Logged out'})
|
resp = jsonify({'message': 'Logged out'})
|
||||||
_clear_auth_cookies(resp)
|
_clear_auth_cookies(resp)
|
||||||
return resp, 200
|
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
|
||||||
|
|||||||
60
frontend/vue-app/package-lock.json
generated
60
frontend/vue-app/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "chore-app-frontend",
|
"name": "chore-app-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
@@ -1522,6 +1523,21 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"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": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
@@ -4924,6 +4940,50 @@
|
|||||||
"node": ">=0.10"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
|
|||||||
60
frontend/vue-app/playwright.config.ts
Normal file
60
frontend/vue-app/playwright.config.ts
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
39
frontend/vue-app/tests/.auth/user.json
Normal file
39
frontend/vue-app/tests/.auth/user.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"cookies": [
|
||||||
|
{
|
||||||
|
"name": "refresh_token",
|
||||||
|
"value": "eGf3kkzCP5BSOTDLz-_lkzfeBD_j5mzKfWFrJbPD6CY",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/api/auth",
|
||||||
|
"expires": 1780671228.638382,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "Strict"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "access_token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzI4OTYxMjh9.zGwBu1uhcEs_aH5MTDQFYKNWb9bjfgdIgSO9YnS3ez8",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "Strict"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"origins": [
|
||||||
|
{
|
||||||
|
"origin": "https://localhost:5173",
|
||||||
|
"localStorage": [
|
||||||
|
{
|
||||||
|
"name": "authSyncEvent",
|
||||||
|
"value": "{\"type\":\"logout\",\"at\":1772895228428}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "parentAuth",
|
||||||
|
"value": "{\"expiresAt\":1773068028652}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
40
frontend/vue-app/tests/chores-create.smoke.spec.ts
Normal file
40
frontend/vue-app/tests/chores-create.smoke.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
// Test: Create a Chore
|
||||||
|
|
||||||
|
test.describe('Chores Tab - Create Chore', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Log in and navigate to Chores tab
|
||||||
|
await page.goto('/')
|
||||||
|
// Assume login is handled by globalSetup and storageState
|
||||||
|
await page.click('button:has-text("Chores")')
|
||||||
|
await expect(page).toHaveURL(/chores/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should open create chore form and validate fields', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("Add Chore")')
|
||||||
|
await expect(page.locator('form')).toBeVisible()
|
||||||
|
|
||||||
|
// Try submitting empty form
|
||||||
|
await page.click('button:has-text("Create")')
|
||||||
|
await expect(page.locator('.error')).toBeVisible()
|
||||||
|
|
||||||
|
// Fill valid fields
|
||||||
|
await page.fill('input[name="name"]', 'Take out trash')
|
||||||
|
await page.fill('textarea[name="description"]', 'Take out trash before dinner')
|
||||||
|
await page.fill('input[name="due_date"]', '2099-12-31')
|
||||||
|
await page.selectOption('select[name="assigned_child"]', { index: 0 })
|
||||||
|
await page.selectOption('select[name="reward"]', { index: 0 })
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await page.click('button:has-text("Create")')
|
||||||
|
await expect(page.locator('.success')).toBeVisible()
|
||||||
|
await expect(page.locator('.chore-list')).toContainText('Take out trash')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should cancel chore creation', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("Add Chore")')
|
||||||
|
await page.click('button:has-text("Cancel")')
|
||||||
|
await expect(page.locator('form')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
40
frontend/vue-app/tests/chores-create.spec.ts
Normal file
40
frontend/vue-app/tests/chores-create.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
// Test: Create a Chore
|
||||||
|
|
||||||
|
test.describe('Chores Tab - Create Chore', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Log in and navigate to Chores tab
|
||||||
|
await page.goto('/')
|
||||||
|
// Assume login is handled by globalSetup and storageState
|
||||||
|
await page.click('button:has-text("Chores")')
|
||||||
|
await expect(page).toHaveURL(/chores/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should open create chore form and validate fields', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("Add Chore")')
|
||||||
|
await expect(page.locator('form')).toBeVisible()
|
||||||
|
|
||||||
|
// Try submitting empty form
|
||||||
|
await page.click('button:has-text("Create")')
|
||||||
|
await expect(page.locator('.error')).toBeVisible()
|
||||||
|
|
||||||
|
// Fill valid fields
|
||||||
|
await page.fill('input[name="name"]', 'Take out trash')
|
||||||
|
await page.fill('textarea[name="description"]', 'Take out trash before dinner')
|
||||||
|
await page.fill('input[name="due_date"]', '2099-12-31')
|
||||||
|
await page.selectOption('select[name="assigned_child"]', { index: 0 })
|
||||||
|
await page.selectOption('select[name="reward"]', { index: 0 })
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await page.click('button:has-text("Create")')
|
||||||
|
await expect(page.locator('.success')).toBeVisible()
|
||||||
|
await expect(page.locator('.chore-list')).toContainText('Take out trash')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should cancel chore creation', async ({ page }) => {
|
||||||
|
await page.click('button:has-text("Add Chore")')
|
||||||
|
await page.click('button:has-text("Cancel")')
|
||||||
|
await expect(page.locator('form')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
44
frontend/vue-app/tests/global-setup.ts
Normal file
44
frontend/vue-app/tests/global-setup.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { chromium } from '@playwright/test'
|
||||||
|
|
||||||
|
const BACKEND_URL = 'http://localhost:5000'
|
||||||
|
const BASE_URL = 'https://localhost:5173'
|
||||||
|
|
||||||
|
export const E2E_EMAIL = 'e2e@test.com'
|
||||||
|
export const E2E_PASSWORD = 'E2eTestPass1!'
|
||||||
|
export const E2E_PIN = '1234'
|
||||||
|
export const STORAGE_STATE = 'tests/.auth/user.json'
|
||||||
|
|
||||||
|
// Matches PARENT_AUTH_KEY and PARENT_AUTH_EXPIRY_PERSISTENT in src/stores/auth.ts
|
||||||
|
const PARENT_AUTH_KEY = 'parentAuth'
|
||||||
|
const TWO_DAYS_MS = 172_800_000
|
||||||
|
|
||||||
|
export default async function globalSetup() {
|
||||||
|
// Reset all tables and insert a verified test user directly via the backend
|
||||||
|
const seedRes = await fetch(`${BACKEND_URL}/auth/e2e-seed`, { method: 'POST' })
|
||||||
|
if (!seedRes.ok) {
|
||||||
|
throw new Error(`e2e-seed failed: ${seedRes.status} ${await seedRes.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a real browser to log in so that HttpOnly auth cookies are captured correctly
|
||||||
|
const browser = await chromium.launch()
|
||||||
|
const context = await browser.newContext({ ignoreHTTPSErrors: true })
|
||||||
|
const page = await context.newPage()
|
||||||
|
|
||||||
|
await page.goto(`${BASE_URL}/auth/login`)
|
||||||
|
await page.fill('#email', E2E_EMAIL)
|
||||||
|
await page.fill('#password', E2E_PASSWORD)
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
// After login the router redirects away from /auth — wait for that navigation
|
||||||
|
await page.waitForURL(/\/(child|parent)/)
|
||||||
|
|
||||||
|
// Inject persistent parent auth into localStorage so tests can access /parent routes
|
||||||
|
// without navigating through the PIN prompt UI
|
||||||
|
await page.evaluate(
|
||||||
|
({ key, expiresAt }) => localStorage.setItem(key, JSON.stringify({ expiresAt })),
|
||||||
|
{ key: PARENT_AUTH_KEY, expiresAt: Date.now() + TWO_DAYS_MS },
|
||||||
|
)
|
||||||
|
|
||||||
|
await context.storageState({ path: STORAGE_STATE })
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
25
frontend/vue-app/tests/pages/ChildEditPage.ts
Normal file
25
frontend/vue-app/tests/pages/ChildEditPage.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export class ChildEditPage {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillName(name: string) {
|
||||||
|
await this.page.getByRole('textbox', { name: 'Name' }).fill(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillAge(age: string) {
|
||||||
|
await this.page.getByLabel('Age').fill(age);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectImage(imageAlt: string) {
|
||||||
|
await this.page.getByRole('img', { name: imageAlt }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
await this.page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/vue-app/tests/pages/LandingPage.ts
Normal file
13
frontend/vue-app/tests/pages/LandingPage.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export class LandingPage {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickGetStartedFree() {
|
||||||
|
await this.page.getByRole('button', { name: 'Get Started Free' }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user