/** * 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: //