Compare commits
5 Commits
notificati
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4391cb2881 | ||
| 0b9c243969 | |||
|
|
6afe1c4c13 | ||
|
|
f31eb5f6c9 | ||
|
|
9f976f011a |
@@ -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 parent–child 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**: ~≤100–150 LOC, ≤3 reactive fields, ≤2 handlers.
|
||||
- **Purely presentational** with trivial logic.
|
||||
- **Local invariants**: behavior depends entirely on the view’s 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 don’t 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 that’s 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
|
||||
@@ -204,3 +204,4 @@ Follow this exact order **after** the Base Contract’s **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`)
|
||||
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
|
||||
@@ -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(" ", " ") }}</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(" ", " ") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
</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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(" ", " ") }}</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(" ", " ") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
</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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user