feat(coop): Add BuddyManagement, CheckinHistory, and EmergencyContactsForm components with E2E tests for cooperative safety functionality

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-03-03 02:26:17 -08:00
parent 6d0e6997c9
commit 661c5845f5
5 changed files with 68 additions and 41 deletions

View file

@ -87,10 +87,9 @@ export class CoopSafetyTab {
.locator('section')
.filter({ hasText: /my buddies/i })
.or(this.tabPanel.getByText(/my buddies/i).locator('..'))
this.buddyCards = this.buddySection.getByRole('article')
.or(this.buddySection.locator('li'))
this.buddyCards = this.buddySection.locator('[data-testid="buddy-card"]')
this.designateBuddyButton = this.tabPanel.getByRole('button', {
name: /designate buddy|add buddy|choose buddy/i,
name: /add.*buddy/i,
})
this.buddyAlertBanner = this.tabPanel
.getByRole('alert')
@ -123,7 +122,7 @@ export class CoopSafetyTab {
})
// ── Panic button ──
this.panicButton = this.tabPanel.getByRole('button', { name: /panic|emergency/i })
this.panicButton = this.tabPanel.getByRole('button', { name: /activate panic/i })
// ── Emergency contacts form — located by heading text ──
this.emergencyContactsSection = this.tabPanel
@ -131,8 +130,7 @@ export class CoopSafetyTab {
.filter({ hasText: /emergency contacts/i })
.or(this.tabPanel.getByText(/emergency contacts/i).locator('..'))
this.emergencyContactItems = this.emergencyContactsSection
.getByRole('article')
.or(this.emergencyContactsSection.locator('li'))
.locator('[data-testid="emergency-contact-item"]')
this.addEmergencyContactButton = this.tabPanel.getByRole('button', {
name: /add contact|add emergency/i,
})
@ -159,8 +157,7 @@ export class CoopSafetyTab {
.filter({ hasText: /check.in history/i })
.or(this.tabPanel.getByText(/check.in history/i).locator('..'))
this.checkinHistoryItems = this.checkinHistory
.getByRole('article')
.or(this.checkinHistory.locator('li'))
.locator('[data-testid="checkin-session-row"]')
}
// ── Buddy actions ──

View file

@ -37,54 +37,84 @@ const panicApiUrl = /\/api\/cooperatives\/[^/]+\/checkin\/panic/
const activeCoop = TEST_COOPERATIVES.activeCoop
const emptyCheckinState = { sessions: [], active: null, history: [] }
const emptyBuddyState = { buddies: [], pendingRequests: [] }
const emptyCheckinState = { sessions: [], total: 0 }
const emptyBuddyState = { buddies: [] }
const emptyContacts = { contacts: [] }
const mockContacts = {
contacts: [
{ id: 'ec-1', name: 'Jane Smith', phone: '+44 7700 900123', relationship: 'Sister' },
{ id: 'ec-2', name: 'Bob Jones', phone: '+44 7700 900456', relationship: 'Partner' },
{ id: 'ec-1', profileId: 'profile-sophia-1', name: 'Jane Smith', phone: '+44 7700 900123', email: null, relationship: 'Sister', contactOrder: 0, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
{ id: 'ec-2', profileId: 'profile-sophia-1', name: 'Bob Jones', phone: '+44 7700 900456', email: null, relationship: 'Partner', contactOrder: 1, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
],
}
const now = Date.now()
const activeCheckinState = {
active: {
id: 'checkin-active-1',
startedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
intervalMinutes: 60,
nextCheckinAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
},
sessions: [],
history: [
sessions: [
{
id: 'checkin-hist-1',
startedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
endedAt: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(),
status: 'completed',
cooperativeId: activeCoop.id,
profileId: 'profile-sophia-1',
sessionType: 'timer',
status: 'resolved',
deadlineAt: new Date(now - 90 * 60 * 1000).toISOString(),
durationMinutes: 60,
assignedBuddyProfileId: null,
currentEscalationTier: 'none',
escalationStartedAt: null,
checkedInAt: new Date(now - 60 * 60 * 1000).toISOString(),
resolvedAt: new Date(now - 60 * 60 * 1000).toISOString(),
resolvedByProfileId: 'profile-sophia-1',
resolutionMethod: 'self_checkin',
hasLocation: false,
metadata: {},
createdAt: new Date(now - 2 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(now - 60 * 60 * 1000).toISOString(),
},
],
total: 1,
}
const overdueCheckinState = {
active: {
id: 'checkin-overdue-1',
startedAt: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
intervalMinutes: 60,
nextCheckinAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
buddyAlerted: true,
},
sessions: [],
history: [],
sessions: [
{
id: 'checkin-overdue-1',
cooperativeId: activeCoop.id,
profileId: 'profile-jade-1',
sessionType: 'timer',
status: 'overdue',
deadlineAt: new Date(now - 60 * 60 * 1000).toISOString(),
durationMinutes: 60,
assignedBuddyProfileId: 'profile-sophia-1',
currentEscalationTier: 'none',
escalationStartedAt: null,
checkedInAt: null,
resolvedAt: null,
resolvedByProfileId: null,
resolutionMethod: null,
hasLocation: false,
metadata: {},
createdAt: new Date(now - 3 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(now - 60 * 60 * 1000).toISOString(),
},
],
total: 1,
}
const buddiesWithRequest = {
buddies: [],
pendingRequests: [
const buddiesWithPending = {
buddies: [
{
id: 'buddy-req-1',
requesterProfileId: TEST_COOP_MEMBERS['coop-active-1'][1].profileId,
requesterDisplayName: TEST_COOP_MEMBERS['coop-active-1'][1].displayName,
cooperativeId: activeCoop.id,
profileId: TEST_COOP_MEMBERS['coop-active-1'][1].profileId,
buddyProfileId: 'profile-sophia-1',
priority: 1,
status: 'pending',
isOnCall: false,
onCallSchedule: null,
createdAt: new Date(now - 24 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(now - 24 * 60 * 60 * 1000).toISOString(),
},
],
}
@ -129,7 +159,7 @@ test.describe('Coop Safety Tab', () => {
test('accept buddy request from pending requests', async ({ page }) => {
await mockApiRoute(page, 'GET', checkinSessionsApiUrl, emptyCheckinState)
await mockApiRoute(page, 'GET', buddiesApiUrl, buddiesWithRequest)
await mockApiRoute(page, 'GET', buddiesApiUrl, buddiesWithPending)
await mockApiRoute(page, 'GET', emergencyContactsApiUrl, emptyContacts)
await mockApiRoute(page, 'POST', buddiesApiUrl, { success: true })

View file

@ -196,7 +196,7 @@ const BuddyCardItem: FC<BuddyCardProps> = ({ buddy, currentProfileId, coopId })
}, [buddy.id, removeBuddy]);
return (
<S.BuddyCard>
<S.BuddyCard data-testid="buddy-card">
<S.BuddyCardHeader>
<S.PriorityBadge aria-label={`Priority ${buddy.priority}`}>
{buddy.priority}

View file

@ -175,7 +175,7 @@ const SessionHistoryRow: FC<SessionHistoryRowProps> = ({ session }) => {
const tierNumber = tierMap[session.currentEscalationTier] ?? 0;
return (
<S.SessionRow>
<S.SessionRow data-testid="checkin-session-row">
<S.SessionTypeIcon
title={session.sessionType}
aria-label={`Session type: ${session.sessionType}`}

View file

@ -223,7 +223,7 @@ const ContactRowItem: FC<ContactRowProps> = ({
}, [contact.id, deleteContact]);
return (
<S.ContactRow>
<S.ContactRow data-testid="emergency-contact-item">
<S.ContactRowHeader>
<S.OrderControls>
<S.OrderButton