/** * Property-Based Test: Profile panel displays all required fields * * Feature: user-profile, Property 1: Profile panel displays all required fields * **Validates: Requirements 1.2** * * For any valid profile object with arbitrary username, email, group, created_at, * and last_login values, rendering UserProfilePanel displays all five values * in the output. */ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import fc from 'fast-check'; import UserProfilePanel from '../components/UserProfilePanel'; // Replicate the component's formatting logic so we know what to expect in the DOM function formatGroupName(group) { if (!group) return ''; return group.replace(/_/g, ' '); } function formatDate(dateStr) { if (!dateStr) return 'Never'; try { const date = new Date(dateStr); if (isNaN(date.getTime())) return 'Unknown'; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }) + ' at ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, }); } catch { return 'Unknown'; } } // Generate ISO date strings from integer timestamps to avoid invalid Date issues const MIN_TS = new Date('2020-01-01T00:00:00Z').getTime(); const MAX_TS = new Date('2030-12-31T23:59:59Z').getTime(); const isoDateArbitrary = fc .integer({ min: MIN_TS, max: MAX_TS }) .map(ts => new Date(ts).toISOString()); // Arbitrary that generates valid profile objects. // Use minLength >= 3 for username to avoid single-character strings that // match substrings in other UI text (e.g., "d" appearing in "Password"). // Use a custom email generator with a longer local part for the same reason. const profileArbitrary = fc.record({ id: fc.integer({ min: 1, max: 100000 }), username: fc.stringMatching(/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/), email: fc.tuple( fc.stringMatching(/^[a-z]{4,10}$/), fc.stringMatching(/^[a-z]{3,8}$/), fc.constantFrom('com', 'org', 'net', 'io') ).map(([local, domain, tld]) => `${local}@${domain}.${tld}`), group: fc.constantFrom('Admin', 'Standard_User', 'Leadership', 'Read_Only'), created_at: isoDateArbitrary, last_login: isoDateArbitrary, }); /** * Helper: find all fieldValue spans in the rendered component. * The component renders each profile field in a fieldRow div containing * a fieldLabel span and a fieldValue span. We query by the known label * text to locate the corresponding value span. */ function getFieldValueByLabel(container, labelText) { // Each field row has structure: //
// //
// LABEL // VALUE //
//
const labels = container.querySelectorAll('span'); for (const label of labels) { if (label.textContent.trim().toLowerCase() === labelText.toLowerCase()) { // The value span is the next sibling of the label span const valueSibling = label.nextElementSibling; if (valueSibling) { return valueSibling.textContent; } } } return null; } beforeEach(() => { process.env.REACT_APP_API_BASE = 'http://localhost:3001/api'; }); afterEach(() => { jest.restoreAllMocks(); }); it('Property 1: Profile panel displays all required fields for any valid profile', async () => { await fc.assert( fc.asyncProperty(profileArbitrary, async (profile) => { // Mock fetch to return the generated profile global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ ...profile }), }); const onClose = jest.fn(); const { container, unmount } = render( ); try { // Wait for the profile data to be fetched and rendered. // Check for the username label to confirm the profile section loaded. await waitFor(() => { expect(getFieldValueByLabel(container, 'Username')).not.toBeNull(); }, { timeout: 3000 }); // Assert all five field values appear in their respective field rows // 1. Username — rendered directly in the Username field value span const usernameValue = getFieldValueByLabel(container, 'Username'); expect(usernameValue).toBe(profile.username); // 2. Email — rendered directly in the Email field value span const emailValue = getFieldValueByLabel(container, 'Email'); expect(emailValue).toBe(profile.email); // 3. Group — rendered through formatGroupName in the Group field value span const groupValue = getFieldValueByLabel(container, 'Group'); const expectedGroup = formatGroupName(profile.group); expect(groupValue).toContain(expectedGroup); // 4. Created at — rendered through formatDate in the Account Created field value span const createdAtValue = getFieldValueByLabel(container, 'Account Created'); const expectedCreatedAt = formatDate(profile.created_at); expect(createdAtValue).toBe(expectedCreatedAt); // 5. Last login — rendered through formatDate in the Last Login field value span const lastLoginValue = getFieldValueByLabel(container, 'Last Login'); const expectedLastLogin = formatDate(profile.last_login); expect(lastLoginValue).toBe(expectedLastLogin); } finally { // Clean up to avoid leaking state between iterations unmount(); } }), { numRuns: 100 } ); }, 120000);