Compare commits
18 Commits
electron-b
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e67c97821a | ||
|
|
40fa38a9ce | ||
|
|
96e4d3c394 | ||
|
|
c4f2bb5e3a | ||
|
|
f51408e32a | ||
| 6f9847b524 | |||
| 01279b61f5 | |||
| 43e7bc1c12 | |||
|
|
1a77dfb750 | ||
|
|
1365adad92 | ||
|
|
baccb962cf | ||
|
|
aa55588cbb | ||
|
|
5f63e05090 | ||
|
|
4391cb2881 | ||
| 0b9c243969 | |||
|
|
74c70c7fa0 | ||
|
|
f31eb5f6c9 | ||
|
|
9f976f011a |
207
.cursor/rules/harbor_pilot_universal.mdc
Normal file
207
.cursor/rules/harbor_pilot_universal.mdc
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
inherits: base_context.mdc
|
||||||
|
---
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"coaching_level": "standard",
|
||||||
|
"socratic_max_questions": 2,
|
||||||
|
"verbosity": "concise",
|
||||||
|
"timebox_minutes": 10,
|
||||||
|
"format_enforcement": "strict"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Harbor Pilot — Universal Directive for Human-Facing Technical Guides
|
||||||
|
|
||||||
|
**Author**: System/Shared
|
||||||
|
**Date**: 2025-08-21 (UTC)
|
||||||
|
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human Competence First*
|
||||||
|
|
||||||
|
> **Alignment with Base Context**
|
||||||
|
> - **Purpose fit**: Prioritizes human competence and collaboration while delivering reproducible artifacts.
|
||||||
|
> - **Output Contract**: This directive **adds universal constraints** for any technical topic while **inheriting** the Base Context contract sections.
|
||||||
|
> - **Toggles honored**: Uses the same toggle semantics; defaults above can be overridden by the caller.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Produce a **developer-grade, reproducible guide** for any technical topic that onboards a competent practitioner **without meta narration** and **with evidence-backed steps**.
|
||||||
|
|
||||||
|
## Scope & Constraints
|
||||||
|
- **One Markdown document** as the deliverable.
|
||||||
|
- Use **absolute dates** in **UTC** (e.g., `2025-08-21T14:22Z`) — avoid “today/yesterday”.
|
||||||
|
- Include at least **one diagram** (Mermaid preferred). Choose the most fitting type:
|
||||||
|
- `sequenceDiagram` (protocols/flows), `flowchart`, `stateDiagram`, `gantt` (timelines), or `classDiagram` (schemas).
|
||||||
|
- Provide runnable examples where applicable:
|
||||||
|
- **APIs**: `curl` + one client library (e.g., `httpx` for Python).
|
||||||
|
- **CLIs**: literal command blocks and expected output snippets.
|
||||||
|
- **Code**: minimal, self-contained samples (language appropriate).
|
||||||
|
- Cite **evidence** for *Works/Doesn’t* items (timestamps, filenames, line numbers, IDs/status codes, or logs).
|
||||||
|
- If something is unknown, output `TODO:<missing>` — **never invent**.
|
||||||
|
|
||||||
|
## Required Sections (extends Base Output Contract)
|
||||||
|
Follow this exact order **after** the Base Contract’s **Objective → Result → Use/Run** headers:
|
||||||
|
|
||||||
|
1. **Context & Scope**
|
||||||
|
- Problem statement, audience, in/out-of-scope bullets.
|
||||||
|
2. **Artifacts & Links**
|
||||||
|
- Repos/PRs, design docs, datasets/HARs/pcaps, scripts/tools, dashboards.
|
||||||
|
3. **Environment & Preconditions**
|
||||||
|
- OS/runtime, versions/build IDs, services/endpoints/URLs, credentials/auth mode (describe acquisition, do not expose secrets).
|
||||||
|
4. **Architecture / Process Overview**
|
||||||
|
- Short prose + **one diagram** selected from the list above.
|
||||||
|
5. **Interfaces & Contracts (choose one)**
|
||||||
|
- **API-based**: Endpoint table (*Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref*).
|
||||||
|
- **Data/Files**: I/O contract table (*Source, Format, Schema/Columns, Size, Validation rules*).
|
||||||
|
- **Systems/Hardware**: Interfaces table (*Port/Bus, Protocol, Voltage/Timing, Constraints*).
|
||||||
|
6. **Repro: End-to-End Procedure**
|
||||||
|
- Minimal copy-paste steps with code/commands and **expected outputs**.
|
||||||
|
7. **What Works (with Evidence)**
|
||||||
|
- Each item: **Time (UTC)** • **Artifact/Req IDs** • **Status/Result** • **Where to verify**.
|
||||||
|
8. **What Doesn’t (Evidence & Hypotheses)**
|
||||||
|
- Each failure: locus (file/endpoint/module), evidence snippet; short hypothesis and **next probe**.
|
||||||
|
9. **Risks, Limits, Assumptions**
|
||||||
|
- SLOs/limits, rate/size caps, security boundaries (CORS/CSRF/ACLs), retries/backoff/idempotency patterns.
|
||||||
|
10. **Next Steps (Owner • Exit Criteria • Target Date)**
|
||||||
|
- Actionable, assigned, and time-bound.
|
||||||
|
11. **References**
|
||||||
|
- Canonical docs, specs, tickets, prior analyses.
|
||||||
|
|
||||||
|
> **Competence Hooks (per Base Context; keep lightweight):**
|
||||||
|
> - *Why this works* (≤3 bullets) — core invariants or guarantees.
|
||||||
|
> - *Common pitfalls* (≤3 bullets) — the traps we saw in evidence.
|
||||||
|
> - *Next skill unlock* (1 line) — the next capability to implement/learn.
|
||||||
|
> - *Teach-back* (1 line) — prompt the reader to restate the flow/architecture.
|
||||||
|
|
||||||
|
> **Collaboration Hooks (per Base Context):**
|
||||||
|
> - Name reviewers for **Interfaces & Contracts** and the **diagram**.
|
||||||
|
> - Short **sign-off checklist** before merging/publishing the guide.
|
||||||
|
|
||||||
|
## Do / Don’t (Base-aligned)
|
||||||
|
- **Do** quantify progress only against a defined scope with acceptance criteria.
|
||||||
|
- **Do** include minimal sample payloads/headers or I/O schemas; redact sensitive values.
|
||||||
|
- **Do** keep commentary lean; if timeboxed, move depth to **Deferred for depth**.
|
||||||
|
- **Don’t** use marketing language or meta narration (“Perfect!”, “tool called”, “new chat”).
|
||||||
|
- **Don’t** include IDE-specific chatter or internal rules unrelated to the task.
|
||||||
|
|
||||||
|
## Validation Checklist (self-check before returning)
|
||||||
|
- [ ] All Required Sections present and ordered.
|
||||||
|
- [ ] Diagram compiles (basic Mermaid syntax) and fits the problem.
|
||||||
|
- [ ] If API-based, **Auth** and **Key Headers/Params** are listed for each endpoint.
|
||||||
|
- [ ] Repro section includes commands/code **and expected outputs**.
|
||||||
|
- [ ] Every Works/Doesn’t item has **UTC timestamp**, **status/result**, and **verifiable evidence**.
|
||||||
|
- [ ] Next Steps include **Owner**, **Exit Criteria**, **Target Date**.
|
||||||
|
- [ ] Unknowns are `TODO:<missing>` — no fabrication.
|
||||||
|
- [ ] Base **Output Contract** sections satisfied (Objective/Result/Use/Run/Competence/Collaboration/Assumptions/References).
|
||||||
|
|
||||||
|
## Universal Template (fill-in)
|
||||||
|
```markdown
|
||||||
|
# <Title> — Working Notes (As of YYYY-MM-DDTHH:MMZ)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
<one line>
|
||||||
|
|
||||||
|
## Result
|
||||||
|
<link to the produced guide file or say “this document”>
|
||||||
|
|
||||||
|
## Use/Run
|
||||||
|
<how to apply/test and where to run samples>
|
||||||
|
|
||||||
|
## Context & Scope
|
||||||
|
- Audience: <role(s)>
|
||||||
|
- In scope: <bullets>
|
||||||
|
- Out of scope: <bullets>
|
||||||
|
|
||||||
|
## Artifacts & Links
|
||||||
|
- Repo/PR: <link>
|
||||||
|
- Data/Logs: <paths or links>
|
||||||
|
- Scripts/Tools: <paths>
|
||||||
|
- Dashboards: <links>
|
||||||
|
|
||||||
|
## Environment & Preconditions
|
||||||
|
- OS/Runtime: <details>
|
||||||
|
- Versions/Builds: <list>
|
||||||
|
- Services/Endpoints: <list>
|
||||||
|
- Auth mode: <Bearer/Session/Keys + how acquired>
|
||||||
|
|
||||||
|
## Architecture / Process Overview
|
||||||
|
<short prose>
|
||||||
|
```mermaid
|
||||||
|
<one suitable diagram: sequenceDiagram | flowchart | stateDiagram | gantt | classDiagram>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interfaces & Contracts
|
||||||
|
### If API-based
|
||||||
|
| Step | Method | Path/URL | Auth | Key Headers/Params | Sample |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| <…> | <…> | <…> | <…> | <…> | below |
|
||||||
|
|
||||||
|
### If Data/Files
|
||||||
|
| Source | Format | Schema/Columns | Size | Validation |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| <…> | <…> | <…> | <…> | <…> |
|
||||||
|
|
||||||
|
### If Systems/Hardware
|
||||||
|
| Interface | Protocol | Timing/Voltage | Constraints | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| <…> | <…> | <…> | <…> | <…> |
|
||||||
|
|
||||||
|
## Repro: End-to-End Procedure
|
||||||
|
```bash
|
||||||
|
# commands / curl examples (redacted where necessary)
|
||||||
|
```
|
||||||
|
```python
|
||||||
|
# minimal client library example (language appropriate)
|
||||||
|
```
|
||||||
|
> Expected output: <snippet/checks>
|
||||||
|
|
||||||
|
## What Works (Evidence)
|
||||||
|
- ✅ <short statement>
|
||||||
|
- **Time**: <YYYY-MM-DDTHH:MMZ>
|
||||||
|
- **Evidence**: file/line/log or request id/status
|
||||||
|
- **Verify at**: <where>
|
||||||
|
|
||||||
|
## What Doesn’t (Evidence & Hypotheses)
|
||||||
|
- ❌ <short failure> at `<component/endpoint/file>`
|
||||||
|
- **Time**: <YYYY-MM-DDTHH:MMZ>
|
||||||
|
- **Evidence**: <snippet/id/status>
|
||||||
|
- **Hypothesis**: <short>
|
||||||
|
- **Next probe**: <short>
|
||||||
|
|
||||||
|
## Risks, Limits, Assumptions
|
||||||
|
<bullets: limits, security boundaries, retries/backoff, idempotency, SLOs>
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| <name> | <action> | <measurable outcome> | <YYYY-MM-DD> |
|
||||||
|
|
||||||
|
## References
|
||||||
|
<links/titles>
|
||||||
|
|
||||||
|
## Competence Hooks
|
||||||
|
- *Why this works*: <≤3 bullets>
|
||||||
|
- *Common pitfalls*: <≤3 bullets>
|
||||||
|
- *Next skill unlock*: <1 line>
|
||||||
|
- *Teach-back*: <1 line>
|
||||||
|
|
||||||
|
## Collaboration Hooks
|
||||||
|
- Reviewers: <names/roles>
|
||||||
|
- Sign-off checklist: <≤5 checks>
|
||||||
|
|
||||||
|
## Assumptions & Limits
|
||||||
|
<bullets>
|
||||||
|
|
||||||
|
## Deferred for depth
|
||||||
|
<park deeper material here to respect timeboxing>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Notes for Implementers:**
|
||||||
|
- Respect Base *Do-Not* (no filler, no invented facts, no censorship).
|
||||||
|
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
|
||||||
|
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
|
||||||
|
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
||||||
|
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
|
||||||
356
.cursor/rules/playwright-test-investigation.mdc
Normal file
356
.cursor/rules/playwright-test-investigation.mdc
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
---
|
||||||
|
description: when working with playwright tests either generating them or using them to test code
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Playwright Test Investigation — Harbor Pilot Directive
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: 2025-08-21T14:22Z
|
||||||
|
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
||||||
|
|
||||||
|
## Context & Scope
|
||||||
|
- **Audience**: Developers debugging Playwright test failures
|
||||||
|
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
||||||
|
- **Out of scope**: Test writing best practices, CI/CD configuration
|
||||||
|
|
||||||
|
## Artifacts & Links
|
||||||
|
- Test results: `test-results/` directory
|
||||||
|
- Error context: `error-context.md` files with page snapshots
|
||||||
|
- Trace files: `trace.zip` files for failed tests
|
||||||
|
- HTML reports: Interactive test reports with screenshots
|
||||||
|
|
||||||
|
## Environment & Preconditions
|
||||||
|
- OS/Runtime: Linux/Windows/macOS with Node.js
|
||||||
|
- Versions: Playwright test framework, browser drivers
|
||||||
|
- Services: Local test server (localhost:8080), test data setup
|
||||||
|
- Auth mode: None required for test investigation
|
||||||
|
|
||||||
|
## Architecture / Process Overview
|
||||||
|
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Test Failure] --> B[Check Error Context]
|
||||||
|
B --> C[Analyze Page Snapshot]
|
||||||
|
C --> D[Identify UI Conflicts]
|
||||||
|
D --> E[Check Trace Files]
|
||||||
|
E --> F[Verify Selector Uniqueness]
|
||||||
|
F --> G[Test Selector Fixes]
|
||||||
|
G --> H[Document Root Cause]
|
||||||
|
|
||||||
|
B --> I[Check Test Results Directory]
|
||||||
|
I --> J[Locate Failed Test Results]
|
||||||
|
J --> K[Extract Error Details]
|
||||||
|
|
||||||
|
D --> L[Multiple Alerts?]
|
||||||
|
L --> M[Button Text Conflicts?]
|
||||||
|
M --> N[Timing Issues?]
|
||||||
|
|
||||||
|
E --> O[Use Trace Viewer]
|
||||||
|
O --> P[Analyze Action Sequence]
|
||||||
|
P --> Q[Identify Failure Point]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interfaces & Contracts
|
||||||
|
|
||||||
|
### Test Results Structure
|
||||||
|
| Component | Format | Content | Validation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
||||||
|
| Trace Files | ZIP archive | Detailed execution trace | Use `npx playwright show-trace` |
|
||||||
|
| HTML Reports | Interactive HTML | Screenshots, traces, logs | Check browser for full report |
|
||||||
|
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
||||||
|
|
||||||
|
### Investigation Commands
|
||||||
|
| Step | Command | Expected Output | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
||||||
|
| Check error context | `cat test-results/*/error-context.md` | Page snapshots | Look for UI state conflicts |
|
||||||
|
| View traces | `npx playwright show-trace trace.zip` | Interactive trace viewer | Analyze exact failure sequence |
|
||||||
|
|
||||||
|
## Repro: End-to-End Investigation Procedure
|
||||||
|
|
||||||
|
### 1. Locate Failed Test Results
|
||||||
|
```bash
|
||||||
|
# Find all results for a specific test
|
||||||
|
find test-results -name "*test-name*" -type d
|
||||||
|
|
||||||
|
# Check for error context files
|
||||||
|
find test-results -name "error-context.md" | head -5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Analyze Error Context
|
||||||
|
```bash
|
||||||
|
# Read error context for specific test
|
||||||
|
cat test-results/test-name-test-description-browser/error-context.md
|
||||||
|
|
||||||
|
# Look for UI conflicts in page snapshot
|
||||||
|
grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Check Trace Files
|
||||||
|
```bash
|
||||||
|
# List available trace files
|
||||||
|
find test-results -name "*.zip" | grep trace
|
||||||
|
|
||||||
|
# View trace in browser
|
||||||
|
npx playwright show-trace test-results/test-name/trace.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Investigate Selector Issues
|
||||||
|
```typescript
|
||||||
|
// Check for multiple elements with same text
|
||||||
|
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
||||||
|
|
||||||
|
// Use more specific selectors
|
||||||
|
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Works (Evidence)
|
||||||
|
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
||||||
|
- **Verify at**: Error context files in test results directory
|
||||||
|
|
||||||
|
- ✅ **Trace files** capture detailed execution sequence for failed tests
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: `trace.zip` files available for all failed tests
|
||||||
|
- **Verify at**: Use `npx playwright show-trace <filename>`
|
||||||
|
|
||||||
|
- ✅ **Page snapshots** reveal UI conflicts like multiple alerts with duplicate button text
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: YAML snapshots show registration + export alerts simultaneously
|
||||||
|
- **Verify at**: Error context markdown files
|
||||||
|
|
||||||
|
## What Doesn't (Evidence & Hypotheses)
|
||||||
|
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
||||||
|
- **Hypothesis**: Selector ambiguity due to multiple alerts with conflicting button text
|
||||||
|
- **Next probe**: Use more specific selectors or dismiss alerts sequentially
|
||||||
|
|
||||||
|
- ❌ **Timing-dependent tests** fail due to alert stacking at `src/views/ContactsView.vue:860,1283`
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: Both alerts use identical 1000ms delays, ensuring simultaneous display
|
||||||
|
- **Hypothesis**: Race condition between alert displays creates UI conflicts
|
||||||
|
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
||||||
|
|
||||||
|
## Risks, Limits, Assumptions
|
||||||
|
- **Trace file size**: Large trace files may impact storage and analysis time
|
||||||
|
- **Browser compatibility**: Trace viewer requires specific browser support
|
||||||
|
- **Test isolation**: Shared state between tests may affect investigation results
|
||||||
|
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
||||||
|
| Development Team | Implement alert queuing system | No overlapping alerts with conflicting buttons | 2025-08-25 |
|
||||||
|
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
||||||
|
|
||||||
|
## References
|
||||||
|
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
||||||
|
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
||||||
|
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
||||||
|
|
||||||
|
## Competence Hooks
|
||||||
|
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
||||||
|
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
||||||
|
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
||||||
|
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
||||||
|
|
||||||
|
## Collaboration Hooks
|
||||||
|
- **Reviewers**: QA team, test automation engineers
|
||||||
|
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
||||||
|
|
||||||
|
## Assumptions & Limits
|
||||||
|
- Test results directory structure follows Playwright conventions
|
||||||
|
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
||||||
|
- Error context files contain valid YAML page snapshots
|
||||||
|
- Browser environment supports trace viewer functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Active investigation directive
|
||||||
|
**Priority**: High
|
||||||
|
**Maintainer**: Development team
|
||||||
|
**Next Review**: 2025-09-21
|
||||||
|
# Playwright Test Investigation — Harbor Pilot Directive
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: 2025-08-21T14:22Z
|
||||||
|
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
||||||
|
|
||||||
|
## Context & Scope
|
||||||
|
- **Audience**: Developers debugging Playwright test failures
|
||||||
|
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
||||||
|
- **Out of scope**: Test writing best practices, CI/CD configuration
|
||||||
|
|
||||||
|
## Artifacts & Links
|
||||||
|
- Test results: `test-results/` directory
|
||||||
|
- Error context: `error-context.md` files with page snapshots
|
||||||
|
- Trace files: `trace.zip` files for failed tests
|
||||||
|
- HTML reports: Interactive test reports with screenshots
|
||||||
|
|
||||||
|
## Environment & Preconditions
|
||||||
|
- OS/Runtime: Linux/Windows/macOS with Node.js
|
||||||
|
- Versions: Playwright test framework, browser drivers
|
||||||
|
- Services: Local test server (localhost:8080), test data setup
|
||||||
|
- Auth mode: None required for test investigation
|
||||||
|
|
||||||
|
## Architecture / Process Overview
|
||||||
|
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Test Failure] --> B[Check Error Context]
|
||||||
|
B --> C[Analyze Page Snapshot]
|
||||||
|
C --> D[Identify UI Conflicts]
|
||||||
|
D --> E[Check Trace Files]
|
||||||
|
E --> F[Verify Selector Uniqueness]
|
||||||
|
F --> G[Test Selector Fixes]
|
||||||
|
G --> H[Document Root Cause]
|
||||||
|
|
||||||
|
B --> I[Check Test Results Directory]
|
||||||
|
I --> J[Locate Failed Test Results]
|
||||||
|
J --> K[Extract Error Details]
|
||||||
|
|
||||||
|
D --> L[Multiple Alerts?]
|
||||||
|
L --> M[Button Text Conflicts?]
|
||||||
|
M --> N[Timing Issues?]
|
||||||
|
|
||||||
|
E --> O[Use Trace Viewer]
|
||||||
|
O --> P[Analyze Action Sequence]
|
||||||
|
P --> Q[Identify Failure Point]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interfaces & Contracts
|
||||||
|
|
||||||
|
### Test Results Structure
|
||||||
|
| Component | Format | Content | Validation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
||||||
|
| Trace Files | ZIP archive | Detailed execution trace | Use `npx playwright show-trace` |
|
||||||
|
| HTML Reports | Interactive HTML | Screenshots, traces, logs | Check browser for full report |
|
||||||
|
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
||||||
|
|
||||||
|
### Investigation Commands
|
||||||
|
| Step | Command | Expected Output | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
||||||
|
| Check error context | `cat test-results/*/error-context.md` | Page snapshots | Look for UI state conflicts |
|
||||||
|
| View traces | `npx playwright show-trace trace.zip` | Interactive trace viewer | Analyze exact failure sequence |
|
||||||
|
|
||||||
|
## Repro: End-to-End Investigation Procedure
|
||||||
|
|
||||||
|
### 1. Locate Failed Test Results
|
||||||
|
```bash
|
||||||
|
# Find all results for a specific test
|
||||||
|
find test-results -name "*test-name*" -type d
|
||||||
|
|
||||||
|
# Check for error context files
|
||||||
|
find test-results -name "error-context.md" | head -5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Analyze Error Context
|
||||||
|
```bash
|
||||||
|
# Read error context for specific test
|
||||||
|
cat test-results/test-name-test-description-browser/error-context.md
|
||||||
|
|
||||||
|
# Look for UI conflicts in page snapshot
|
||||||
|
grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Check Trace Files
|
||||||
|
```bash
|
||||||
|
# List available trace files
|
||||||
|
find test-results -name "*.zip" | grep trace
|
||||||
|
|
||||||
|
# View trace in browser
|
||||||
|
npx playwright show-trace test-results/test-name/trace.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Investigate Selector Issues
|
||||||
|
```typescript
|
||||||
|
// Check for multiple elements with same text
|
||||||
|
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
||||||
|
|
||||||
|
// Use more specific selectors
|
||||||
|
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Works (Evidence)
|
||||||
|
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
||||||
|
- **Verify at**: Error context files in test results directory
|
||||||
|
|
||||||
|
- ✅ **Trace files** capture detailed execution sequence for failed tests
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: `trace.zip` files available for all failed tests
|
||||||
|
- **Verify at**: Use `npx playwright show-trace <filename>`
|
||||||
|
|
||||||
|
- ✅ **Page snapshots** reveal UI conflicts like multiple alerts with duplicate button text
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: YAML snapshots show registration + export alerts simultaneously
|
||||||
|
- **Verify at**: Error context markdown files
|
||||||
|
|
||||||
|
## What Doesn't (Evidence & Hypotheses)
|
||||||
|
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
||||||
|
- **Hypothesis**: Selector ambiguity due to multiple alerts with conflicting button text
|
||||||
|
- **Next probe**: Use more specific selectors or dismiss alerts sequentially
|
||||||
|
|
||||||
|
- ❌ **Timing-dependent tests** fail due to alert stacking at `src/views/ContactsView.vue:860,1283`
|
||||||
|
- **Time**: 2025-08-21T14:22Z
|
||||||
|
- **Evidence**: Both alerts use identical 1000ms delays, ensuring simultaneous display
|
||||||
|
- **Hypothesis**: Race condition between alert displays creates UI conflicts
|
||||||
|
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
||||||
|
|
||||||
|
## Risks, Limits, Assumptions
|
||||||
|
- **Trace file size**: Large trace files may impact storage and analysis time
|
||||||
|
- **Browser compatibility**: Trace viewer requires specific browser support
|
||||||
|
- **Test isolation**: Shared state between tests may affect investigation results
|
||||||
|
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
||||||
|
| Development Team | Implement alert queuing system | No overlapping alerts with conflicting buttons | 2025-08-25 |
|
||||||
|
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
||||||
|
|
||||||
|
## References
|
||||||
|
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
||||||
|
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
||||||
|
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
||||||
|
|
||||||
|
## Competence Hooks
|
||||||
|
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
||||||
|
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
||||||
|
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
||||||
|
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
||||||
|
|
||||||
|
## Collaboration Hooks
|
||||||
|
- **Reviewers**: QA team, test automation engineers
|
||||||
|
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
||||||
|
|
||||||
|
## Assumptions & Limits
|
||||||
|
- Test results directory structure follows Playwright conventions
|
||||||
|
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
||||||
|
- Error context files contain valid YAML page snapshots
|
||||||
|
- Browser environment supports trace viewer functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Active investigation directive
|
||||||
|
**Priority**: High
|
||||||
|
**Maintainer**: Development team
|
||||||
|
**Next Review**: 2025-09-21
|
||||||
26
doc/duplicate-account-import-implementation.md
Normal file
26
doc/duplicate-account-import-implementation.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## What Works (Evidence)
|
||||||
|
|
||||||
|
- ✅ **ImportAccountView duplicate check**
|
||||||
|
- **Time**: 2025-01-27T14:30:00Z
|
||||||
|
- **Evidence**: Added `checkForDuplicateAccount()` method with DID derivation and database query
|
||||||
|
- **Verify at**: `src/views/ImportAccountView.vue:180-200`
|
||||||
|
|
||||||
|
- ✅ **ImportAccountView error handling**
|
||||||
|
- **Time**: 2025-01-27T15:00:00Z
|
||||||
|
- **Evidence**: Enhanced error handling to catch duplicate errors from saveNewIdentity and display user-friendly warnings
|
||||||
|
- **Verify at**: `src/views/ImportAccountView.vue:230-240`
|
||||||
|
|
||||||
|
- ✅ **ImportDerivedAccountView duplicate check**
|
||||||
|
- **Time**: 2025-01-27T14:30:00Z
|
||||||
|
- **Evidence**: Added duplicate check in `incrementDerivation()` method
|
||||||
|
- **Verify at**: `src/views/ImportDerivedAccountView.vue:170-190`
|
||||||
|
|
||||||
|
- ✅ **saveNewIdentity safety check**
|
||||||
|
- **Time**: 2025-01-27T14:30:00Z
|
||||||
|
- **Evidence**: Added database query before INSERT operation
|
||||||
|
- **Verify at**: `src/libs/util.ts:625-635`
|
||||||
|
|
||||||
|
- ✅ **User-friendly error messages**
|
||||||
|
- **Time**: 2025-01-27T14:30:00Z
|
||||||
|
- **Evidence**: Clear warning messages in both import views
|
||||||
|
- **Verify at**: ImportAccountView and ImportDerivedAccountView notification calls
|
||||||
@@ -1689,3 +1689,19 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
|
|||||||
title: "They're Added To Your List",
|
title: "They're Added To Your List",
|
||||||
message: "Would you like to go to the main page now?",
|
message: "Would you like to go to the main page now?",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ImportAccountView.vue specific constants
|
||||||
|
// Used in: ImportAccountView.vue (onImportClick method - duplicate account warning)
|
||||||
|
export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = {
|
||||||
|
title: "Account Already Imported",
|
||||||
|
message:
|
||||||
|
"This account has already been imported. Please use a different seed phrase or check your existing accounts.",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ImportDerivedAccountView.vue specific constants
|
||||||
|
// Used in: ImportDerivedAccountView.vue (incrementDerivation method - duplicate derived account warning)
|
||||||
|
export const NOTIFY_DUPLICATE_DERIVED_ACCOUNT = {
|
||||||
|
title: "Derived Account Already Exists",
|
||||||
|
message:
|
||||||
|
"This derived account already exists. Please try a different derivation path.",
|
||||||
|
};
|
||||||
|
|||||||
@@ -626,6 +626,18 @@ export async function saveNewIdentity(
|
|||||||
// add to the new sql db
|
// add to the new sql db
|
||||||
const platformService = await getPlatformService();
|
const platformService = await getPlatformService();
|
||||||
|
|
||||||
|
// Check if account already exists before attempting to save
|
||||||
|
const existingAccount = await platformService.dbQuery(
|
||||||
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
|
[identity.did],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingAccount?.values?.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Account with DID ${identity.did} already exists. Cannot import duplicate account.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const secrets = await platformService.dbQuery(
|
const secrets = await platformService.dbQuery(
|
||||||
`SELECT secretBase64 FROM secret`,
|
`SELECT secretBase64 FROM secret`,
|
||||||
);
|
);
|
||||||
@@ -660,10 +672,8 @@ export async function saveNewIdentity(
|
|||||||
|
|
||||||
await platformService.insertNewDidIntoSettings(identity.did);
|
await platformService.insertNewDidIntoSettings(identity.did);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to update default settings:", error);
|
logger.error("Failed to save new identity:", error);
|
||||||
throw new Error(
|
throw error;
|
||||||
"Failed to set default settings. Please try again or restart the app.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ import {
|
|||||||
didInfoForContact,
|
didInfoForContact,
|
||||||
displayAmount,
|
displayAmount,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
|
isDid,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
@@ -289,6 +290,7 @@ import {
|
|||||||
NOTIFY_REGISTRATION_ERROR,
|
NOTIFY_REGISTRATION_ERROR,
|
||||||
NOTIFY_SERVER_ACCESS_ERROR,
|
NOTIFY_SERVER_ACCESS_ERROR,
|
||||||
NOTIFY_NO_IDENTITY_ERROR,
|
NOTIFY_NO_IDENTITY_ERROR,
|
||||||
|
NOTIFY_CONTACT_INVALID_DID,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
||||||
|
|
||||||
@@ -380,22 +382,29 @@ export default class DIDView extends Vue {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines which DID to display based on URL parameters
|
* Determines which DID to display based on URL parameters
|
||||||
* Falls back to active DID if no parameter provided
|
* Validates DID format and shows error for invalid DIDs
|
||||||
*/
|
*/
|
||||||
private async determineDIDToDisplay() {
|
private async determineDIDToDisplay() {
|
||||||
const pathParam = window.location.pathname.substring("/did/".length);
|
const pathParam = window.location.pathname.substring("/did/".length);
|
||||||
let showDid = pathParam;
|
let showDid = pathParam;
|
||||||
|
|
||||||
if (!showDid) {
|
if (!showDid) {
|
||||||
|
// No DID provided in URL, use active DID
|
||||||
showDid = this.activeDid;
|
showDid = this.activeDid;
|
||||||
if (showDid) {
|
this.notifyDefaultToActiveDID();
|
||||||
this.notifyDefaultToActiveDID();
|
} else {
|
||||||
|
// DID provided in URL, validate it
|
||||||
|
const decodedDid = decodeURIComponent(showDid);
|
||||||
|
if (!isDid(decodedDid)) {
|
||||||
|
// Invalid DID format - show error and redirect
|
||||||
|
this.notify.error(NOTIFY_CONTACT_INVALID_DID.message, TIMEOUTS.LONG);
|
||||||
|
this.$router.push({ name: "home" });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
showDid = decodedDid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDid) {
|
this.viewingDid = showDid;
|
||||||
this.viewingDid = decodeURIComponent(showDid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -87,10 +87,15 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
import {
|
||||||
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
|
deriveAddress,
|
||||||
|
newIdentifier,
|
||||||
|
} from "../libs/crypto";
|
||||||
import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
|
import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import Account View Component
|
* Import Account View Component
|
||||||
@@ -198,6 +203,16 @@ export default class ImportAccountView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check for duplicate account before importing
|
||||||
|
const isDuplicate = await this.checkForDuplicateAccount();
|
||||||
|
if (isDuplicate) {
|
||||||
|
this.notify.warning(
|
||||||
|
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await importFromMnemonic(
|
await importFromMnemonic(
|
||||||
this.mnemonic,
|
this.mnemonic,
|
||||||
this.derivationPath,
|
this.derivationPath,
|
||||||
@@ -223,12 +238,64 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.$logError("Import failed: " + error);
|
this.$logError("Import failed: " + error);
|
||||||
|
|
||||||
|
// Check if this is a duplicate account error from saveNewIdentity
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (
|
||||||
|
errorMessage.includes("already exists") &&
|
||||||
|
errorMessage.includes("Cannot import duplicate account")
|
||||||
|
) {
|
||||||
|
this.notify.warning(
|
||||||
|
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
(error instanceof Error ? error.message : String(error)) ||
|
errorMessage || "Failed to import account.",
|
||||||
"Failed to import account.",
|
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the account to be imported already exists
|
||||||
|
*
|
||||||
|
* Derives the DID from the mnemonic and checks if it exists in the database
|
||||||
|
* @returns Promise<boolean> - True if account already exists, false otherwise
|
||||||
|
*/
|
||||||
|
private async checkForDuplicateAccount(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Derive the address and create the identifier to get the DID
|
||||||
|
const [address, privateHex, publicHex] = deriveAddress(
|
||||||
|
this.mnemonic.trim().toLowerCase(),
|
||||||
|
this.derivationPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newId = newIdentifier(
|
||||||
|
address,
|
||||||
|
publicHex,
|
||||||
|
privateHex,
|
||||||
|
this.derivationPath,
|
||||||
|
);
|
||||||
|
const didToCheck = newId.did;
|
||||||
|
|
||||||
|
// Check if an account with this DID already exists
|
||||||
|
const existingAccount = await this.$query(
|
||||||
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
|
[didToCheck],
|
||||||
|
);
|
||||||
|
|
||||||
|
return existingAccount?.values?.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't check for duplicates (e.g., invalid mnemonic),
|
||||||
|
// let the import process handle the error
|
||||||
|
this.$logError("Error checking for duplicate account: " + error);
|
||||||
|
// Return false to let the import process continue and handle the error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
|||||||
import {
|
import {
|
||||||
NOTIFY_ACCOUNT_DERIVATION_SUCCESS,
|
NOTIFY_ACCOUNT_DERIVATION_SUCCESS,
|
||||||
NOTIFY_ACCOUNT_DERIVATION_ERROR,
|
NOTIFY_ACCOUNT_DERIVATION_ERROR,
|
||||||
|
NOTIFY_DUPLICATE_DERIVED_ACCOUNT,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -171,6 +172,16 @@ export default class ImportAccountView extends Vue {
|
|||||||
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check for duplicate account before creating
|
||||||
|
const isDuplicate = await this.checkForDuplicateAccount(newId.did);
|
||||||
|
if (isDuplicate) {
|
||||||
|
this.notify.warning(
|
||||||
|
NOTIFY_DUPLICATE_DERIVED_ACCOUNT.message,
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await saveNewIdentity(newId, mne, newDerivPath);
|
await saveNewIdentity(newId, mne, newDerivPath);
|
||||||
|
|
||||||
// record that as the active DID
|
// record that as the active DID
|
||||||
@@ -192,5 +203,27 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.notify.error(NOTIFY_ACCOUNT_DERIVATION_ERROR.message, TIMEOUTS.LONG);
|
this.notify.error(NOTIFY_ACCOUNT_DERIVATION_ERROR.message, TIMEOUTS.LONG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the account to be created already exists
|
||||||
|
*
|
||||||
|
* @param did - The DID to check for duplicates
|
||||||
|
* @returns Promise<boolean> - True if account already exists, false otherwise
|
||||||
|
*/
|
||||||
|
private async checkForDuplicateAccount(did: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Check if an account with this DID already exists
|
||||||
|
const existingAccount = await this.$query(
|
||||||
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
|
[did],
|
||||||
|
);
|
||||||
|
|
||||||
|
return existingAccount?.values?.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't check for duplicates, let the save process handle the error
|
||||||
|
this.$logError("Error checking for duplicate account: " + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -226,7 +226,7 @@
|
|||||||
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||||
<!-- First, offers on the left-->
|
<!-- First, offers on the left-->
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<div v-if="activeDid && isRegistered">
|
<div v-if="activeDid && isRegistered" class="mb-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
data-testId="offerButton"
|
data-testId="offerButton"
|
||||||
@@ -243,13 +243,19 @@
|
|||||||
:project-name="name"
|
:project-name="name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
|
<h3 class="text-lg font-bold leading-tight mb-3">
|
||||||
|
Offered To This Idea
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div v-if="offersToThis.length === 0">
|
<div v-if="offersToThis.length === 0" class="text-sm">
|
||||||
(None yet. Wanna
|
(None yet.<span v-if="activeDid && isRegistered">
|
||||||
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
|
Wanna
|
||||||
>offer something... especially if others join you</span
|
<span
|
||||||
>?)
|
class="cursor-pointer text-blue-500"
|
||||||
|
@click="openOfferDialog()"
|
||||||
|
>offer something… especially if others join you</span
|
||||||
|
>?</span
|
||||||
|
>)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
@@ -314,7 +320,7 @@
|
|||||||
<!-- Now, gives TO this project in the middle -->
|
<!-- Now, gives TO this project in the middle -->
|
||||||
<!-- (similar to "FROM" gift display below) -->
|
<!-- (similar to "FROM" gift display below) -->
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-to">
|
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-to">
|
||||||
<div v-if="activeDid && isRegistered">
|
<div v-if="activeDid && isRegistered" class="mb-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 text-sm leading-tight rounded-md"
|
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 text-sm leading-tight rounded-md"
|
||||||
@@ -325,7 +331,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold mt-4">Given To This Project</h3>
|
<h3 class="text-lg font-bold leading-tight mb-3">
|
||||||
|
Given To This Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div v-if="givesToThis.length === 0" class="text-sm">
|
<div v-if="givesToThis.length === 0" class="text-sm">
|
||||||
(None yet. If you've seen something, say something by clicking a
|
(None yet. If you've seen something, say something by clicking a
|
||||||
@@ -476,7 +484,7 @@
|
|||||||
<!-- Finally, gives FROM this project on the right -->
|
<!-- Finally, gives FROM this project on the right -->
|
||||||
<!-- (similar to "TO" gift display above) -->
|
<!-- (similar to "TO" gift display above) -->
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-from">
|
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-from">
|
||||||
<div v-if="activeDid && isRegistered">
|
<div v-if="activeDid && isRegistered" class="mb-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 text-sm leading-tight rounded-md"
|
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 text-sm leading-tight rounded-md"
|
||||||
@@ -494,11 +502,13 @@
|
|||||||
:is-from-project-view="true"
|
:is-from-project-view="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold mb-3 mt-4">
|
<h3 class="text-lg font-bold leading-tight mb-3">
|
||||||
Benefitted From This Project
|
Benefitted From This Project
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="givesProvidedByThis.length === 0">(None yet.)</div>
|
<div v-if="givesProvidedByThis.length === 0" class="text-sm">
|
||||||
|
(None yet.)
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
|
|||||||
@@ -69,8 +69,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
import { createContactName, generateNewEthrUser, importUser, importUserFromAccount } from './testUtils';
|
||||||
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
|
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
|
||||||
|
|
||||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||||
// Load app homepage
|
// Load app homepage
|
||||||
@@ -170,36 +170,34 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
|
|||||||
await expect(page.locator('#apiServerInput')).toHaveValue(endorserServer);
|
await expect(page.locator('#apiServerInput')).toHaveValue(endorserServer);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check User 0 can register a random person', async ({ page }) => {
|
test('Check invalid DID shows error and redirects', async ({ page }) => {
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
const newDid = await generateAndRegisterEthrUser(page);
|
|
||||||
expect(newDid).toContain('did:ethr:');
|
// Navigate to an invalid DID URL
|
||||||
|
await page.goto('./did/0');
|
||||||
await page.goto('./');
|
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
// Should show error message about invalid DID format
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
await expect(page.getByText(NOTIFY_CONTACT_INVALID_DID.message)).toBeVisible();
|
||||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
|
||||||
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
// Should redirect to homepage
|
||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
await expect(page).toHaveURL(/.*\/$/);
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
});
|
||||||
// now ensure that alert goes away
|
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
test('Check User 0 can register a random person', async ({ page }) => {
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeHidden();
|
const newDid = await generateNewEthrUser(page); // generate a new user
|
||||||
|
|
||||||
// now delete the contact to test that pages still do reasonable things
|
await importUserFromAccount(page, "00"); // switch to User Zero
|
||||||
await deleteContact(page, newDid);
|
|
||||||
// go the activity page for this new person
|
// As User Zero, add the new user as a contact
|
||||||
await page.goto('./did/' + encodeURIComponent(newDid));
|
await page.goto('./contacts');
|
||||||
// maybe replace by: const popupPromise = page.waitForEvent('popup');
|
const contactName = createContactName(newDid);
|
||||||
let error;
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
|
||||||
try {
|
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
||||||
await page.waitForSelector('div[role="alert"]', { timeout: 2000 });
|
await page.locator('button > svg.fa-plus').click();
|
||||||
error = new Error('Error alert should not show.');
|
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
|
||||||
} catch (error) {
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
|
||||||
// success
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
} finally {
|
await page.locator('div[role="alert"] button:text-is("Yes")').click(); // Register new contact
|
||||||
if (error) {
|
await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt
|
||||||
throw error;
|
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
63
test-playwright/03-duplicate-import-test.spec.ts
Normal file
63
test-playwright/03-duplicate-import-test.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { importUserFromAccount, getTestUserData } from './testUtils';
|
||||||
|
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from '../src/constants/notifications';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test duplicate account import functionality
|
||||||
|
*
|
||||||
|
* This test verifies that:
|
||||||
|
* 1. A user can successfully import an account the first time
|
||||||
|
* 2. Attempting to import the same account again shows a warning message
|
||||||
|
* 3. The duplicate import is prevented
|
||||||
|
*/
|
||||||
|
test.describe('Duplicate Account Import', () => {
|
||||||
|
test('should prevent importing the same account twice', async ({ page }) => {
|
||||||
|
const userData = getTestUserData("00");
|
||||||
|
|
||||||
|
// First import - should succeed
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify first import was successful
|
||||||
|
await expect(page.getByRole("code")).toContainText(userData.did);
|
||||||
|
|
||||||
|
// Navigate back to start page for second import attempt
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify duplicate import shows warning message
|
||||||
|
// The warning can appear either from the pre-check or from the saveNewIdentity error handling
|
||||||
|
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).toBeVisible();
|
||||||
|
|
||||||
|
// Verify we're still on the import page (not redirected to account)
|
||||||
|
await expect(page.getByPlaceholder("Seed Phrase")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow importing different accounts', async ({ page }) => {
|
||||||
|
const userZeroData = getTestUserData("00");
|
||||||
|
const userOneData = getTestUserData("01");
|
||||||
|
|
||||||
|
// Import first user
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify first import was successful
|
||||||
|
await expect(page.getByRole("code")).toContainText(userZeroData.did);
|
||||||
|
|
||||||
|
// Navigate back to start page for second user import
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userOneData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify second import was successful (should not show duplicate warning)
|
||||||
|
await expect(page.getByRole("code")).toContainText(userOneData.did);
|
||||||
|
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,10 +23,11 @@ test('New offers for another user', async ({ page }) => {
|
|||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');
|
||||||
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
|
||||||
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible();
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
|
||||||
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
|
await page.locator('div[role="alert"] button:text-is("No")').click(); // Dismiss register prompt
|
||||||
|
await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt
|
||||||
|
|
||||||
// show buttons to make offers directly to people
|
// show buttons to make offers directly to people
|
||||||
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
|
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
|
|||||||
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContactName(did: string): string {
|
export function createContactName(did: string): string {
|
||||||
return "User " + did.slice(11, 14);
|
return "User " + did.slice(11, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,30 +144,6 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
|
|||||||
return newDid;
|
return newDid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random user and register them.
|
|
||||||
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
|
||||||
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
|
||||||
const newDid = await generateNewEthrUser(page);
|
|
||||||
|
|
||||||
await importUser(page, "000"); // switch to user 000
|
|
||||||
|
|
||||||
await page.goto("./contacts");
|
|
||||||
const contactName = createContactName(newDid);
|
|
||||||
await page
|
|
||||||
.getByPlaceholder("URL or DID, Name, Public Key")
|
|
||||||
.fill(`${newDid}, ${contactName}`);
|
|
||||||
await page.locator("button > svg.fa-plus").click();
|
|
||||||
// register them
|
|
||||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
|
||||||
// wait for it to disappear because the next steps may depend on alerts being gone
|
|
||||||
await expect(
|
|
||||||
page.locator('div[role="alert"] button:has-text("Yes")')
|
|
||||||
).toBeHidden();
|
|
||||||
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
|
||||||
|
|
||||||
return newDid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a random string of specified length
|
// Function to generate a random string of specified length
|
||||||
export async function generateRandomString(length: number): Promise<string> {
|
export async function generateRandomString(length: number): Promise<string> {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
|
|||||||
Reference in New Issue
Block a user