Skip to content

Commit 55c83d3

Browse files
author
Nick Campanini
committed
Merge branch 'main' into feat/DC-153-request-replacement-card-flow
# Conflicts: # src/SEBT.Portal.Infrastructure/Repositories/MockHouseholdRepository.cs # src/SEBT.Portal.Web/src/features/household/api/schema.test.ts
2 parents d47db49 + 5345856 commit 55c83d3

File tree

39 files changed

+739
-110
lines changed

39 files changed

+739
-110
lines changed

.claude/skills/qa/SKILL.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
name: qa
3+
description: "Generate QA test summary from current branch changes for PR handoff"
4+
allowed-tools: Read, Grep, Glob, Bash, Write
5+
---
6+
7+
# /qa - QA Test Summary Generator
8+
9+
Generate a QA handoff document from the current branch's changes. Output should be ready to paste as a **Jira comment** on the ticket.
10+
11+
**Write two versions of the output:**
12+
1. **`.claude/qa-summary.md`** — Markdown version with proper headings, bold, checkboxes, and code formatting. Useful for GitHub PRs or reference.
13+
2. **`.claude/qa-summary.txt`** — Plain text version following the Jira formatting rules below. This is the one that gets copied to clipboard.
14+
15+
After writing both files, run `cat .claude/qa-summary.txt | pbcopy` to copy the Jira version to the clipboard. Tell the user: "Copied to clipboard — paste into Jira with Cmd+V. Markdown version also saved to `.claude/qa-summary.md`."
16+
17+
## Instructions
18+
19+
1. **Discover changes**: Diff against the branch's parent, not production. Run `git diff development --stat` and `git log development..HEAD --oneline` to see only what this branch adds. Also run `git diff HEAD --stat` to catch any uncommitted work.
20+
2. **Read changed files**: Read the key modified files to understand the actual changes, not just the diff stats
21+
3. **Identify scope**: Determine which features, pages, and components are affected
22+
4. **Generate test summary in plain text with light formatting.** The output will be pasted into Jira Cloud's rich text editor. Use this format:
23+
24+
```
25+
QA Summary — [Branch Name]
26+
27+
What Changed
28+
Brief 2-3 sentence description of the feature/fix.
29+
30+
Affected Areas
31+
- All pages — page views and browser tab titles
32+
- Sign in / Sign out flow
33+
34+
Test Cases
35+
36+
Happy Path
37+
1. Step-by-step test case with expected result
38+
2. Another test case...
39+
40+
Edge Cases
41+
1. Edge case scenario with expected behavior
42+
43+
Regression Checks
44+
1. Verify [related feature] still works as expected
45+
46+
Environment Notes
47+
- Environment setup needed
48+
- Which environments to test on
49+
```
50+
51+
### Formatting rules — CRITICAL
52+
- Use plain text with simple numbered and bulleted lists
53+
- NO markdown syntax (no ##, no **, no backticks, no [ ] checkboxes)
54+
- NO Jira wiki markup (no h2., no *, no {{}}, no #)
55+
- Section headers should be plain text on their own line, in ALL CAPS or Title Case to stand out
56+
- Use dashes (-) for bullet lists and numbers (1. 2. 3.) for ordered lists
57+
- For inline code references like event names, just use quotes: "page_view", "sign_in"
58+
- For links, write the full URL on its own line
59+
60+
## Key Behaviors
61+
- Focus on **user-facing behavior**, not implementation details
62+
- Test cases should be actionable by someone who didn't write the code
63+
- Use relative paths for navigation (e.g. "Navigate to Tools > Tag Generator"), never reference localhost or full URLs — QA knows their environment
64+
- Assume QA is already signed in and on the app unless the test specifically involves auth
65+
- Call out any data dependencies (specific campaigns, creatives, etc.)
66+
- If changes touch auth, API layer, or shared components, expand regression scope
67+
- Keep it concise — QA doesn't need to know about refactors or code style changes
68+
- Use checkboxes so QA can track completion
69+
70+
## Language Rules — CRITICAL
71+
- **NEVER reference file names, paths, components, stores, functions, or code concepts.** QA does not read code.
72+
- Write everything in terms of **what the user sees and does in the browser** — pages, buttons, modals, fields, messages.
73+
- Instead of "interceptors.js tracks CRUD operations", say "Creating, editing, or deleting any item should be tracked"
74+
- Instead of "TableFilter.vue search is debounced", say "Type in a search bar, wait a second, and the search should be recorded"
75+
- Instead of "Affected: src/features/analytics/...", say "Affected: Analytics reports page"
76+
- The "Affected Areas" section should list **page names and UI areas**, not files or components
77+
- Environment Notes should only include things QA needs to configure or be aware of — no technical implementation details

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ FodyWeavers.xsd
444444
# AI
445445
CLAUDE.local.md
446446
.claude/settings.local.json
447+
.claude/qa-summary.*
447448

448449
# Developer-local workflow artifacts (branch context, PR reviews, screenshots)
449450
docs/.local/
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { act, render } from '@testing-library/react'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { DataLayer } from './data-layer'
5+
import { DataLayerProvider } from './DataLayerProvider'
6+
import type { RoutePageContextMap } from './DataLayerProvider'
7+
8+
let mockPathname = '/dashboard'
9+
vi.mock('next/navigation', () => ({
10+
usePathname: () => mockPathname
11+
}))
12+
13+
// jsdom does not implement requestAnimationFrame; stub it to flush synchronously
14+
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
15+
cb(0)
16+
return 0
17+
})
18+
vi.stubGlobal('cancelAnimationFrame', vi.fn())
19+
20+
const routes: RoutePageContextMap = {
21+
'/dashboard': { name: 'Dashboard', flow: 'dashboard', step: 'dashboard' },
22+
'/login': { name: 'Login', flow: 'auth', step: 'login' }
23+
}
24+
25+
describe('PageTracker (via DataLayerProvider)', () => {
26+
beforeEach(() => {
27+
delete (window as unknown as Record<string, unknown>).digitalData
28+
mockPathname = '/dashboard'
29+
})
30+
31+
it('sets correct context for a matched route', () => {
32+
new DataLayer('digitalData')
33+
34+
render(
35+
<DataLayerProvider application="test" routes={routes}>
36+
<div />
37+
</DataLayerProvider>
38+
)
39+
40+
expect(window.digitalData!.get('page.name')).toBe('Dashboard')
41+
expect(window.digitalData!.get('page.flow')).toBe('dashboard')
42+
expect(window.digitalData!.get('page.step')).toBe('dashboard')
43+
})
44+
45+
it('falls back to document.title for an unmatched route', () => {
46+
mockPathname = '/unknown'
47+
document.title = 'Fallback Title'
48+
new DataLayer('digitalData')
49+
50+
render(
51+
<DataLayerProvider application="test" routes={routes}>
52+
<div />
53+
</DataLayerProvider>
54+
)
55+
56+
expect(window.digitalData!.get('page.name')).toBe('Fallback Title')
57+
})
58+
59+
it('fires a page_load event on mount', () => {
60+
new DataLayer('digitalData')
61+
62+
render(
63+
<DataLayerProvider application="test" routes={routes}>
64+
<div />
65+
</DataLayerProvider>
66+
)
67+
68+
expect(window.digitalData!.event.length).toBeGreaterThanOrEqual(1)
69+
const pageLoadEvent = window.digitalData!.event.find((e) => e.eventName === 'page_load')
70+
expect(pageLoadEvent).toBeDefined()
71+
expect(pageLoadEvent!.eventData).toEqual(
72+
expect.objectContaining({ name: 'Dashboard', flow: 'dashboard' })
73+
)
74+
})
75+
76+
it('updates context when pathname changes', () => {
77+
new DataLayer('digitalData')
78+
79+
const { rerender } = render(
80+
<DataLayerProvider application="test" routes={routes}>
81+
<div />
82+
</DataLayerProvider>
83+
)
84+
85+
expect(window.digitalData!.get('page.name')).toBe('Dashboard')
86+
87+
// Simulate navigation
88+
mockPathname = '/login'
89+
act(() => {
90+
rerender(
91+
<DataLayerProvider application="test" routes={routes}>
92+
<div />
93+
</DataLayerProvider>
94+
)
95+
})
96+
97+
expect(window.digitalData!.get('page.name')).toBe('Login')
98+
expect(window.digitalData!.get('page.flow')).toBe('auth')
99+
expect(window.digitalData!.get('page.step')).toBe('login')
100+
})
101+
102+
it('does not fire extra page_load when routes prop reference changes', () => {
103+
new DataLayer('digitalData')
104+
105+
const { rerender } = render(
106+
<DataLayerProvider application="test" routes={{ ...routes }}>
107+
<div />
108+
</DataLayerProvider>
109+
)
110+
111+
const countAfterMount = window.digitalData!.event.filter(
112+
(e) => e.eventName === 'page_load'
113+
).length
114+
115+
// Re-render with a new routes object reference but same pathname
116+
act(() => {
117+
rerender(
118+
<DataLayerProvider application="test" routes={{ ...routes }}>
119+
<div />
120+
</DataLayerProvider>
121+
)
122+
})
123+
124+
const countAfterRerender = window.digitalData!.event.filter(
125+
(e) => e.eventName === 'page_load'
126+
).length
127+
128+
expect(countAfterRerender).toBe(countAfterMount)
129+
})
130+
})

packages/analytics/src/DataLayerProvider.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,27 @@ import { usePathname } from 'next/navigation'
44
import { useEffect, useRef, type ReactNode } from 'react'
55

66
import { DataLayer } from './data-layer'
7-
import { CTA_CLICK, PAGE_LOAD } from './events'
7+
import { CTA_CLICK } from './events'
8+
9+
export interface PageContext {
10+
/** Logical page title */
11+
name: string
12+
/** High-level flow this page belongs to (e.g., "auth", "dashboard", "address_update") */
13+
flow: string
14+
/** Step index or logical step name within the flow */
15+
step: string
16+
}
17+
18+
/** Map of pathname to page context for automatic page load tracking. */
19+
export type RoutePageContextMap = Record<string, PageContext>
820

921
interface DataLayerProviderProps {
1022
/** Application identifier set on page.application (e.g., "sebt-portal", "sebt-enrollment-checker") */
1123
application: string
1224
/** Environment name (e.g., "dev", "staging", "production"). Defaults to NEXT_PUBLIC_ENVIRONMENT or "production". */
1325
environment?: string
26+
/** Map of pathname to page context. When navigation occurs, matching context is set and pageLoad() fires. */
27+
routes?: RoutePageContextMap
1428
children: ReactNode
1529
}
1630

@@ -19,7 +33,12 @@ interface DataLayerProviderProps {
1933
* Automatically tracks page views on navigation and CTA clicks via delegation.
2034
* Must be rendered client-side. Initializes once and persists across navigations.
2135
*/
22-
export function DataLayerProvider({ application, environment, children }: DataLayerProviderProps) {
36+
export function DataLayerProvider({
37+
application,
38+
environment,
39+
routes,
40+
children
41+
}: DataLayerProviderProps) {
2342
const initialized = useRef(false)
2443

2544
useEffect(() => {
@@ -32,6 +51,7 @@ export function DataLayerProvider({ application, environment, children }: DataLa
3251
}
3352

3453
window.digitalData!.page.set('application', application)
54+
window.digitalData!.page.set('entry_source', application.replace('sebt-', '').replace(/-/g, '_'))
3555
window.digitalData!.page.set(
3656
'environment',
3757
environment ?? process.env.NEXT_PUBLIC_ENVIRONMENT ?? 'production'
@@ -40,16 +60,18 @@ export function DataLayerProvider({ application, environment, children }: DataLa
4060

4161
return (
4262
<>
43-
<PageTracker />
63+
<PageTracker routes={routes} />
4464
<CtaTracker />
4565
{children}
4666
</>
4767
)
4868
}
4969

50-
/** Fires a page_load event on every client-side navigation. */
51-
function PageTracker() {
70+
/** Fires a page_load event on every client-side navigation via the dedicated pageLoad() API. */
71+
function PageTracker({ routes }: { routes: RoutePageContextMap | undefined }) {
5272
const pathname = usePathname()
73+
const routesRef = useRef(routes)
74+
routesRef.current = routes
5375

5476
useEffect(() => {
5577
if (typeof window === 'undefined' || !window.digitalData?.initialized) return
@@ -59,11 +81,20 @@ function PageTracker() {
5981
const dl = window.digitalData!
6082
const lang = document.documentElement.lang || 'en'
6183

62-
dl.page.set('name', document.title)
6384
dl.page.set('language', lang)
6485
dl.page.set('locale', `${lang}_US`)
6586

66-
dl.trackEvent(PAGE_LOAD)
87+
// Set route-specific context or fall back to document.title
88+
const ctx = routesRef.current?.[pathname]
89+
if (ctx) {
90+
dl.page.set('name', ctx.name)
91+
dl.page.set('flow', ctx.flow)
92+
dl.page.set('step', ctx.step)
93+
} else {
94+
dl.page.set('name', document.title)
95+
}
96+
97+
dl.pageLoad()
6798
})
6899

69100
return () => cancelAnimationFrame(raf)

0 commit comments

Comments
 (0)