Compare commits

..

5 Commits

Author SHA1 Message Date
Matthew Raymer
4391cb2881 feat(harbor-pilot): add Playwright test investigation directive
- Create comprehensive MDC rule for systematic Playwright test failure investigation
- Integrate rule into harbor_pilot_universal.mdc for team-wide access
- Include investigation workflow, diagnostic commands, and evidence-based analysis
- Document specific failure patterns (alert stacking, selector conflicts, timing issues)
- Provide practical examples from recent test failure investigation
- Add investigation commands for error context, trace files, and page snapshots

This rule transforms ad-hoc test debugging into systematic investigation process,
leveraging Playwright's built-in debugging tools for faster root cause identification.
2025-08-21 06:12:25 +00:00
0b9c243969 Merge branch 'master' into playwright-test-60-fix 2025-08-21 01:57:33 -04:00
Matthew Raymer
6afe1c4c13 feat(harbor-pilot): add historical comment management and no time estimates rules
Add two new Harbor Pilot directives to improve code quality and planning:

1. Historical Comment Management: Guidelines for transforming or removing
   obsolete comments into actionable architectural guidance
2. No Time Estimates: Rule prohibiting time estimates in favor of
   phase-based planning with complexity levels and milestones

Both rules are integrated into main Harbor Pilot directive for automatic
application across all operations.
2025-08-21 05:42:01 +00:00
Jose Olarte III
f31eb5f6c9 Merge branch 'master' into playwright-test-60-fix 2025-08-19 18:48:08 +08:00
Jose Olarte III
9f976f011a Fix: account for new Export Data dialog
- Stricter targeting of buttons since Register and Export Data dialogs appear on screen at the same time
- Locate success notification first since it appears first (and cannot be "clicked" through the overlapping dialog-overlay)
2025-08-19 18:43:33 +08:00
9 changed files with 564 additions and 763 deletions

View File

@@ -1,321 +0,0 @@
---
alwaysApply: true
version: "2.0.0"
lastUpdated: "2025-08-15"
priority: "critical"
---
# Component Creation Ideals — TimeSafari Architecture (Directive MDC, v2)
> **Agent role**: Apply these rules when creating, refactoring, or reviewing Vue components (Vue 3 with **vue-facing-decorator**). Prioritize **self-contained** components that hydrate from and persist to **PlatformServiceMixin** settings. Minimize parentchild coupling and prop drilling. Prefer **concision** where a separate component would add more API surface than value.
## 📚 Cross-References
- **Migration Example**: See `src/components/NotificationSection.vue` for successful refactor implementation
- **Parent Component**: See `src/views/AccountViewView.vue` for before/after comparison
- **Settings Infrastructure**: See `src/utils/PlatformServiceMixin.ts` for available methods
- **Related Rules**: See `.cursor/rules/` for other architectural guidelines
## Golden Rules (Enforce)
### Priority Levels
- 🔴 **CRITICAL**: Must be followed - breaking these creates architectural problems
- 🟡 **HIGH**: Strongly recommended - important for maintainability
- 🟢 **MEDIUM**: Good practice - improves code quality
1. **Self-Contained Components** 🔴 **CRITICAL**
- Do **not** require parent props for internal state or behavior.
- Hydrate on `mounted()` via `this.$accountSettings()`; persist with `this.$saveSettings()`.
- Encapsulate business logic inside the component; avoid delegating core logic to the parent.
- Prefer **computed** getters for derived values; avoid stored duplicates of derived state.
2. **Settings-First Architecture** 🔴 **CRITICAL**
- Use `PlatformServiceMixin` for reading/writing settings. Do **not** introduce new state managers (Pinia, custom stores) unless the state is *truly global*.
- Prefer **fetch-on-mount** over passing values via props.
3. **Single Responsibility** 🟡 **HIGH**
- Each component owns **one clear purpose** (UI + related logic + settings persistence). Avoid splitting UI/logic across multiple components unless reusability clearly benefits.
4. **Internal State Lifecycle** 🟡 **HIGH**
- Pattern: **defaults → hydrate on mount → computed for derived → persist on change**.
- Handle hydration errors gracefully (keep safe defaults; surface actionable UI states as needed).
5. **Minimal Props** 🔴 **CRITICAL**
- Props are for **pure configuration** (labels, limits, feature flags). Do not pass data that can be loaded internally.
- **Never** pass props that mirror settings values (e.g., `isRegistered`, `notifying*`). Load those from settings.
6. **Communication & Events** 🟡 **HIGH**
- Children may emit events for *user interactions* (e.g., `submitted`, `closed`) but **not** to offload core logic to the parent.
- Do not emit events solely to persist settings; the child handles persistence.
7. **Testing** 🟢 **MEDIUM**
- Unit tests mount components in isolation; mock `this.$accountSettings`/`this.$saveSettings`.
- Verify hydration on mount, persistence on mutation, and graceful failure on settings errors.
---
## Concision-First Decision Framework
> **Goal:** Avoid unnecessary components when a concise script, composable, or helper suffices.
**Prefer NOT making a component when:**
- **One-off UI**: used in exactly one view, unlikely to repeat.
- **Small scope**: ~≤100150 LOC, ≤3 reactive fields, ≤2 handlers.
- **Purely presentational** with trivial logic.
- **Local invariants**: behavior depends entirely on the views context.
- **Abstraction cost > benefit**: would create an anemic component with props mirroring parent state.
- **Better fit as code reuse**: logic works as a **composable/service/helper** without introducing new UI.
**Concise alternatives:**
- **Composable**: `useFeature()` encapsulates settings I/O and state.
- **Service/Module**: plain TS helpers for formatting/validation.
- **Directive**: tiny DOM behaviors that dont need a lifecycle boundary.
**When to make a component (even without reuse yet):**
- **Isolation boundary**: async side effects, permission prompts, or recoverable error states.
- **Stateful widget**: internal settings persistence, media controls, complex a11y.
- **Slots/composition**: needs flexible children or layout.
- **Different change rate**: sub-tree churns independently of the parent.
- **Testability/ownership**: clear, ownable surface thats easier to unit-test in isolation.
**Rule of Three (guardrail):**
- 1st time: inline or composable.
- 2nd time: consider shared abstraction.
- 3rd time: extract a component.
**PR language (use when choosing concision):**
- “One-off, ~80 LOC, no expected reuse. A component would add an API surface with no consumer. Keeping it local reduces cognitive load. If we see a second usage, promote to a composable; third usage, a component.”
- “Logic lives in `useX()` to keep the view concise without prop plumbing; settings stay via `PlatformServiceMixin`.”
---
## Anti-Patterns (Reject)
- Props that duplicate internal/derivable state: `:is-registered`, `:notifying-*`, etc.
- Child components that are **UI-only** while parents hold the business logic for that feature.
- Introducing Pinia/custom stores for per-component state.
- Emitting `update:*` events to push settings responsibilities to parents.
- Scattering a feature across multiple micro-components without a clear reuse reason.
- Using props for computed/derived values (e.g., `showAdvancedFeatures` as a prop).
---
## Quick Examples
### ✅ DO (Self-Contained, Settings-First)
```vue
<!-- Parent renders with no props -->
<NotificationSection />
```
```ts
// NotificationSection.vue (vue-facing-decorator + PlatformServiceMixin)
import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({
mixins: [PlatformServiceMixin],
})
export default class NotificationSection extends Vue {
private notifyingNewActivity: boolean = false;
async mounted(): Promise<void> {
await this.hydrateFromSettings();
}
private async hydrateFromSettings(): Promise<void> {
try {
const s = await this.$accountSettings();
this.notifyingNewActivity = !!s.notifyingNewActivityTime;
} catch (err) {
// Keep defaults; optionally surface a non-blocking UI notice
}
}
private async updateNotifying(state: boolean): Promise<void> {
await this.$saveSettings({ notifyingNewActivityTime: state ? Date.now() : null });
this.notifyingNewActivity = state;
}
}
```
### ❌ DON'T (Prop-Driven Coupling)
```vue
<!-- Parent passes internal state down -->
<NotificationSection
:is-registered="isRegistered"
:notifying-new-activity="notifyingNewActivity"
/>
```
```ts
// Child receives props (anti-pattern)
export default class NotificationSection extends Vue {
isRegistered: boolean = false;
notifyingNewActivity: boolean = false;
}
```
---
## Component Structure Template (Drop-In)
```ts
/**
* ComponentName.vue — Purpose
* Owns: UI + logic + settings persistence (PlatformServiceMixin).
*/
import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import type { Router } from "vue-router";
@Component({
components: {
// child components here
},
mixins: [PlatformServiceMixin], // Settings access
})
export default class ComponentName extends Vue {
// Internal state
private myState: boolean = false;
private myData: string = "";
// Derived
private get isEnabled(): boolean {
return this.myState && this.hasPermissions;
}
async mounted(): Promise<void> {
await this.hydrateFromSettings();
}
private async hydrateFromSettings(): Promise<void> {
try {
const s = await this.$accountSettings();
this.myState = !!s.mySetting;
this.myData = s.myData ?? "";
} catch (err) {
// keep defaults
}
}
private async updateState(v: boolean): Promise<void> {
await this.$saveSettings({ mySetting: v });
this.myState = v;
}
async handleUserAction(): Promise<void> {
await this.updateState(true);
}
}
```
---
## Migration Playbook (Props → Self-Contained)
> **Agent, when you see a component receiving `@Prop()` values that mirror settings or internal state, apply this playbook.**
1. **Detect Anti-Props**
- Flag props matching: `is*`, `has*`, `notifying*`, or mirroring settings keys.
- Search for parent usage of these props and events.
2. **Inline State**
- Remove anti-props and `@Prop()` declarations.
- Add private fields for state inside the child.
- Add `mounted()` hydration and persistence helpers via `PlatformServiceMixin`.
3. **Parent Simplification**
- Replace `<Child :foo="..." :bar="..." @update="..."/>` with `<Child />`.
- Delete now-unused local state, watchers, and computed values that existed only to feed the child.
4. **Events**
- Keep only user-interaction events (e.g., `submitted`). Remove persistence/logic events that the child now owns.
5. **Tests**
- Update unit tests to mount child in isolation; mock settings I/O.
- Verify: hydrate on mount, persist on change, safe fallback on error.
---
## When Exceptions Are Acceptable
- **Truly Global State** across distant components → use Pinia/service *sparingly*.
- **Reusable Form Inputs** → accept `value` prop and emit `input`/`update:modelValue`; keep validation/business logic internal.
- **Configuration-Only Props** for labels, visual variants, or limits — not for state that can be fetched.
---
## Definition of Done (Checklist)
- [ ] No props required for internal state or settings-backed values.
- [ ] Uses `PlatformServiceMixin` for all settings I/O.
- [ ] Hydrates on `mounted()`, persists on mutations.
- [ ] Single-responsibility: UI + logic + persistence together.
- [ ] Computed getters for derived state.
- [ ] Unit tests mock settings and cover hydrate/persist/failure paths.
- [ ] Parent components contain no leftover `notifying-*` or similar prop wiring.
---
## Testing Snippets
```ts
// Hydration on mount
test("hydrates from settings on mount", async () => {
const wrap = mount(MyComponent);
await wrap.vm.$nextTick();
expect((wrap.vm as any).myState).toBe(true);
});
```
```ts
// Mock settings methods
jest.spyOn(wrapper.vm as any, "$accountSettings").mockResolvedValue({ mySetting: true });
jest.spyOn(wrapper.vm as any, "$saveSettings").mockResolvedValue(void 0);
```
```ts
// Graceful failure
test("handles settings load failure", async () => {
jest.spyOn(wrapper.vm as any, "$accountSettings").mockRejectedValue(new Error("DB Error"));
const wrap = mount(MyComponent);
await wrap.vm.$nextTick();
expect((wrap.vm as any).myState).toBe(false); // default
});
```
---
## Rationale (Short)
- **Concision first where appropriate** → avoid unnecessary components and API surfaces.
- **Reduced coupling** → portability and reuse when boundaries are justified.
- **Maintainability** → changes localized to the owning component.
- **Consistency** → one canonical path for settings-backed features.
**Rule of thumb:** _Can this feature operate independently, and does a component materially improve isolation or testability?_ If not, keep it concise (inline or composable).
---
## 📝 Version History
### v2.0.0 (2025-08-15)
- **Major Enhancement**: Added Concision-First Decision Framework
- **New**: Rule of Three guardrail for component extraction
- **New**: PR language guidance for code reviews
- **Enhanced**: Agent role includes concision preference
- **Refined**: Anti-patterns and examples updated
### v1.0.0 (2025-08-15)
- Initial creation based on successful NotificationSection refactor
- Established core architectural principles for self-contained components
- Added comprehensive migration playbook and testing guidelines
- Included practical examples and anti-pattern detection
### Future Enhancements
- Additional migration patterns for complex components
- Integration with automated refactoring tools
- Performance benchmarking guidelines
- Advanced testing strategies for complex state management

View File

@@ -204,3 +204,4 @@ Follow this exact order **after** the Base Contracts **Objective → Result
- 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`)

View 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

View File

@@ -1,194 +0,0 @@
<template>
<section
v-if="isRegistered"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="notificationsHeading"
>
<h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
<div class="flex items-center justify-between">
<div>
Reminder Notification
<button
class="text-slate-400 fa-fw cursor-pointer"
aria-label="Learn more about reminder notifications"
@click.stop="showReminderNotificationInfo"
>
<font-awesome-icon icon="question-circle" aria-hidden="true" />
</button>
</div>
<div
class="relative ml-2 cursor-pointer"
role="switch"
:aria-checked="notifyingReminder"
aria-label="Toggle reminder notifications"
tabindex="0"
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingReminder" class="w-full flex justify-between">
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
<span>{{ notifyingReminderTime.replace(" ", "&nbsp;") }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<!-- label -->
<div>
New Activity Notification
<font-awesome-icon
icon="question-circle"
class="text-slate-400 fa-fw cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
/>
</div>
<!-- toggle -->
<div
class="relative ml-2 cursor-pointer"
@click="showNewActivityNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingNewActivity" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingNewActivityTime" class="w-full text-right">
{{ notifyingNewActivityTime.replace(" ", "&nbsp;") }}
</div>
<div class="mt-2 text-center">
<router-link class="text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notifications&hellip;
</router-link>
</div>
</section>
<PushNotificationPermission ref="pushNotificationPermission" />
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import PushNotificationPermission from "./PushNotificationPermission.vue";
import { createNotifyHelpers } from "@/utils/notify";
import { NotificationIface } from "@/constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import {
createNotificationSettingsService,
type NotificationSettingsService,
} from "@/composables/useNotificationSettings";
/**
* NotificationSection.vue - Notification UI component with service-based logic
*
* This component handles the UI presentation of notification functionality:
* - Reminder notifications with custom messages
* - New activity notifications
* - Notification permission management
* - Help and troubleshooting links
*
* Business logic is delegated to NotificationSettingsService for:
* - Settings hydration and persistence
* - Registration status checking
* - Notification permission management
*
* The component maintains its lifecycle boundary while keeping logic separate.
*
* @author Matthew Raymer
* @component NotificationSection
* @vue-facing-decorator
*/
@Component({
components: {
FontAwesomeIcon,
PushNotificationPermission,
},
mixins: [PlatformServiceMixin],
})
export default class NotificationSection extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
// Notification settings service - handles all business logic
private notificationService!: NotificationSettingsService;
private notify!: ReturnType<typeof createNotifyHelpers>;
created() {
this.notify = createNotifyHelpers(this.$notify);
this.notificationService = createNotificationSettingsService(
this,
this.notify,
);
}
/**
* Computed property to determine if user is registered
* Reads from the notification service
*/
private get isRegistered(): boolean {
return this.notificationService.isRegistered;
}
/**
* Initialize component state from persisted settings
* Called when component is mounted
*/
async mounted(): Promise<void> {
await this.notificationService.hydrateFromSettings();
}
// Computed properties for template access
private get notifyingNewActivity(): boolean {
return this.notificationService.notifyingNewActivity;
}
private get notifyingNewActivityTime(): string {
return this.notificationService.notifyingNewActivityTime;
}
private get notifyingReminder(): boolean {
return this.notificationService.notifyingReminder;
}
private get notifyingReminderMessage(): string {
return this.notificationService.notifyingReminderMessage;
}
private get notifyingReminderTime(): string {
return this.notificationService.notifyingReminderTime;
}
async showNewActivityNotificationInfo(): Promise<void> {
await this.notificationService.showNewActivityNotificationInfo(
this.$router,
);
}
async showNewActivityNotificationChoice(): Promise<void> {
await this.notificationService.showNewActivityNotificationChoice(
this.$refs.pushNotificationPermission as PushNotificationPermission,
);
}
async showReminderNotificationInfo(): Promise<void> {
await this.notificationService.showReminderNotificationInfo(this.$router);
}
async showReminderNotificationChoice(): Promise<void> {
await this.notificationService.showReminderNotificationChoice(
this.$refs.pushNotificationPermission as PushNotificationPermission,
);
}
}
</script>

View File

@@ -1,233 +0,0 @@
/**
* useNotificationSettings.ts - Notification settings and permissions service
*
* This service handles all notification-related business logic including:
* - Settings hydration and persistence
* - Registration status checking
* - Notification permission management
* - Settings state management
*
* Separates business logic from UI components while maintaining
* the lifecycle boundary and settings access patterns.
*
* @author Matthew Raymer
* @service NotificationSettingsService
*/
import { createNotifyHelpers } from "@/utils/notify";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { DAILY_CHECK_TITLE, DIRECT_PUSH_TITLE } from "@/libs/util";
import type { ComponentPublicInstance } from "vue";
/**
* Interface for notification settings state
*/
export interface NotificationSettingsState {
isRegistered: boolean;
notifyingNewActivity: boolean;
notifyingNewActivityTime: string;
notifyingReminder: boolean;
notifyingReminderMessage: string;
notifyingReminderTime: string;
}
/**
* Interface for notification settings actions
*/
export interface NotificationSettingsActions {
hydrateFromSettings: () => Promise<void>;
updateNewActivityNotification: (
enabled: boolean,
timeText?: string,
) => Promise<void>;
updateReminderNotification: (
enabled: boolean,
timeText?: string,
message?: string,
) => Promise<void>;
showNewActivityNotificationInfo: (router: any) => Promise<void>;
showReminderNotificationInfo: (router: any) => Promise<void>;
showNewActivityNotificationChoice: (
pushNotificationPermissionRef: any,
) => Promise<void>;
showReminderNotificationChoice: (
pushNotificationPermissionRef: any,
) => Promise<void>;
}
/**
* Service class for managing notification settings and permissions
*
* @param platformService - PlatformServiceMixin instance for settings access
* @param notify - Notification helper functions
*/
export class NotificationSettingsService
implements NotificationSettingsState, NotificationSettingsActions
{
// State properties
public isRegistered: boolean = false;
public notifyingNewActivity: boolean = false;
public notifyingNewActivityTime: string = "";
public notifyingReminder: boolean = false;
public notifyingReminderMessage: string = "";
public notifyingReminderTime: string = "";
constructor(
private platformService: ComponentPublicInstance & {
$accountSettings: () => Promise<any>;
$saveSettings: (changes: any) => Promise<boolean>;
},
private notify: ReturnType<typeof createNotifyHelpers>,
) {}
/**
* Load notification settings from database and hydrate internal state
* Uses the existing settings mechanism for consistency
*/
public async hydrateFromSettings(): Promise<void> {
try {
const settings = await this.platformService.$accountSettings();
// Hydrate registration status
this.isRegistered = !!settings?.isRegistered;
// Hydrate boolean flags from time presence
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime;
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
this.notifyingReminderTime = settings.notifyingReminderTime || "";
} catch (error) {
console.error("Failed to hydrate notification settings:", error);
// Keep default values on error
}
}
/**
* Update new activity notification settings
*/
public async updateNewActivityNotification(
enabled: boolean,
timeText: string = "",
): Promise<void> {
await this.platformService.$saveSettings({
notifyingNewActivityTime: timeText,
});
this.notifyingNewActivity = enabled;
this.notifyingNewActivityTime = timeText;
}
/**
* Update reminder notification settings
*/
public async updateReminderNotification(
enabled: boolean,
timeText: string = "",
message: string = "",
): Promise<void> {
await this.platformService.$saveSettings({
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
this.notifyingReminder = enabled;
this.notifyingReminderMessage = message;
this.notifyingReminderTime = timeText;
}
/**
* Show new activity notification info dialog
*/
public async showNewActivityNotificationInfo(router: any): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
async () => {
await router.push({
name: "help-notification-types",
});
},
);
}
/**
* Show reminder notification info dialog
*/
public async showReminderNotificationInfo(router: any): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
async () => {
await router.push({
name: "help-notification-types",
});
},
);
}
/**
* Handle new activity notification choice (enable/disable)
*/
public async showNewActivityNotificationChoice(
pushNotificationPermissionRef: any,
): Promise<void> {
if (!this.notifyingNewActivity) {
// Enable notification
pushNotificationPermissionRef.open(
DAILY_CHECK_TITLE,
async (success: boolean, timeText: string) => {
if (success) {
await this.updateNewActivityNotification(true, timeText);
}
},
);
} else {
// Disable notification
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
if (success) {
await this.updateNewActivityNotification(false);
}
});
}
}
/**
* Handle reminder notification choice (enable/disable)
*/
public async showReminderNotificationChoice(
pushNotificationPermissionRef: any,
): Promise<void> {
if (!this.notifyingReminder) {
// Enable notification
pushNotificationPermissionRef.open(
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await this.updateReminderNotification(true, timeText, message);
}
},
);
} else {
// Disable notification
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
if (success) {
await this.updateReminderNotification(false);
}
});
}
}
}
/**
* Factory function to create a NotificationSettingsService instance
*
* @param platformService - PlatformServiceMixin instance
* @param notify - Notification helper functions
* @returns NotificationSettingsService instance
*/
export function createNotificationSettingsService(
platformService: ComponentPublicInstance & {
$accountSettings: () => Promise<any>;
$saveSettings: (changes: any) => Promise<boolean>;
},
notify: ReturnType<typeof createNotifyHelpers>,
): NotificationSettingsService {
return new NotificationSettingsService(platformService, notify);
}

View File

@@ -60,7 +60,91 @@
@share-info="onShareInfo"
/>
<NotificationSection />
<!-- Notifications -->
<!-- Currently disabled because it doesn't work, even on Chrome.
If restored, make sure it works or doesn't show on mobile/electron. -->
<section
v-if="false"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="notificationsHeading"
>
<h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
<div class="flex items-center justify-between">
<div>
Reminder Notification
<button
class="text-slate-400 fa-fw cursor-pointer"
aria-label="Learn more about reminder notifications"
@click.stop="showReminderNotificationInfo"
>
<font-awesome
icon="question-circle"
aria-hidden="true"
></font-awesome>
</button>
</div>
<div
class="relative ml-2 cursor-pointer"
role="switch"
:aria-checked="notifyingReminder"
aria-label="Toggle reminder notifications"
tabindex="0"
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingReminder" class="w-full flex justify-between">
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
<span>{{ notifyingReminderTime.replace(" ", "&nbsp;") }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<!-- label -->
<div>
New Activity Notification
<font-awesome
icon="question-circle"
class="text-slate-400 fa-fw cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
/>
</div>
<!-- toggle -->
<div
class="relative ml-2 cursor-pointer"
@click="showNewActivityNotificationChoice()"
>
<!-- input -->
<input
v-model="notifyingNewActivity"
type="checkbox"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingNewActivityTime" class="w-full text-right">
{{ notifyingNewActivityTime.replace(" ", "&nbsp;") }}
</div>
<div class="mt-2 text-center">
<router-link class="text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notifications&hellip;
</router-link>
</div>
</section>
<PushNotificationPermission ref="pushNotificationPermission" />
<LocationSearchSection :search-box="searchBox" />
@@ -681,13 +765,12 @@ import { Capacitor } from "@capacitor/core";
import EntityIcon from "../components/EntityIcon.vue";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import DataExportSection from "../components/DataExportSection.vue";
import IdentitySection from "@/components/IdentitySection.vue";
import NotificationSection from "@/components/NotificationSection.vue";
import RegistrationNotice from "@/components/RegistrationNotice.vue";
import LocationSearchSection from "@/components/LocationSearchSection.vue";
import UsageLimitsSection from "@/components/UsageLimitsSection.vue";
@@ -713,7 +796,11 @@ import {
getHeaders,
tokenExpiryTimeDescription,
} from "../libs/endorserServer";
import { retrieveAccountMetadata } from "../libs/util";
import {
DAILY_CHECK_TITLE,
DIRECT_PUSH_TITLE,
retrieveAccountMetadata,
} from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
@@ -742,7 +829,7 @@ interface UserNameDialogRef {
LMap,
LMarker,
LTileLayer,
NotificationSection,
PushNotificationPermission,
QuickNav,
TopMessage,
UserNameDialog,
@@ -793,7 +880,12 @@ export default class AccountViewView extends Vue {
includeUserProfileLocation: boolean = false;
savingProfile: boolean = false;
// Push notification subscription (kept for service worker checks)
// Notification properties
notifyingNewActivity: boolean = false;
notifyingNewActivityTime: string = "";
notifyingReminder: boolean = false;
notifyingReminderMessage: string = "";
notifyingReminderTime: string = "";
subscription: PushSubscription | null = null;
// UI state properties
@@ -911,8 +1003,10 @@ export default class AccountViewView extends Vue {
const registration = await navigator.serviceWorker?.ready;
this.subscription = await registration.pushManager.getSubscription();
if (!this.subscription) {
// Notification settings cleanup is now handled by the NotificationSection component
// which manages its own state lifecycle and persistence
if (this.notifyingNewActivity || this.notifyingReminder) {
// the app thought there was a subscription but there isn't, so fix the settings
this.turnOffNotifyingFlags();
}
}
} catch (error) {
this.notify.warning(
@@ -952,6 +1046,11 @@ export default class AccountViewView extends Vue {
this.isRegistered = !!settings?.isRegistered;
this.isSearchAreasSet = !!settings.searchBoxes;
this.searchBox = settings.searchBoxes?.[0] || null;
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime;
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
this.notifyingReminderTime = settings.notifyingReminderTime || "";
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
this.partnerApiServerInput =
settings.partnerApiServer || this.partnerApiServerInput;
@@ -1033,6 +1132,87 @@ export default class AccountViewView extends Vue {
}
}
async showNewActivityNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
);
}
async showNewActivityNotificationChoice(): Promise<void> {
if (!this.notifyingNewActivity) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: timeText,
});
this.notifyingNewActivity = true;
this.notifyingNewActivityTime = timeText;
}
});
} else {
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
});
}
}
async showReminderNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
);
}
async showReminderNotificationChoice(): Promise<void> {
if (!this.notifyingReminder) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
this.notifyingReminder = true;
this.notifyingReminderMessage = message || "";
this.notifyingReminderTime = timeText;
}
},
);
} else {
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
});
}
}
public async toggleHideRegisterPromptOnNewContact(): Promise<void> {
const newSetting = !this.hideRegisterPromptOnNewContact;
await this.$saveSettings({
@@ -1049,8 +1229,19 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
// Note: Notification state management has been migrated to NotificationSection component
// which handles its own lifecycle and persistence via PlatformServiceMixin
public async turnOffNotifyingFlags(): Promise<void> {
// should tell the push server as well
await this.$saveSettings({
notifyingNewActivityTime: "",
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
/**
* Asynchronously exports the database into a downloadable JSON file.

View File

@@ -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 expect(page.locator('button > svg.fa-plus')).toBeVisible();
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();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
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
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();

View File

@@ -158,10 +158,10 @@ export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
.fill(`${newDid}, ${contactName}`);
await page.locator("button > svg.fa-plus").click();
// register them
await page.locator('div[role="alert"] button:has-text("Yes")').click();
await page.locator('div[role="alert"] button:text-is("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")')
page.locator('div[role="alert"] button:text-is("Yes")')
).toBeHidden();
await expect(page.locator("li", { hasText: contactName })).toBeVisible();