import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import './usa-card.ts';
import type { USACard } from './usa-card.js';
import {
testComponentAccessibility,
USWDS_A11Y_CONFIG,
} from '../../../__tests__/accessibility-utils.js';
import { quickUSWDSComplianceTest } from '../../../__tests__/uswds-compliance-utils.js';
import {
validateComponentJavaScript,
testSlottedContent,
assertSlottedContentWorks,
mockNavigation,
} from '../../../__tests__/test-utils.js';
import {
testTextResize,
testReflow,
testTextSpacing,
testMobileAccessibility,
} from '../../../__tests__/responsive-accessibility-utils.js';
describe('USACard', () => {
let element: USACard;
beforeEach(() => {
// Mock navigation to avoid jsdom errors
mockNavigation();
element = document.createElement('usa-card') as USACard;
document.body.appendChild(element);
});
afterEach(() => {
element.remove();
});
describe('Default Properties', () => {
it('should have correct default properties', async () => {
await element.updateComplete;
expect(element.heading).toBe('');
expect(element.text).toBe('');
expect(element.mediaType).toBe('none');
expect(element.mediaSrc).toBe('');
expect(element.mediaAlt).toBe('');
expect(element.mediaPosition).toBe('inset');
expect(element.flagLayout).toBe(false);
expect(element.headerFirst).toBe(false);
expect(element.actionable).toBe(false);
expect(element.href).toBe('');
expect(element.target).toBe('');
expect(element.footerText).toBe('');
expect(element.headingLevel).toBe('3');
});
});
describe('Basic Content Rendering', () => {
it('should render heading when provided', async () => {
element.heading = 'Test Card Title';
await element.updateComplete;
const heading = element.querySelector('.usa-card__heading') as HTMLHeadingElement;
expect(heading).toBeTruthy();
expect(heading.textContent).toBe('Test Card Title');
expect(heading.tagName).toBe('H3'); // default heading level
});
it('should render text content when provided', async () => {
element.text = 'This is test card content';
await element.updateComplete;
const body = element.querySelector('.usa-card__body');
const paragraph = body?.querySelector('p');
expect(paragraph?.textContent).toBe('This is test card content');
});
it('should render footer text when provided', async () => {
element.footerText = 'Footer content';
await element.updateComplete;
const footer = element.querySelector('.usa-card__footer');
const paragraph = footer?.querySelector('p');
expect(paragraph?.textContent).toBe('Footer content');
});
it('should not render sections when content is empty', async () => {
await element.updateComplete;
expect(element.querySelector('.usa-card__header')).toBeNull();
expect(element.querySelector('.usa-card__body')).toBeNull();
expect(element.querySelector('.usa-card__footer')).toBeNull();
expect(element.querySelector('.usa-card__media')).toBeNull();
});
});
describe('Heading Levels', () => {
it('should render different heading levels correctly', async () => {
element.heading = 'Test Heading';
// Test different heading levels
const levels = ['1', '2', '3', '4', '5', '6'] as const;
for (const level of levels) {
element.headingLevel = level;
await element.updateComplete;
const heading = element.querySelector('.usa-card__heading') as HTMLHeadingElement;
expect(heading.tagName).toBe(`H${level}`);
}
});
it('should default to h3 heading level', async () => {
element.heading = 'Default Level Heading';
await element.updateComplete;
const heading = element.querySelector('.usa-card__heading') as HTMLHeadingElement;
expect(heading.tagName).toBe('H3');
});
});
describe('Media Rendering', () => {
it('should render image media when configured', async () => {
element.mediaType = 'image';
element.mediaSrc = 'test-image.jpg';
element.mediaAlt = 'Test image';
await element.updateComplete;
const media = element.querySelector('.usa-card__media');
const imgContainer = media?.querySelector('.usa-card__img');
const img = imgContainer?.querySelector('img');
expect(media).toBeTruthy();
expect(imgContainer).toBeTruthy();
expect(img?.src).toContain('test-image.jpg');
expect(img?.alt).toBe('Test image');
expect(img?.getAttribute('loading')).toBe('lazy');
});
it('should render video media when configured', async () => {
element.mediaType = 'video';
element.mediaSrc = 'test-video.mp4';
element.mediaAlt = 'Test video';
await element.updateComplete;
const media = element.querySelector('.usa-card__media');
const videoContainer = media?.querySelector('.usa-card__img');
const video = videoContainer?.querySelector('video');
expect(media).toBeTruthy();
expect(videoContainer).toBeTruthy();
expect(video?.src).toContain('test-video.mp4');
expect(video?.getAttribute('aria-label')).toBe('Test video');
expect(video?.hasAttribute('controls')).toBe(true);
});
it('should not render media when type is none', async () => {
element.mediaType = 'none';
element.mediaSrc = 'test-image.jpg';
await element.updateComplete;
expect(element.querySelector('.usa-card__media')).toBeNull();
});
it('should not render media when src is empty', async () => {
element.mediaType = 'image';
element.mediaSrc = '';
await element.updateComplete;
expect(element.querySelector('.usa-card__media')).toBeNull();
});
});
describe('Media Position Classes', () => {
beforeEach(() => {
element.mediaType = 'image';
element.mediaSrc = 'test.jpg';
});
it('should apply inset media class', async () => {
element.mediaPosition = 'inset';
await element.updateComplete;
const media = element.querySelector('.usa-card__media');
expect(media?.classList.contains('usa-card__media--inset')).toBe(true);
});
it('should apply exdent media class', async () => {
element.mediaPosition = 'exdent';
await element.updateComplete;
const media = element.querySelector('.usa-card__media');
expect(media?.classList.contains('usa-card__media--exdent')).toBe(true);
});
it('should apply media-right class to card when mediaPosition is right', async () => {
element.mediaPosition = 'right';
await element.updateComplete;
expect(element.classList.contains('usa-card--media-right')).toBe(true);
});
it('should automatically enable flag layout when mediaPosition is right', async () => {
// Media right requires flag layout according to USWDS
element.mediaPosition = 'right';
element.flagLayout = false; // Explicitly set to false
await element.updateComplete;
// Should automatically apply flag layout
expect(element.classList.contains('usa-card--flag')).toBe(true);
expect(element.classList.contains('usa-card--media-right')).toBe(true);
});
});
describe('Card Layout Variants', () => {
it('should apply flag layout class', async () => {
element.flagLayout = true;
await element.updateComplete;
expect(element.classList.contains('usa-card--flag')).toBe(true);
});
it('should apply header-first class', async () => {
element.headerFirst = true;
await element.updateComplete;
expect(element.classList.contains('usa-card--header-first')).toBe(true);
});
it('should apply multiple layout classes together', async () => {
element.flagLayout = true;
element.headerFirst = true;
element.mediaType = 'image';
element.mediaSrc = 'test.jpg';
element.mediaPosition = 'right';
await element.updateComplete;
expect(element.classList.contains('usa-card--flag')).toBe(true);
expect(element.classList.contains('usa-card--header-first')).toBe(true);
expect(element.classList.contains('usa-card--media-right')).toBe(true);
});
it('should always have base usa-card class', async () => {
await element.updateComplete;
expect(element.classList.contains('usa-card')).toBe(true);
});
});
describe('Actionable Cards', () => {
it('should add role and tabindex when actionable', async () => {
element.actionable = true;
await element.updateComplete;
expect(element.getAttribute('role')).toBe('button');
expect(element.getAttribute('tabindex')).toBe('0');
});
it('should remove role and tabindex when not actionable', async () => {
element.actionable = true;
await element.updateComplete;
element.actionable = false;
await element.updateComplete;
expect(element.hasAttribute('role')).toBe(false);
expect(element.hasAttribute('tabindex')).toBe(false);
});
});
describe('Event Handling', () => {
it('should dispatch card-click event when actionable card is clicked', async () => {
element.actionable = true;
element.heading = 'Test Card';
element.href = ''; // Clear href to avoid navigation
await element.updateComplete;
let eventDetail: any;
element.addEventListener('card-click', (e: any) => {
eventDetail = e.detail;
});
element.click();
expect(eventDetail).toBeTruthy();
expect(eventDetail.heading).toBe('Test Card');
});
it('should not dispatch card-click event when not actionable', async () => {
element.actionable = false;
await element.updateComplete;
let eventFired = false;
element.addEventListener('card-click', () => {
eventFired = true;
});
element.click();
expect(eventFired).toBe(false);
});
it('should handle keyboard events on actionable cards', async () => {
element.actionable = true;
element.heading = 'Keyboard Test';
await element.updateComplete;
let eventDetail: any;
element.addEventListener('card-click', (e: any) => {
eventDetail = e.detail;
});
// Test Enter key
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
element.dispatchEvent(enterEvent);
expect(eventDetail?.heading).toBe('Keyboard Test');
// Test Space key
eventDetail = null;
const spaceEvent = new KeyboardEvent('keydown', { key: ' ' });
element.dispatchEvent(spaceEvent);
expect(eventDetail?.heading).toBe('Keyboard Test');
});
it('should not handle keyboard events when not actionable', async () => {
element.actionable = false;
await element.updateComplete;
let eventFired = false;
element.addEventListener('card-click', () => {
eventFired = true;
});
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
element.dispatchEvent(enterEvent);
expect(eventFired).toBe(false);
});
});
describe('Content Order and Layout', () => {
beforeEach(() => {
element.heading = 'Test Heading';
element.text = 'Test content';
element.mediaType = 'image';
element.mediaSrc = 'test.jpg';
element.footerText = 'Test footer';
});
it('should render content in default order', async () => {
await element.updateComplete;
const container = element.querySelector('.usa-card__container');
const children = Array.from(container?.children || []);
// Should have media first, then header, body, footer
expect(children[0]?.classList.contains('usa-card__media')).toBe(true);
expect(children[1]?.classList.contains('usa-card__header')).toBe(true);
expect(children[2]?.classList.contains('usa-card__body')).toBe(true);
expect(children[3]?.classList.contains('usa-card__footer')).toBe(true);
});
it('should render header first when headerFirst is true', async () => {
element.headerFirst = true;
await element.updateComplete;
const container = element.querySelector('.usa-card__container');
const children = Array.from(container?.children || []);
// Should have header first, then media, body, footer
expect(children[0]?.classList.contains('usa-card__header')).toBe(true);
expect(children[1]?.classList.contains('usa-card__media')).toBe(true);
});
it('should handle media-right non-flag layout differently', async () => {
element.mediaPosition = 'right';
element.flagLayout = false;
await element.updateComplete;
// Media-right non-flag should have a different structure
const container = element.querySelector('.usa-card__container');
expect(container).toBeTruthy();
// Should have media and then a body wrapper
const media = container?.querySelector('.usa-card__media');
const bodyWrapper = container?.querySelector('.usa-card__body');
expect(media).toBeTruthy();
expect(bodyWrapper).toBeTruthy();
});
});
describe('Comprehensive Slotted Content Validation', () => {
beforeEach(() => {
// Set a heading to ensure the card renders
element.heading = 'Test Card';
});
it('should render body slot content correctly', async () => {
const validation = await testSlottedContent(element, [
{
slotName: 'body',
description: 'Card body slot',
testContent: '
Test body content
',
expectedWrapperSelector: '.usa-card__body',
}
]);
expect(validation.isValid).toBe(true);
expect(validation.issues).toHaveLength(0);
expect(validation.results[0].slotExists).toBe(true);
expect(validation.results[0].wrapperRendered).toBe(true);
});
it('should render footer slot content correctly', async () => {
const validation = await testSlottedContent(element, [
{
slotName: 'footer',
description: 'Card footer slot',
testContent: '',
expectedWrapperSelector: '.usa-card__footer',
}
]);
expect(validation.isValid).toBe(true);
expect(validation.issues).toHaveLength(0);
expect(validation.results[0].slotExists).toBe(true);
expect(validation.results[0].wrapperRendered).toBe(true);
});
it('should render both body and footer slots together', async () => {
const validation = await testSlottedContent(element, [
{
slotName: 'body',
description: 'Card body slot',
testContent: 'Test body content
',
expectedWrapperSelector: '.usa-card__body',
},
{
slotName: 'footer',
description: 'Card footer slot',
testContent: '',
expectedWrapperSelector: '.usa-card__footer',
}
]);
expect(validation.isValid).toBe(true);
expect(validation.issues).toHaveLength(0);
expect(validation.results).toHaveLength(2);
expect(validation.results[0].slotExists).toBe(true);
expect(validation.results[0].wrapperRendered).toBe(true);
expect(validation.results[1].slotExists).toBe(true);
expect(validation.results[1].wrapperRendered).toBe(true);
});
it('should work with assertSlottedContentWorks helper', async () => {
// This will throw if validation fails
await assertSlottedContentWorks(element, [
{
slotName: 'body',
description: 'Card body',
testContent: 'Test
',
expectedWrapperSelector: '.usa-card__body'
}
]);
});
it('should handle complex slotted content', async () => {
const validation = await testSlottedContent(element, [
{
slotName: 'body',
description: 'Card body with complex HTML',
testContent: `
Complex Content
Description text
`,
expectedWrapperSelector: '.usa-card__body',
}
]);
expect(validation.isValid).toBe(true);
expect(validation.issues).toHaveLength(0);
});
});
describe('USWDS CSS Classes', () => {
it('should always have base usa-card class on host', async () => {
await element.updateComplete;
expect(element.classList.contains('usa-card')).toBe(true);
});
it('should have correct container structure', async () => {
element.heading = 'Test';
await element.updateComplete;
const container = element.querySelector('.usa-card__container');
const header = element.querySelector('.usa-card__header');
const heading = element.querySelector('.usa-card__heading');
expect(container).toBeTruthy();
expect(header).toBeTruthy();
expect(heading).toBeTruthy();
});
it('should apply proper USWDS structure', async () => {
element.heading = 'Test Heading';
element.text = 'Test content';
element.footerText = 'Test footer';
element.mediaType = 'image';
element.mediaSrc = 'test.jpg';
await element.updateComplete;
// Check all USWDS card elements exist
expect(element.querySelector('.usa-card__container')).toBeTruthy();
expect(element.querySelector('.usa-card__header')).toBeTruthy();
expect(element.querySelector('.usa-card__heading')).toBeTruthy();
expect(element.querySelector('.usa-card__body')).toBeTruthy();
expect(element.querySelector('.usa-card__footer')).toBeTruthy();
expect(element.querySelector('.usa-card__media')).toBeTruthy();
});
});
describe('Light DOM Rendering', () => {
it('should render in light DOM for USWDS compatibility', async () => {
await element.updateComplete;
expect(element.shadowRoot).toBeNull();
expect(element.querySelector('.usa-card__container')).toBeTruthy();
});
});
describe('Property Updates', () => {
it('should update host classes when layout properties change', async () => {
await element.updateComplete;
// Initially should only have base class
expect(element.classList.contains('usa-card')).toBe(true);
expect(element.classList.contains('usa-card--flag')).toBe(false);
// Update flagLayout
element.flagLayout = true;
await element.updateComplete;
expect(element.classList.contains('usa-card--flag')).toBe(true);
// Update headerFirst
element.headerFirst = true;
await element.updateComplete;
expect(element.classList.contains('usa-card--header-first')).toBe(true);
});
it('should update content when properties change', async () => {
element.heading = 'Initial Heading';
await element.updateComplete;
let heading = element.querySelector('.usa-card__heading');
expect(heading?.textContent).toBe('Initial Heading');
element.heading = 'Updated Heading';
await element.updateComplete;
heading = element.querySelector('.usa-card__heading');
expect(heading?.textContent).toBe('Updated Heading');
});
});
describe('Navigation Handling', () => {
it('should include href in card-click event when actionable', async () => {
element.actionable = true;
element.href = 'https://example.com/page';
await element.updateComplete;
let eventDetail: any;
element.addEventListener('card-click', (e: any) => {
eventDetail = e.detail;
// Prevent actual navigation in test
e.preventDefault();
});
// Clear href to avoid navigation in test
element.href = '';
element.click();
expect(eventDetail).toBeTruthy();
});
it('should handle target _blank attribute', async () => {
element.actionable = true;
element.href = 'https://example.com/page';
element.target = '_blank';
await element.updateComplete;
// Test that target is properly set
expect(element.target).toBe('_blank');
expect(element.href).toBe('https://example.com/page');
});
});
describe('Event Listener Management', () => {
it('should add event listeners when connected', () => {
// Create a new element to test connection
const newElement = document.createElement('usa-card') as USACard;
// Spy on addEventListener
const addEventListenerSpy = vi.spyOn(newElement, 'addEventListener');
document.body.appendChild(newElement);
expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
newElement.remove();
});
it('should remove event listeners when disconnected', () => {
const newElement = document.createElement('usa-card') as USACard;
document.body.appendChild(newElement);
// Spy on removeEventListener
const removeEventListenerSpy = vi.spyOn(newElement, 'removeEventListener');
newElement.remove();
expect(removeEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
});
});
describe('CRITICAL: Component Lifecycle Stability', () => {
beforeEach(() => {
element = document.createElement('usa-card') as USACard;
document.body.appendChild(element);
});
it('should remain in DOM after property changes', async () => {
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
element.heading = 'Test Card Title';
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
element.text = 'Test card content text';
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
element.actionable = true;
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
});
it('should maintain element stability during card content updates', async () => {
const originalElement = element;
const contentUpdates = [
{ heading: 'Card 1', text: 'Content 1' },
{ heading: 'Card 2', text: 'Content 2', footerText: 'Footer 2' },
{ heading: 'Card 3', text: 'Content 3', mediaType: 'image', mediaSrc: '/test.jpg' },
];
for (const update of contentUpdates) {
Object.assign(element, update);
await element.updateComplete;
expect(element).toBe(originalElement);
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
it('should preserve DOM connection through layout property changes', async () => {
const layoutConfigurations = [
{ flagLayout: false, headerFirst: false, mediaPosition: 'inset' },
{ flagLayout: true, headerFirst: true, mediaPosition: 'exdent' },
{ flagLayout: false, headerFirst: false, mediaPosition: 'right' },
];
for (const config of layoutConfigurations) {
Object.assign(element, config);
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
});
describe('CRITICAL: Event System Stability', () => {
beforeEach(async () => {
element = document.createElement('usa-card') as USACard;
element.heading = 'Event Card';
element.text = 'Event card content';
element.actionable = true;
document.body.appendChild(element);
await element.updateComplete;
});
it('should not pollute global event handling', async () => {
const globalClickSpy = vi.fn();
document.addEventListener('click', globalClickSpy);
const cardClickSpy = vi.fn();
element.addEventListener('card-click', cardClickSpy);
element.click();
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
document.removeEventListener('click', globalClickSpy);
});
it('should maintain stability during card click interactions', async () => {
const cardClickSpy = vi.fn();
element.addEventListener('card-click', cardClickSpy);
// Multiple click interactions
for (let i = 0; i < 3; i++) {
element.click();
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
expect(cardClickSpy.mock.calls.length).toBe(3);
});
it('should maintain stability during actionable state changes', async () => {
const actionableStates = [false, true, false, true];
for (const actionable of actionableStates) {
element.actionable = actionable;
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
});
describe('CRITICAL: Card State Management Stability', () => {
beforeEach(async () => {
element = document.createElement('usa-card') as USACard;
document.body.appendChild(element);
await element.updateComplete;
});
it('should maintain DOM connection during media configuration changes', async () => {
const mediaConfigurations = [
{ mediaType: 'none', mediaSrc: '', mediaAlt: '' },
{ mediaType: 'image', mediaSrc: '/test.jpg', mediaAlt: 'Test image' },
{ mediaType: 'video', mediaSrc: '/test.mp4', mediaAlt: 'Test video' },
{ mediaType: 'none', mediaSrc: '', mediaAlt: '' },
];
for (const config of mediaConfigurations) {
Object.assign(element, config);
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
it('should preserve element stability during complex card updates', async () => {
const originalElement = element;
const complexUpdates = [
{
heading: 'Government Service Card',
text: 'Access government services online',
footerText: 'Learn more',
actionable: true,
href: '/services',
flagLayout: true,
},
{
heading: 'Updated Service',
text: 'Updated service description',
mediaType: 'image',
mediaSrc: '/service.jpg',
mediaAlt: 'Service image',
mediaPosition: 'exdent',
headerFirst: true,
},
{
heading: 'Final Configuration',
text: 'Final card content',
actionable: false,
mediaType: 'none',
flagLayout: false,
headerFirst: false,
},
];
for (const update of complexUpdates) {
Object.assign(element, update);
await element.updateComplete;
expect(element).toBe(originalElement);
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
it('should maintain stability during heading level changes', async () => {
element.heading = 'Heading Level Test';
const headingLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
for (const level of headingLevels) {
element.headingLevel = level as any;
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
});
describe('CRITICAL: Storybook Integration', () => {
it('should render in Storybook-like environment without auto-dismiss', async () => {
const storyContainer = document.createElement('div');
storyContainer.id = 'storybook-root';
document.body.appendChild(storyContainer);
element = document.createElement('usa-card') as USACard;
element.heading = 'Storybook Card';
element.text = 'Storybook card content';
element.footerText = 'Storybook footer';
element.actionable = true;
storyContainer.appendChild(element);
await element.updateComplete;
// Simulate Storybook control updates
element.heading = 'Updated Storybook Card';
element.flagLayout = true;
element.headerFirst = true;
element.mediaType = 'image';
element.mediaSrc = '/storybook-image.jpg';
element.mediaAlt = 'Storybook image';
await element.updateComplete;
expect(storyContainer.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
expect(element.heading).toBe('Updated Storybook Card');
storyContainer.remove();
});
it('should handle Storybook args updates without component removal', async () => {
element = document.createElement('usa-card') as USACard;
document.body.appendChild(element);
await element.updateComplete;
const storyArgs = [
{
heading: 'Args Card 1',
text: 'Args content 1',
actionable: false,
flagLayout: false,
},
{
heading: 'Args Card 2',
text: 'Args content 2',
footerText: 'Args footer 2',
actionable: true,
flagLayout: true,
mediaType: 'image',
mediaSrc: '/args.jpg',
},
{
heading: 'Args Card 3',
text: 'Args content 3',
actionable: false,
mediaType: 'none',
flagLayout: false,
},
];
for (const args of storyArgs) {
Object.assign(element, args);
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
it('should maintain stability during complex Storybook interactions', async () => {
element = document.createElement('usa-card') as USACard;
document.body.appendChild(element);
// Simulate complex Storybook scenario with rapid changes
const interactions = [
() => {
element.heading = 'Interactive Card';
},
() => {
element.text = 'Interactive content';
},
() => {
element.actionable = true;
},
() => {
element.flagLayout = true;
},
() => {
element.mediaType = 'image';
element.mediaSrc = '/interactive.jpg';
},
() => {
element.mediaPosition = 'right';
},
() => {
element.headerFirst = true;
},
() => {
element.footerText = 'Interactive footer';
},
];
for (const interaction of interactions) {
interaction();
await element.updateComplete;
expect(document.body.contains(element)).toBe(true);
expect(element.isConnected).toBe(true);
}
});
describe('JavaScript Implementation Validation', () => {
it('should pass JavaScript implementation validation', async () => {
// Validate USWDS JavaScript implementation patterns
const componentPath =
`${process.cwd()}/src/components/card/usa-card.ts`;
const validation = validateComponentJavaScript(componentPath, 'card');
if (!validation.isValid) {
console.warn('JavaScript validation issues:', validation.issues);
}
// JavaScript validation should pass for critical integration patterns
expect(validation.score).toBeGreaterThan(50); // Allow some non-critical issues
// Critical USWDS integration should be present
const criticalIssues = validation.issues.filter((issue) =>
issue.includes('Missing USWDS JavaScript integration')
);
expect(criticalIssues.length).toBe(0);
});
});
});
describe('Accessibility', () => {
it('should pass comprehensive accessibility tests (same as Storybook)', async () => {
element.heading = 'Test Card Header';
element.text = 'Test card body content for accessibility testing.';
await element.updateComplete;
await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE);
});
it('should pass comprehensive USWDS compliance tests (prevents structural issues)', async () => {
element.heading = 'Test Card Header';
element.text = 'Test card content';
await element.updateComplete;
quickUSWDSComplianceTest(element, 'usa-card');
});
});
describe('Responsive/Reflow Accessibility (WCAG 1.4)', () => {
it('should resize text properly up to 200% (WCAG 1.4.4)', async () => {
element.heading = 'Card Heading';
element.text = 'Card body text content';
await element.updateComplete;
const heading = element.querySelector('.usa-card__heading');
expect(heading).toBeTruthy();
const result = testTextResize(heading as Element, 200);
expect(result).toBeDefined();
expect(result.violations).toBeDefined();
});
it('should reflow card content at 320px width (WCAG 1.4.10)', async () => {
element.heading = 'Responsive Card';
element.text = 'This card should reflow properly on mobile devices';
await element.updateComplete;
const result = testReflow(element, 320);
expect(result).toBeDefined();
expect(result.contentWidth).toBeGreaterThanOrEqual(0);
});
it('should support text spacing adjustments (WCAG 1.4.12)', async () => {
element.heading = 'Text Spacing Test';
element.text = 'Testing text spacing adjustments in card content';
await element.updateComplete;
const body = element.querySelector('.usa-card__body');
expect(body).toBeTruthy();
const result = testTextSpacing(body as Element);
expect(result.readable).toBe(true);
expect(result.violations.length).toBe(0);
});
it('should be accessible on mobile devices (comprehensive)', async () => {
element.heading = 'Mobile Card Test';
element.text = 'Testing mobile accessibility';
await element.updateComplete;
const result = await testMobileAccessibility(element);
expect(result).toBeDefined();
expect(result.details.reflowWorks).toBeDefined();
expect(result.details.textResizable).toBeDefined();
});
it('should maintain responsive behavior in flag layout (WCAG 1.4.10)', async () => {
element.flagLayout = true;
element.heading = 'Flag Layout Card';
element.text = 'Testing flag layout responsiveness';
await element.updateComplete;
const result = testReflow(element, 320);
expect(result).toBeDefined();
expect(result.contentWidth).toBeGreaterThanOrEqual(0);
});
it('should reflow card with media content (WCAG 1.4.10)', async () => {
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaAlt = 'Test image';
element.heading = 'Card with Media';
element.text = 'Card containing media that should reflow';
await element.updateComplete;
const result = testReflow(element, 320);
expect(result).toBeDefined();
expect(result.contentWidth).toBeGreaterThanOrEqual(0);
});
});
// CRITICAL: Layout and Structure Validation Tests
// These tests prevent layout issues like incorrect media positioning
describe('Layout and Structure Validation (Prevent Media Positioning Issues)', () => {
describe('Media Positioning', () => {
it('should position media on right when mediaPosition="right"', async () => {
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaAlt = 'Test';
element.mediaPosition = 'right';
await element.updateComplete;
// Card classes are applied to the host element (usa-card tag)
expect(element.classList.contains('usa-card--media-right')).toBe(true);
const media = element.querySelector('.usa-card__media');
expect(media).toBeTruthy();
});
it('should use flag layout class for side media', async () => {
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaPosition = 'right';
element.flagLayout = true;
await element.updateComplete;
// Card classes are applied to the host element (usa-card tag)
expect(element.classList.contains('usa-card--flag')).toBe(true);
});
it('should match USWDS reference structure for card with media', async () => {
element.heading = 'Test Card';
element.text = 'Test text';
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaPosition = 'right';
await element.updateComplete;
// Expected structure from USWDS:
//
//
//
const container = element.querySelector('.usa-card__container');
const header = element.querySelector('.usa-card__header');
const media = element.querySelector('.usa-card__media');
const body = element.querySelector('.usa-card__body');
expect(element.classList.contains('usa-card')).toBe(true);
expect(container).toBeTruthy();
expect(media).toBeTruthy();
expect(body).toBeTruthy();
// Verify nesting
expect(element.contains(container as Node)).toBe(true);
expect(container?.contains(media as Node)).toBe(true);
expect(container?.contains(body as Node)).toBe(true);
});
});
describe('Flag Layout Structure', () => {
it('should have proper flag layout classes', async () => {
element.flagLayout = true;
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
await element.updateComplete;
// Card classes are applied to the host element (usa-card tag)
expect(element.classList.contains('usa-card--flag')).toBe(true);
});
it('should NOT have flag class when flagLayout is false (unless mediaPosition is right)', async () => {
element.flagLayout = false;
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaPosition = 'inset'; // Not right
await element.updateComplete;
// Card classes are applied to the host element (usa-card tag)
// Note: mediaPosition='right' automatically enables flag layout
expect(element.classList.contains('usa-card--flag')).toBe(false);
});
});
describe('CSS Display Properties', () => {
it('should have block display on host element', async () => {
element.heading = 'Test Card';
await element.updateComplete;
// In jsdom, getComputedStyle won't return actual CSS values
// This test validates the element exists - Cypress will test actual display
const computedStyle = window.getComputedStyle(element);
expect(computedStyle).toBeTruthy();
// Note: In a real browser (Cypress), we would verify:
// expect(computedStyle.display).toBe('block');
});
});
describe('Visual Rendering Validation', () => {
it('should render card with media in DOM (visual tests in Cypress)', async () => {
element.heading = 'Test Card';
element.text = 'Test text';
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaPosition = 'right';
await element.updateComplete;
const media = element.querySelector('.usa-card__media');
const img = element.querySelector('.usa-card__img');
expect(element.classList.contains('usa-card')).toBe(true);
expect(media).toBeTruthy();
expect(img).toBeTruthy();
// CRITICAL: This structure validation prevents media positioning issues
// In a real browser (Cypress), we would also check:
// - Media appears on correct side (right/left)
// - Image dimensions are correct
// - Layout doesn't break on mobile
// Note: jsdom doesn't render, so visual checks are in Cypress tests
});
it('should render flag layout correctly', async () => {
element.heading = 'Flag Card';
element.text = 'Flag layout text';
element.mediaType = 'image';
element.mediaSrc = '/test.jpg';
element.mediaPosition = 'right';
element.flagLayout = true;
await element.updateComplete;
// Card classes are applied to the host element (usa-card tag)
expect(element.classList.contains('usa-card--flag')).toBe(true);
expect(element.classList.contains('usa-card--media-right')).toBe(true);
// CRITICAL: Flag layout structure correct
// Visual side-by-side layout verified in Cypress
});
});
describe('Header and Footer Positioning', () => {
it('should have header before body', async () => {
element.heading = 'Test Heading';
element.text = 'Test text';
await element.updateComplete;
const container = element.querySelector('.usa-card__container');
const header = element.querySelector('.usa-card__header');
const body = element.querySelector('.usa-card__body');
expect(container).toBeTruthy();
expect(header).toBeTruthy();
expect(body).toBeTruthy();
// Header should come before body
const children = Array.from(container?.children || []);
const headerIndex = children.indexOf(header as Element);
const bodyIndex = children.indexOf(body as Element);
expect(headerIndex).toBeLessThan(bodyIndex);
});
it('should have footer after body when present', async () => {
element.heading = 'Test';
element.text = 'Test text';
element.footerContent = 'Link';
await element.updateComplete;
const container = element.querySelector('.usa-card__container');
const body = element.querySelector('.usa-card__body');
const footer = element.querySelector('.usa-card__footer');
if (footer) {
const children = Array.from(container?.children || []);
const bodyIndex = children.indexOf(body as Element);
const footerIndex = children.indexOf(footer as Element);
expect(footerIndex).toBeGreaterThan(bodyIndex);
}
});
});
});
});