temp changes
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m59s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m59s
This commit is contained in:
41
.github/agents/playwright-implement.agent.md
vendored
Normal file
41
.github/agents/playwright-implement.agent.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: playwright-implementation
|
||||||
|
|
||||||
|
description: Converts plans into code and performs self-healing verification.
|
||||||
|
tools:
|
||||||
|
[
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"terminal/*",
|
||||||
|
"playwright/*",
|
||||||
|
"edit",
|
||||||
|
"execute",
|
||||||
|
"vscode",
|
||||||
|
"web",
|
||||||
|
"search",
|
||||||
|
"todo",
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Senior QA Automation Engineer
|
||||||
|
|
||||||
|
You are a Playwright expert. Your goal is to create robust, flake-free E2E tests.
|
||||||
|
|
||||||
|
# Test Implementation & Healing Workflow
|
||||||
|
|
||||||
|
When you receive a test plan:
|
||||||
|
|
||||||
|
1. **Implement**: Generate the `.spec.ts` files in `/tests` using standard Playwright patterns.
|
||||||
|
2. **Verify**: Once files are written, execute the following command in the terminal:
|
||||||
|
`npx playwright test --agent=healer`
|
||||||
|
3. **Analyze & Repair**:
|
||||||
|
- If the Healer Agent proposes a patch, review it.
|
||||||
|
- If the test still fails after healing, check the **Flask backend logs** to see if it's an API error rather than a UI error.
|
||||||
|
4. **Final Check**: Only mark the task as "Complete" once `npx playwright test` returns a clean pass.
|
||||||
|
|
||||||
|
## Rules of Engagement
|
||||||
|
|
||||||
|
1. **Locators:** Prioritize `getByRole`, `getByLabel`, and `getByText`. Avoid CSS selectors unless necessary.
|
||||||
|
2. **Page Objects:** Always use the Page Object Model (POM). Check `tests/pages/` for existing objects before creating new ones.
|
||||||
|
3. **Environment:** The app runs at `https://localhost:5173` (HTTPS — self-signed cert). The backend runs at `http://localhost:5000`.
|
||||||
|
4. **Authentication:** Auth is handled globally via `storageState`. Do NOT navigate to `/auth/login` in any test — you are already logged in. Never hardcode credentials; import `E2E_EMAIL` and `E2E_PASSWORD` from `tests/global-setup.ts` if needed.
|
||||||
2
.github/agents/playwright-research.agent.md
vendored
2
.github/agents/playwright-research.agent.md
vendored
@@ -5,7 +5,7 @@ description: Scans codebase and explores URLs to create Playwright test plans.
|
|||||||
tools: ["read", "search", "playwright/*", "web"]
|
tools: ["read", "search", "playwright/*", "web"]
|
||||||
handoffs:
|
handoffs:
|
||||||
- label: Start Implementation
|
- label: Start Implementation
|
||||||
agent: agent
|
agent: playwright-implementation
|
||||||
prompt: Implement the test plan
|
prompt: Implement the test plan
|
||||||
send: true
|
send: true
|
||||||
# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.
|
# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.
|
||||||
|
|||||||
87
.github/agents/playwright-test-generator.agent.md
vendored
Normal file
87
.github/agents/playwright-test-generator.agent.md
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: playwright-test-generator
|
||||||
|
description: 'Use this agent when you need to create automated browser tests using Playwright Examples: <example>Context: User wants to generate a test for the test plan item. <test-suite><!-- Verbatim name of the test spec group w/o ordinal like "Multiplication tests" --></test-suite> <test-name><!-- Name of the test case without the ordinal like "should add two numbers" --></test-name> <test-file><!-- Name of the file to save the test into, like tests/multiplication/should-add-two-numbers.spec.ts --></test-file> <seed-file><!-- Seed file path from test plan --></seed-file> <body><!-- Test case content including steps and expectations --></body></example>'
|
||||||
|
tools:
|
||||||
|
- search
|
||||||
|
- playwright-test/browser_click
|
||||||
|
- playwright-test/browser_drag
|
||||||
|
- playwright-test/browser_evaluate
|
||||||
|
- playwright-test/browser_file_upload
|
||||||
|
- playwright-test/browser_handle_dialog
|
||||||
|
- playwright-test/browser_hover
|
||||||
|
- playwright-test/browser_navigate
|
||||||
|
- playwright-test/browser_press_key
|
||||||
|
- playwright-test/browser_select_option
|
||||||
|
- playwright-test/browser_snapshot
|
||||||
|
- playwright-test/browser_type
|
||||||
|
- playwright-test/browser_verify_element_visible
|
||||||
|
- playwright-test/browser_verify_list_visible
|
||||||
|
- playwright-test/browser_verify_text_visible
|
||||||
|
- playwright-test/browser_verify_value
|
||||||
|
- playwright-test/browser_wait_for
|
||||||
|
- playwright-test/generator_read_log
|
||||||
|
- playwright-test/generator_setup_page
|
||||||
|
- playwright-test/generator_write_test
|
||||||
|
model: Claude Sonnet 4
|
||||||
|
mcp-servers:
|
||||||
|
playwright-test:
|
||||||
|
type: stdio
|
||||||
|
command: npx
|
||||||
|
args:
|
||||||
|
- playwright
|
||||||
|
- run-test-mcp-server
|
||||||
|
tools:
|
||||||
|
- "*"
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
|
||||||
|
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
|
||||||
|
application behavior.
|
||||||
|
|
||||||
|
# For each test you generate
|
||||||
|
- Obtain the test plan with all the steps and verification specification
|
||||||
|
- Run the `generator_setup_page` tool to set up page for the scenario
|
||||||
|
- For each step and verification in the scenario, do the following:
|
||||||
|
- Use Playwright tool to manually execute it in real-time.
|
||||||
|
- Use the step description as the intent for each Playwright tool call.
|
||||||
|
- Retrieve generator log via `generator_read_log`
|
||||||
|
- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
|
||||||
|
- File should contain single test
|
||||||
|
- File name must be fs-friendly scenario name
|
||||||
|
- Test must be placed in a describe matching the top-level test plan item
|
||||||
|
- Test title must match the scenario name
|
||||||
|
- Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
|
||||||
|
multiple actions.
|
||||||
|
- Always use best practices from the log when generating tests.
|
||||||
|
|
||||||
|
<example-generation>
|
||||||
|
For following plan:
|
||||||
|
|
||||||
|
```markdown file=specs/plan.md
|
||||||
|
### 1. Adding New Todos
|
||||||
|
**Seed:** `tests/seed.spec.ts`
|
||||||
|
|
||||||
|
#### 1.1 Add Valid Todo
|
||||||
|
**Steps:**
|
||||||
|
1. Click in the "What needs to be done?" input field
|
||||||
|
|
||||||
|
#### 1.2 Add Multiple Todos
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Following file is generated:
|
||||||
|
|
||||||
|
```ts file=add-valid-todo.spec.ts
|
||||||
|
// spec: specs/plan.md
|
||||||
|
// seed: tests/seed.spec.ts
|
||||||
|
|
||||||
|
test.describe('Adding New Todos', () => {
|
||||||
|
test('Add Valid Todo', async { page } => {
|
||||||
|
// 1. Click in the "What needs to be done?" input field
|
||||||
|
await page.click(...);
|
||||||
|
|
||||||
|
...
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
</example-generation>
|
||||||
63
.github/agents/playwright-test-healer.agent.md
vendored
Normal file
63
.github/agents/playwright-test-healer.agent.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: playwright-test-healer
|
||||||
|
description: Use this agent when you need to debug and fix failing Playwright tests
|
||||||
|
tools:
|
||||||
|
- search
|
||||||
|
- edit
|
||||||
|
- playwright-test/browser_console_messages
|
||||||
|
- playwright-test/browser_evaluate
|
||||||
|
- playwright-test/browser_generate_locator
|
||||||
|
- playwright-test/browser_network_requests
|
||||||
|
- playwright-test/browser_snapshot
|
||||||
|
- playwright-test/test_debug
|
||||||
|
- playwright-test/test_list
|
||||||
|
- playwright-test/test_run
|
||||||
|
model: Claude Sonnet 4
|
||||||
|
mcp-servers:
|
||||||
|
playwright-test:
|
||||||
|
type: stdio
|
||||||
|
command: npx
|
||||||
|
args:
|
||||||
|
- playwright
|
||||||
|
- run-test-mcp-server
|
||||||
|
tools:
|
||||||
|
- "*"
|
||||||
|
---
|
||||||
|
|
||||||
|
You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and
|
||||||
|
resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
|
||||||
|
broken Playwright tests using a methodical approach.
|
||||||
|
|
||||||
|
Your workflow:
|
||||||
|
1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests
|
||||||
|
2. **Debug failed tests**: For each failing test run `test_debug`.
|
||||||
|
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
|
||||||
|
- Examine the error details
|
||||||
|
- Capture page snapshot to understand the context
|
||||||
|
- Analyze selectors, timing issues, or assertion failures
|
||||||
|
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
|
||||||
|
- Element selectors that may have changed
|
||||||
|
- Timing and synchronization issues
|
||||||
|
- Data dependencies or test environment problems
|
||||||
|
- Application changes that broke test assumptions
|
||||||
|
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
|
||||||
|
- Updating selectors to match current application state
|
||||||
|
- Fixing assertions and expected values
|
||||||
|
- Improving test reliability and maintainability
|
||||||
|
- For inherently dynamic data, utilize regular expressions to produce resilient locators
|
||||||
|
6. **Verification**: Restart the test after each fix to validate the changes
|
||||||
|
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
|
||||||
|
|
||||||
|
Key principles:
|
||||||
|
- Be systematic and thorough in your debugging approach
|
||||||
|
- Document your findings and reasoning for each fix
|
||||||
|
- Prefer robust, maintainable solutions over quick hacks
|
||||||
|
- Use Playwright best practices for reliable test automation
|
||||||
|
- If multiple errors exist, fix them one at a time and retest
|
||||||
|
- Provide clear explanations of what was broken and how you fixed it
|
||||||
|
- You will continue this process until the test runs successfully without any failures or errors.
|
||||||
|
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
|
||||||
|
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
|
||||||
|
of the expected behavior.
|
||||||
|
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
|
||||||
|
- Never wait for networkidle or use other discouraged or deprecated apis
|
||||||
81
.github/agents/playwright-test-planner.agent.md
vendored
Normal file
81
.github/agents/playwright-test-planner.agent.md
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
name: playwright-test-planner
|
||||||
|
description: Use this agent when you need to create comprehensive test plan for a web application or website
|
||||||
|
tools:
|
||||||
|
- search
|
||||||
|
- playwright-test/browser_click
|
||||||
|
- playwright-test/browser_close
|
||||||
|
- playwright-test/browser_console_messages
|
||||||
|
- playwright-test/browser_drag
|
||||||
|
- playwright-test/browser_evaluate
|
||||||
|
- playwright-test/browser_file_upload
|
||||||
|
- playwright-test/browser_handle_dialog
|
||||||
|
- playwright-test/browser_hover
|
||||||
|
- playwright-test/browser_navigate
|
||||||
|
- playwright-test/browser_navigate_back
|
||||||
|
- playwright-test/browser_network_requests
|
||||||
|
- playwright-test/browser_press_key
|
||||||
|
- playwright-test/browser_run_code
|
||||||
|
- playwright-test/browser_select_option
|
||||||
|
- playwright-test/browser_snapshot
|
||||||
|
- playwright-test/browser_take_screenshot
|
||||||
|
- playwright-test/browser_type
|
||||||
|
- playwright-test/browser_wait_for
|
||||||
|
- playwright-test/planner_setup_page
|
||||||
|
- playwright-test/planner_save_plan
|
||||||
|
model: Claude Sonnet 4
|
||||||
|
mcp-servers:
|
||||||
|
playwright-test:
|
||||||
|
type: stdio
|
||||||
|
command: npx
|
||||||
|
args:
|
||||||
|
- playwright
|
||||||
|
- run-test-mcp-server
|
||||||
|
tools:
|
||||||
|
- "*"
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
|
||||||
|
scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage
|
||||||
|
planning.
|
||||||
|
|
||||||
|
You will:
|
||||||
|
|
||||||
|
1. **Navigate and Explore**
|
||||||
|
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
|
||||||
|
- Explore the browser snapshot
|
||||||
|
- Do not take screenshots unless absolutely necessary
|
||||||
|
- Use `browser_*` tools to navigate and discover interface
|
||||||
|
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
|
||||||
|
|
||||||
|
2. **Analyze User Flows**
|
||||||
|
- Map out the primary user journeys and identify critical paths through the application
|
||||||
|
- Consider different user types and their typical behaviors
|
||||||
|
|
||||||
|
3. **Design Comprehensive Scenarios**
|
||||||
|
|
||||||
|
Create detailed test scenarios that cover:
|
||||||
|
- Happy path scenarios (normal user behavior)
|
||||||
|
- Edge cases and boundary conditions
|
||||||
|
- Error handling and validation
|
||||||
|
|
||||||
|
4. **Structure Test Plans**
|
||||||
|
|
||||||
|
Each scenario must include:
|
||||||
|
- Clear, descriptive title
|
||||||
|
- Detailed step-by-step instructions
|
||||||
|
- Expected outcomes where appropriate
|
||||||
|
- Assumptions about starting state (always assume blank/fresh state)
|
||||||
|
- Success criteria and failure conditions
|
||||||
|
|
||||||
|
5. **Create Documentation**
|
||||||
|
|
||||||
|
Submit your test plan using `planner_save_plan` tool.
|
||||||
|
|
||||||
|
**Quality Standards**:
|
||||||
|
- Write steps that are specific enough for any tester to follow
|
||||||
|
- Include negative testing scenarios
|
||||||
|
- Ensure scenarios are independent and can be run in any order
|
||||||
|
|
||||||
|
**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
|
||||||
|
professional formatting suitable for sharing with development and QA teams.
|
||||||
27
.github/agents/playwright.agent.md
vendored
27
.github/agents/playwright.agent.md
vendored
@@ -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.
|
|
||||||
34
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
34
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: "Copilot Setup Steps"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
copilot-setup-steps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
# Customize this step as needed
|
||||||
|
- name: Build application
|
||||||
|
run: npx run build
|
||||||
13
.vscode/mcp.json
vendored
Normal file
13
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"playwright-test": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"playwright",
|
||||||
|
"run-test-mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inputs": []
|
||||||
|
}
|
||||||
34
frontend/vue-app/.github/workflows/copilot-setup-steps.yml
vendored
Normal file
34
frontend/vue-app/.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: "Copilot Setup Steps"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
copilot-setup-steps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
# Customize this step as needed
|
||||||
|
- name: Build application
|
||||||
|
run: npx run build
|
||||||
27
frontend/vue-app/.github/workflows/playwright.yml
vendored
Normal file
27
frontend/vue-app/.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: npx playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
9
frontend/vue-app/.gitignore
vendored
9
frontend/vue-app/.gitignore
vendored
@@ -34,3 +34,12 @@ coverage
|
|||||||
|
|
||||||
# Vitest
|
# Vitest
|
||||||
__screenshots__/
|
__screenshots__/
|
||||||
|
|
||||||
|
*.old
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/playwright/.auth/
|
||||||
|
|||||||
BIN
frontend/vue-app/auth-setup-before-signin.png
Normal file
BIN
frontend/vue-app/auth-setup-before-signin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
BIN
frontend/vue-app/auth-setup-parent-fail.png
Normal file
BIN
frontend/vue-app/auth-setup-parent-fail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 338 KiB |
35
frontend/vue-app/e2e/.auth/user-no-pin.json
Normal file
35
frontend/vue-app/e2e/.auth/user-no-pin.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"cookies": [
|
||||||
|
{
|
||||||
|
"name": "refresh_token",
|
||||||
|
"value": "exz9voXnacTUkQGnKkc2QHLZA1DB3-7neit29Gtan5w",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/api/auth",
|
||||||
|
"expires": 1780801137.642288,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "Strict"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "access_token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI2NmQ5Yzk0NC05MzFmLTQyODktOWYxZS1kNzZhODQyZTM0MzIiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjYwMzd9.gjcizOIYTbdX6B-AobROaoJtMczY-7EnoyUco-b-xE8",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "Strict"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"origins": [
|
||||||
|
{
|
||||||
|
"origin": "https://localhost:5173",
|
||||||
|
"localStorage": [
|
||||||
|
{
|
||||||
|
"name": "authSyncEvent",
|
||||||
|
"value": "{\"type\":\"logout\",\"at\":1773025137442}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,17 +2,17 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "eGf3kkzCP5BSOTDLz-_lkzfeBD_j5mzKfWFrJbPD6CY",
|
"value": "aQ7Hdjmxefq4F6nLro-Sz0d2qO_3XN3v_tO4ioHOH6w",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1780671228.638382,
|
"expires": 1780799347.476442,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzI4OTYxMjh9.zGwBu1uhcEs_aH5MTDQFYKNWb9bjfgdIgSO9YnS3ez8",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzg1YWRjNC0yYmI4LTQyMTktOGFiNi05ZjkxNzAyNzA0MjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzMwMjQyNDd9.g3audbmZ_S-Bc5ZfgwvpfoQuJEjCS2vd3dF8baExFEA",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": -1,
|
"expires": -1,
|
||||||
@@ -27,11 +27,7 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1772895228428}"
|
"value": "{\"type\":\"parent_logout\",\"at\":1773023350687}"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "parentAuth",
|
|
||||||
"value": "{\"expiresAt\":1773068028652}"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
18
frontend/vue-app/e2e/auth-no-pin.setup.ts
Normal file
18
frontend/vue-app/e2e/auth-no-pin.setup.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { test as setup } from '@playwright/test'
|
||||||
|
import { STORAGE_STATE_NO_PIN, E2E_EMAIL, E2E_PASSWORD } from './e2e-constants'
|
||||||
|
|
||||||
|
setup('authenticate without parent pin', async ({ page }) => {
|
||||||
|
await page.goto('/auth/login')
|
||||||
|
|
||||||
|
await page.getByLabel('Email address').fill(E2E_EMAIL)
|
||||||
|
await page.getByLabel('Password').fill(E2E_PASSWORD)
|
||||||
|
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||||
|
|
||||||
|
// Wait for redirect to the authenticated area
|
||||||
|
await page.waitForURL(/\/(parent|child)/)
|
||||||
|
|
||||||
|
// Remove parent auth from localStorage so the PIN prompt appears
|
||||||
|
await page.evaluate(() => localStorage.removeItem('parentAuth'))
|
||||||
|
|
||||||
|
await page.context().storageState({ path: STORAGE_STATE_NO_PIN })
|
||||||
|
})
|
||||||
43
frontend/vue-app/e2e/auth.setup.ts
Normal file
43
frontend/vue-app/e2e/auth.setup.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { test as setup } from '@playwright/test'
|
||||||
|
import { STORAGE_STATE, E2E_EMAIL, E2E_PASSWORD, E2E_PIN } from './e2e-constants'
|
||||||
|
|
||||||
|
setup('authenticate', async ({ page }) => {
|
||||||
|
// Seed backend test data
|
||||||
|
const backendUrl = 'http://localhost:5000'
|
||||||
|
const seedRes = await page.request.post(`${backendUrl}/auth/e2e-seed`)
|
||||||
|
if (!seedRes.ok()) {
|
||||||
|
throw new Error(`e2e-seed failed: ${seedRes.status()} ${await seedRes.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto('/auth/login')
|
||||||
|
await page.getByLabel('Email address').fill(E2E_EMAIL)
|
||||||
|
await page.getByLabel('Password').fill(E2E_PASSWORD)
|
||||||
|
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||||
|
|
||||||
|
// After login the router redirects to /child (not parent-authenticated yet)
|
||||||
|
await page.waitForURL(/\/(parent|child)/)
|
||||||
|
|
||||||
|
// Click the LoginButton in the header to open the PIN modal
|
||||||
|
await page.getByRole('button', { name: 'Parent login' }).click()
|
||||||
|
|
||||||
|
// Fill in the PIN and submit
|
||||||
|
const pinInput = page.getByLabel('PIN').or(page.getByPlaceholder('Enter PIN'))
|
||||||
|
await pinInput.waitFor({ timeout: 5000 })
|
||||||
|
await page.screenshot({ path: 'auth-setup-before-pin.png' })
|
||||||
|
await pinInput.fill(E2E_PIN)
|
||||||
|
await page.screenshot({ path: 'auth-setup-after-pin.png' })
|
||||||
|
await page.getByRole('button', { name: 'Verify' }).click()
|
||||||
|
|
||||||
|
// LoginButton does router.push('/parent') after PIN - wait for it
|
||||||
|
await page.waitForURL(/\/parent(\/|$)/)
|
||||||
|
|
||||||
|
// Confirm parent mode is active by waiting for the Add Child FAB at /parent
|
||||||
|
try {
|
||||||
|
await page.getByRole('button', { name: 'Add Child' }).waitFor({ timeout: 5000 })
|
||||||
|
} catch (e) {
|
||||||
|
await page.screenshot({ path: 'auth-setup-parent-fail.png' })
|
||||||
|
throw new Error('Parent mode not reached after PIN entry. See auth-setup-parent-fail.png for details.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.context().storageState({ path: STORAGE_STATE })
|
||||||
|
})
|
||||||
16
frontend/vue-app/e2e/create-child/authorization.spec.ts
Normal file
16
frontend/vue-app/e2e/create-child/authorization.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// spec: e2e/plans/create-child.plan.md
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { STORAGE_STATE_NO_PIN } from '../e2e-constants'
|
||||||
|
|
||||||
|
test.use({ storageState: STORAGE_STATE_NO_PIN })
|
||||||
|
|
||||||
|
test.describe('Create Child', () => {
|
||||||
|
test('Add Child FAB is hidden when parent auth is expired', async ({ page }) => {
|
||||||
|
// Navigate to app root - with no parent auth, router redirects to /child
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
// expect: the 'Add Child' FAB is NOT visible (not in parent mode)
|
||||||
|
await expect(page.getByRole('button', { name: 'Add Child' })).not.toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
104
frontend/vue-app/e2e/create-child/happy-path.spec.ts
Normal file
104
frontend/vue-app/e2e/create-child/happy-path.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// spec: e2e/plans/create-child.plan.md
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const TEST_IMAGE = path.join(__dirname, '../../../../resources/logo/star_only.png')
|
||||||
|
|
||||||
|
async function deleteNamedChildren(request: any, names: string[]) {
|
||||||
|
const res = await request.get('/api/child/list')
|
||||||
|
const data = await res.json()
|
||||||
|
for (const child of data.children ?? []) {
|
||||||
|
if (names.includes(child.name)) {
|
||||||
|
await request.delete(`/api/child/${child.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAllChildren(request: any) {
|
||||||
|
const res = await request.get('/api/child/list')
|
||||||
|
const data = await res.json()
|
||||||
|
for (const child of data.children ?? []) {
|
||||||
|
await request.delete(`/api/child/${child.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Create Child', () => {
|
||||||
|
test('Create a child with name and age only', async ({ page, request }) => {
|
||||||
|
await deleteNamedChildren(request, ['Alice'])
|
||||||
|
|
||||||
|
// 1. Navigate to app root - router redirects to /parent (children list) when parent-authenticated
|
||||||
|
await page.goto('/')
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
|
||||||
|
// 2. Click the 'Add Child' FAB
|
||||||
|
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||||
|
await expect(page.getByLabel('Name')).toBeVisible()
|
||||||
|
await expect(page.getByLabel('Age')).toBeVisible()
|
||||||
|
|
||||||
|
// 3. Enter 'Alice' in the Name field
|
||||||
|
await page.getByLabel('Name').fill('Alice')
|
||||||
|
await expect(page.getByLabel('Name')).toHaveValue('Alice')
|
||||||
|
|
||||||
|
// 4. Enter '8' in the Age field
|
||||||
|
await page.getByLabel('Age').fill('8')
|
||||||
|
await expect(page.getByLabel('Age')).toHaveValue('8')
|
||||||
|
|
||||||
|
// 5. Leave Image as default and click Create
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
await expect(page.locator('.error')).not.toBeVisible()
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
await expect(page.getByText('Alice')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create a child via the inline Create button in empty state', async ({ page, request }) => {
|
||||||
|
await deleteAllChildren(request)
|
||||||
|
|
||||||
|
// 1. Navigate to app root - router redirects to /parent (children list)
|
||||||
|
await page.goto('/')
|
||||||
|
await expect(page.getByText('No children')).toBeVisible()
|
||||||
|
await expect(page.getByRole('button', { name: 'Create' })).toBeVisible()
|
||||||
|
|
||||||
|
// 2. Click the inline 'Create' button
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||||
|
|
||||||
|
// 3. Enter 'Bob' and '10', then submit
|
||||||
|
await page.getByLabel('Name').fill('Bob')
|
||||||
|
await page.getByLabel('Age').fill('10')
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
await expect(page.locator('.error')).not.toBeVisible()
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
await expect(page.getByText('Bob')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create a child with a custom uploaded image', async ({ page, request }) => {
|
||||||
|
await deleteNamedChildren(request, ['Grace'])
|
||||||
|
|
||||||
|
// 1. Navigate to app root - router redirects to /parent (children list)
|
||||||
|
await page.goto('/')
|
||||||
|
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||||
|
|
||||||
|
// 2. Enter 'Grace' and '6'
|
||||||
|
await page.getByLabel('Name').fill('Grace')
|
||||||
|
await page.getByLabel('Age').fill('6')
|
||||||
|
await expect(page.getByLabel('Name')).toHaveValue('Grace')
|
||||||
|
await expect(page.getByLabel('Age')).toHaveValue('6')
|
||||||
|
|
||||||
|
// 3. Upload a local image file via 'Add from device'
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||||
|
await page.getByRole('button', { name: 'Add from device' }).click()
|
||||||
|
const fileChooser = await fileChooserPromise
|
||||||
|
await fileChooser.setFiles(TEST_IMAGE)
|
||||||
|
|
||||||
|
// 4. Submit the form
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
await expect(page.locator('.error')).not.toBeVisible()
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
await expect(page.getByText('Grace')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
21
frontend/vue-app/e2e/create-child/navigation.spec.ts
Normal file
21
frontend/vue-app/e2e/create-child/navigation.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// spec: e2e/plans/create-child.plan.md
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('Create Child', () => {
|
||||||
|
test('Cancel navigates back without saving', async ({ page }) => {
|
||||||
|
// 1. Navigate to app root - router redirects to /parent (children list)
|
||||||
|
await page.goto('/')
|
||||||
|
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||||
|
|
||||||
|
// 2. Fill in 'Frank' and '9', then click Cancel
|
||||||
|
await page.getByLabel('Name').fill('Frank')
|
||||||
|
await page.getByLabel('Age').fill('9')
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||||
|
|
||||||
|
// expect: back on /parent and 'Frank' is NOT listed
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
await expect(page.getByText('Frank')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
37
frontend/vue-app/e2e/create-child/sse.spec.ts
Normal file
37
frontend/vue-app/e2e/create-child/sse.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// spec: e2e/plans/create-child.plan.md
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('Create Child', () => {
|
||||||
|
test('New child appears in list without page reload', async ({ page, context, request }) => {
|
||||||
|
// Clean up 'Hannah' before test
|
||||||
|
const res = await request.get('/api/child/list')
|
||||||
|
const data = await res.json()
|
||||||
|
for (const child of data.children ?? []) {
|
||||||
|
if (child.name === 'Hannah') {
|
||||||
|
await request.delete(`/api/child/${child.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Open two browser tabs both on /parent (children list)
|
||||||
|
await page.goto('/')
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
|
||||||
|
const tab2 = await context.newPage()
|
||||||
|
await tab2.goto('/')
|
||||||
|
await expect(tab2).toHaveURL('/parent')
|
||||||
|
|
||||||
|
// 2. In Tab 1, create child 'Hannah' age '4'
|
||||||
|
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||||
|
await page.getByLabel('Name').fill('Hannah')
|
||||||
|
await page.getByLabel('Age').fill('4')
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
await expect(page.getByText('Hannah')).toBeVisible()
|
||||||
|
|
||||||
|
// 3. Tab 2 should show 'Hannah' via SSE without a manual refresh
|
||||||
|
await expect(tab2.getByText('Hannah')).toBeVisible()
|
||||||
|
|
||||||
|
await tab2.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
74
frontend/vue-app/e2e/create-child/validation.spec.ts
Normal file
74
frontend/vue-app/e2e/create-child/validation.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// spec: e2e/plans/create-child.plan.md
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('Create Child', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Navigate to app root - router redirects to /parent (children list) when parent-authenticated
|
||||||
|
await page.goto('/')
|
||||||
|
await page.getByRole('button', { name: 'Add Child' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create Child' })).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Reject submission when Name is empty', async ({ page }) => {
|
||||||
|
// 2. Leave Name empty, enter '7' in Age, click Create
|
||||||
|
await page.getByLabel('Age').fill('7')
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
|
||||||
|
// expect: error message and still on create form
|
||||||
|
await expect(page.locator('.error')).toHaveText('Child name is required.')
|
||||||
|
await expect(page).toHaveURL('/parent/children/create')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Reject submission when Name is whitespace only', async ({ page }) => {
|
||||||
|
// 2. Enter only spaces in Name, enter '7' in Age, click Create
|
||||||
|
await page.getByLabel('Name').fill(' ')
|
||||||
|
await page.getByLabel('Age').fill('7')
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
|
||||||
|
// expect: error message and still on create form
|
||||||
|
await expect(page.locator('.error')).toHaveText('Child name is required.')
|
||||||
|
await expect(page).toHaveURL('/parent/children/create')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Reject submission when Age is empty', async ({ page }) => {
|
||||||
|
// 2. Enter 'Charlie', clear Age - Create button should be disabled
|
||||||
|
await page.getByLabel('Name').fill('Charlie')
|
||||||
|
await page.getByLabel('Age').clear()
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
|
||||||
|
await expect(page).toHaveURL('/parent/children/create')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Reject negative age', async ({ page }) => {
|
||||||
|
// 2. Enter 'Dave', enter '-1', click Create
|
||||||
|
await page.getByLabel('Name').fill('Dave')
|
||||||
|
await page.getByLabel('Age').fill('-1')
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
|
||||||
|
// expect: error message and still on create form
|
||||||
|
await expect(page.locator('.error')).toHaveText('Age must be a non-negative number.')
|
||||||
|
await expect(page).toHaveURL('/parent/children/create')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Enforce maximum Name length of 64 characters', async ({ page }) => {
|
||||||
|
// 2. Type a 65-character name - HTML maxlength caps it at 64
|
||||||
|
const longName = 'A'.repeat(65)
|
||||||
|
await page.getByLabel('Name').fill(longName)
|
||||||
|
await expect(page.getByLabel('Name')).toHaveValue('A'.repeat(64))
|
||||||
|
|
||||||
|
// 3. Enter '5' in Age and submit successfully
|
||||||
|
await page.getByLabel('Age').fill('5')
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
await expect(page).toHaveURL('/parent')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Reject age greater than 120', async ({ page }) => {
|
||||||
|
// 2. Enter 'Eve', enter '121' in Age - Create button should be disabled
|
||||||
|
await page.getByLabel('Name').fill('Eve')
|
||||||
|
await page.getByLabel('Age').fill('121')
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
|
||||||
|
await expect(page).toHaveURL('/parent/children/create')
|
||||||
|
})
|
||||||
|
})
|
||||||
5
frontend/vue-app/e2e/e2e-constants.ts
Normal file
5
frontend/vue-app/e2e/e2e-constants.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const STORAGE_STATE = 'e2e/.auth/user.json'
|
||||||
|
export const STORAGE_STATE_NO_PIN = 'e2e/.auth/user-no-pin.json'
|
||||||
|
export const E2E_EMAIL = 'e2e@test.com'
|
||||||
|
export const E2E_PASSWORD = 'E2eTestPass1!'
|
||||||
|
export const E2E_PIN = '1234'
|
||||||
18
frontend/vue-app/e2e/example.spec.ts
Normal file
18
frontend/vue-app/e2e/example.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Playwright/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get started link', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Click the get started link.
|
||||||
|
await page.getByRole('link', { name: 'Get started' }).click();
|
||||||
|
|
||||||
|
// Expects page to have a heading with the name of Installation.
|
||||||
|
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||||
|
});
|
||||||
174
frontend/vue-app/e2e/plans/create-child.plan.md
Normal file
174
frontend/vue-app/e2e/plans/create-child.plan.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Create Child
|
||||||
|
|
||||||
|
## Application Overview
|
||||||
|
|
||||||
|
Tests for creating a new child from the Parent dashboard. The user is authenticated and in parent mode via stored auth state (tests/.auth/user.json). All tests start at /parent/children (Children List view).
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### 1. Happy Path
|
||||||
|
|
||||||
|
**Seed:** `frontend/vue-app/seed.ts`
|
||||||
|
|
||||||
|
#### 1.1. Create a child with name and age only
|
||||||
|
|
||||||
|
**File:** `tests/create-child/happy-path.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children
|
||||||
|
- expect: The Children List page is displayed
|
||||||
|
2. Click the 'Add Child' floating action button (FAB) in the bottom-right corner
|
||||||
|
- expect: The Create Child form is displayed with Name, Age, and Image fields
|
||||||
|
3. Enter 'Alice' in the Name field
|
||||||
|
- expect: The Name field shows 'Alice'
|
||||||
|
4. Enter '8' in the Age field
|
||||||
|
- expect: The Age field shows '8'
|
||||||
|
5. Leave the Image field as the default pre-selected value and click the Save/Submit button
|
||||||
|
- expect: No error messages are displayed
|
||||||
|
- expect: Navigation returns to /parent/children
|
||||||
|
- expect: The child 'Alice' appears in the children list
|
||||||
|
|
||||||
|
#### 1.2. Create a child via the inline 'Create' button in empty state
|
||||||
|
|
||||||
|
**File:** `tests/create-child/happy-path.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children with no children in the account
|
||||||
|
- expect: An empty state message is shown with an inline 'Create' button
|
||||||
|
2. Click the inline 'Create' button
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
3. Enter 'Bob' in the Name field and '10' in the Age field, then click Save/Submit
|
||||||
|
- expect: No errors are displayed
|
||||||
|
- expect: 'Bob' appears in the children list
|
||||||
|
|
||||||
|
#### 1.3. Create a child with a custom uploaded image
|
||||||
|
|
||||||
|
**File:** `tests/create-child/happy-path.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Enter 'Grace' in the Name field and '6' in the Age field
|
||||||
|
- expect: Name and Age fields are populated
|
||||||
|
3. In the Image field, select the option to upload a local file and choose a valid PNG or JPEG
|
||||||
|
- expect: The image is selected and shown as a preview
|
||||||
|
4. Click the Save/Submit button
|
||||||
|
- expect: No error messages are displayed
|
||||||
|
- expect: 'Grace' appears in the children list with the uploaded image
|
||||||
|
|
||||||
|
### 2. Validation - Required Fields
|
||||||
|
|
||||||
|
**Seed:** `frontend/vue-app/seed.ts`
|
||||||
|
|
||||||
|
#### 2.1. Reject submission when Name is empty
|
||||||
|
|
||||||
|
**File:** `tests/create-child/validation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Leave the Name field empty, enter '7' in the Age field, and click Save/Submit
|
||||||
|
- expect: Error message 'Child name is required.' is displayed
|
||||||
|
- expect: The form does not navigate away
|
||||||
|
|
||||||
|
#### 2.2. Reject submission when Name is whitespace only
|
||||||
|
|
||||||
|
**File:** `tests/create-child/validation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Enter only spaces in the Name field, enter '7' in the Age field, and click Save/Submit
|
||||||
|
- expect: Error message 'Child name is required.' is displayed
|
||||||
|
- expect: The form does not navigate away
|
||||||
|
|
||||||
|
#### 2.3. Reject submission when Age is empty
|
||||||
|
|
||||||
|
**File:** `tests/create-child/validation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Enter 'Charlie' in the Name field, clear the Age field, and click Save/Submit
|
||||||
|
- expect: An age validation error is displayed
|
||||||
|
- expect: The form does not navigate away
|
||||||
|
|
||||||
|
### 3. Validation - Boundary Conditions
|
||||||
|
|
||||||
|
**Seed:** `frontend/vue-app/seed.ts`
|
||||||
|
|
||||||
|
#### 3.1. Reject negative age
|
||||||
|
|
||||||
|
**File:** `tests/create-child/validation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Enter 'Dave' in the Name field, enter '-1' in the Age field, and click Save/Submit
|
||||||
|
- expect: Error message 'Age must be a non-negative number.' is displayed
|
||||||
|
- expect: The form does not navigate away
|
||||||
|
|
||||||
|
#### 3.2. Enforce maximum Name length of 64 characters
|
||||||
|
|
||||||
|
**File:** `tests/create-child/validation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Attempt to type a name with 65 or more characters in the Name field
|
||||||
|
- expect: The input is capped at 64 characters
|
||||||
|
3. Enter '5' in the Age field and click Save/Submit
|
||||||
|
- expect: The form submits successfully with the 64-character name
|
||||||
|
|
||||||
|
#### 3.3. Reject age greater than 120
|
||||||
|
|
||||||
|
**File:** `tests/create-child/validation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Enter 'Eve' in the Name field, enter '121' in the Age field, and click Save/Submit
|
||||||
|
- expect: A validation error is shown or the value is capped at 120
|
||||||
|
|
||||||
|
### 4. Navigation
|
||||||
|
|
||||||
|
**Seed:** `frontend/vue-app/seed.ts`
|
||||||
|
|
||||||
|
#### 4.1. Cancel navigates back without saving
|
||||||
|
|
||||||
|
**File:** `tests/create-child/navigation.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to /parent/children and click the 'Add Child' FAB
|
||||||
|
- expect: The Create Child form is displayed
|
||||||
|
2. Enter 'Frank' in the Name field and '9' in the Age field, then click the Cancel button
|
||||||
|
- expect: Navigation returns to /parent/children
|
||||||
|
- expect: 'Frank' does NOT appear in the children list
|
||||||
|
|
||||||
|
### 5. Real-Time Updates via SSE
|
||||||
|
|
||||||
|
**Seed:** `frontend/vue-app/seed.ts`
|
||||||
|
|
||||||
|
#### 5.1. New child appears in list without page reload
|
||||||
|
|
||||||
|
**File:** `tests/create-child/sse.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Open two browser tabs both on /parent/children
|
||||||
|
- expect: Both tabs show the Children List page
|
||||||
|
2. In Tab 1, click 'Add Child' FAB, fill in name 'Hannah' and age '4', then submit
|
||||||
|
- expect: Tab 1 navigates to /parent/children and 'Hannah' is visible
|
||||||
|
3. Switch to Tab 2 which is still on /parent/children
|
||||||
|
- expect: 'Hannah' appears in Tab 2 without a manual refresh (SSE real-time update)
|
||||||
|
|
||||||
|
### 6. Authorization
|
||||||
|
|
||||||
|
**Seed:** `frontend/vue-app/seed.ts`
|
||||||
|
|
||||||
|
#### 6.1. Add Child FAB is hidden when parent auth is expired
|
||||||
|
|
||||||
|
**File:** `tests/create-child/authorization.spec.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Clear 'parentAuth' from localStorage to simulate expired parent authentication and navigate to /parent/children
|
||||||
|
- expect: The 'Add Child' FAB is NOT visible on the page
|
||||||
3
frontend/vue-app/e2e/seed.spec.ts
Normal file
3
frontend/vue-app/e2e/seed.spec.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default async function seed(page: any): Promise<void> {
|
||||||
|
// no-op seed
|
||||||
|
}
|
||||||
6
frontend/vue-app/package-lock.json
generated
6
frontend/vue-app/package-lock.json
generated
@@ -8,11 +8,11 @@
|
|||||||
"name": "chore-app-frontend",
|
"name": "chore-app-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/node": "^22.18.11",
|
"@types/node": "^22.18.11",
|
||||||
@@ -1527,6 +1527,7 @@
|
|||||||
"version": "1.58.2",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.58.2"
|
"playwright": "1.58.2"
|
||||||
@@ -4944,6 +4945,7 @@
|
|||||||
"version": "1.58.2",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.58.2"
|
"playwright-core": "1.58.2"
|
||||||
@@ -4962,6 +4964,7 @@
|
|||||||
"version": "1.58.2",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
@@ -4974,6 +4977,7 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/node": "^22.18.11",
|
"@types/node": "^22.18.11",
|
||||||
|
|||||||
@@ -1,34 +1,86 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test'
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
import { STORAGE_STATE, STORAGE_STATE_NO_PIN } from './e2e/e2e-constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests',
|
testDir: './e2e',
|
||||||
globalSetup: './tests/global-setup.ts',
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 1, // Retries help AI "healer" skills see if a failure is flaky
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('')`. */
|
||||||
baseURL: 'https://localhost:5173',
|
baseURL: 'https://localhost:5173',
|
||||||
trace: 'retain-on-failure', // AI needs this to "see" why a test failed
|
|
||||||
video: 'on-first-retry', // Great for visual debugging
|
|
||||||
screenshot: 'only-on-failure',
|
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for different environments */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
|
||||||
name: 'smoke',
|
{ name: 'setup-no-pin', testMatch: /auth-no-pin\.setup\.ts/ },
|
||||||
testMatch: /.*smoke.spec.ts/,
|
|
||||||
use: {
|
|
||||||
...devices['Desktop Chrome'],
|
|
||||||
storageState: 'tests/.auth/user.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE },
|
||||||
|
dependencies: ['setup'],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'chromium-no-pin',
|
||||||
|
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE_NO_PIN },
|
||||||
|
dependencies: ['setup-no-pin'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: 'firefox',
|
||||||
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: 'webkit',
|
||||||
|
// use: { ...devices['Desktop Safari'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
/* Run your local dev server before starting tests */
|
/* Run your local dev server before starting tests */
|
||||||
webServer: [
|
webServer: [
|
||||||
{
|
{
|
||||||
@@ -57,4 +109,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'npm run start',
|
||||||
|
// url: 'http://localhost:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user