154 lines
5.5 KiB
JavaScript
154 lines
5.5 KiB
JavaScript
|
|
/**
|
||
|
|
* 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:
|
||
|
|
// <div style={fieldRow}>
|
||
|
|
// <svg ... />
|
||
|
|
// <div style={fieldContent}>
|
||
|
|
// <span style={fieldLabel}>LABEL</span>
|
||
|
|
// <span style={fieldValue}>VALUE</span>
|
||
|
|
// </div>
|
||
|
|
// </div>
|
||
|
|
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(
|
||
|
|
<UserProfilePanel isOpen={true} onClose={onClose} />
|
||
|
|
);
|
||
|
|
|
||
|
|
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);
|