UNPKG

@narottamdev/component-test-utils

Version:

Universal testing utilities for component libraries across different frameworks

637 lines (628 loc) 22.2 kB
class ReactAdapter { constructor() { try { this.testingLibrary = require('@testing-library/react'); } catch (error) { throw new Error('React Testing Library is required for React components. Please install @testing-library/react'); } } render(component, props) { const { render } = this.testingLibrary; const result = render(component, props); return { element: result.container.firstChild, container: result.container, rerender: result.rerender, unmount: result.unmount, debug: result.debug }; } cleanup() { const { cleanup } = this.testingLibrary; cleanup(); } fireEvent(element, eventType, options) { const { fireEvent } = this.testingLibrary; const eventMethod = fireEvent[eventType]; if (typeof eventMethod === 'function') { eventMethod(element, options); } else { throw new Error('Event type ' + eventType + ' is not supported'); } } async waitFor(callback, options) { const { waitFor } = this.testingLibrary; return waitFor(callback, options); } } class VueAdapter { constructor() { try { this.testUtils = require('@testing-library/vue'); } catch (error) { throw new Error('Vue Testing Library is required for Vue components. Please install @testing-library/vue'); } } render(component, props) { const { render } = this.testUtils; const result = render(component, { props }); return { element: result.container.firstChild, container: result.container, rerender: (newProps) => result.rerender({ props: newProps }), unmount: result.unmount, debug: result.debug }; } cleanup() { const { cleanup } = this.testUtils; if (cleanup) { cleanup(); } } fireEvent(element, eventType, options) { const { fireEvent } = this.testUtils; const eventMethod = fireEvent[eventType]; if (typeof eventMethod === 'function') { eventMethod(element, options); } else { throw new Error('Event type ' + eventType + ' is not supported'); } } async waitFor(callback, options) { const { waitFor } = this.testUtils; return waitFor(callback, options); } } class AngularAdapter { constructor() { try { const { TestBed } = require('@angular/testing'); this.testBed = TestBed; } catch (error) { throw new Error('Angular Testing utilities are required for Angular components. Please install @angular/testing'); } } render(component, props) { const fixture = this.testBed.createComponent(component); if (props) { Object.assign(fixture.componentInstance, props); } fixture.detectChanges(); return { element: fixture.nativeElement, container: fixture.nativeElement.parentElement || fixture.nativeElement, rerender: (newProps) => { if (newProps) { Object.assign(fixture.componentInstance, newProps); } fixture.detectChanges(); }, unmount: () => { fixture.destroy(); }, debug: () => { console.log(fixture.nativeElement.outerHTML); } }; } cleanup() { this.testBed.resetTestingModule(); } fireEvent(element, eventType, options) { const event = new Event(eventType, { bubbles: options?.bubbles || true, cancelable: options?.cancelable || true, ...options }); element.dispatchEvent(event); } async waitFor(callback, options) { const timeout = options?.timeout || 5000; const startTime = Date.now(); return new Promise((resolve, reject) => { const check = async () => { try { await callback(); resolve(); } catch (error) { if (Date.now() - startTime > timeout) { reject(new Error('waitFor timed out after ' + timeout + 'ms')); } else { setTimeout(check, 100); } } }; check(); }); } } class VanillaAdapter { constructor() { this.container = null; } ensureContainer() { if (!this.container) { if (typeof document === 'undefined') { throw new Error('VanillaAdapter requires a DOM environment. Make sure you are running in a browser or have jsdom configured.'); } this.container = document.createElement('div'); this.container.setAttribute('data-testid', 'test-container'); document.body.appendChild(this.container); } return this.container; } render(component, props) { const container = this.ensureContainer(); let element; if (typeof component === 'function') { element = component(props); } else if (component instanceof HTMLElement) { element = component; } else if (typeof component === 'string') { const wrapper = document.createElement('div'); wrapper.innerHTML = component; element = wrapper.firstElementChild; } else { throw new Error('Invalid component type for vanilla adapter'); } container.innerHTML = ''; container.appendChild(element); return { element, container: container, rerender: (newProps) => { if (typeof component === 'function') { const newElement = component(newProps); container.innerHTML = ''; container.appendChild(newElement); } }, unmount: () => { container.innerHTML = ''; }, debug: () => { console.log(container.innerHTML); } }; } cleanup() { if (this.container) { this.container.innerHTML = ''; if (this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.container = null; } } fireEvent(element, eventType, options) { const event = new Event(eventType, { bubbles: options?.bubbles || true, cancelable: options?.cancelable || true, ...options }); element.dispatchEvent(event); } async waitFor(callback, options) { const timeout = options?.timeout || 5000; const startTime = Date.now(); return new Promise((resolve, reject) => { const check = async () => { try { await callback(); resolve(); } catch (error) { if (Date.now() - startTime > timeout) { reject(new Error('waitFor timed out after ' + timeout + 'ms')); } else { setTimeout(check, 100); } } }; check(); }); } } class TestRenderer { constructor(config) { this.config = config; this.adapter = this.createAdapter(config.framework); } createAdapter(framework) { switch (framework) { case 'react': return new ReactAdapter(); case 'vue': return new VueAdapter(); case 'angular': return new AngularAdapter(); case 'vanilla': return new VanillaAdapter(); default: throw new Error('Unsupported framework: ' + framework); } } render(component, props) { return this.adapter.render(component, props); } fireEvent(element, eventType, options) { return this.adapter.fireEvent(element, eventType, options); } async waitFor(callback, options) { return this.adapter.waitFor(callback, options); } cleanup() { return this.adapter.cleanup(); } } function createTestUtils(config) { return new TestRenderer(config); } class AccessibilityTester { static async testAccessibility(element, options) { const violations = []; const checks = [ () => this.checkForAltText(element), () => this.checkForAriaLabels(element), () => this.checkColorContrast(element), () => this.checkKeyboardNavigation(element), () => this.checkHeadingStructure(element) ]; for (const check of checks) { try { const result = await check(); if (result) violations.push(result); } catch (error) { violations.push({ rule: 'accessibility-check-error', impact: 'serious', message: error?.message || 'Unknown error' }); } } return violations; } static checkForAltText(element) { const images = element.querySelectorAll('img'); for (const img of Array.from(images)) { if (!img.getAttribute('alt') && !img.getAttribute('aria-label')) { return { rule: 'image-alt', impact: 'serious', message: 'Images must have alternative text', element: img }; } } return null; } static checkForAriaLabels(element) { const interactive = element.querySelectorAll('button, input, select, textarea, [role="button"]'); for (const el of Array.from(interactive)) { const hasLabel = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.textContent?.trim(); if (!hasLabel) { return { rule: 'aria-label', impact: 'serious', message: 'Interactive elements must have accessible names', element: el }; } } return null; } static checkColorContrast(element) { const computedStyle = window.getComputedStyle(element); const color = computedStyle.color; const backgroundColor = computedStyle.backgroundColor; if (color === backgroundColor) { return { rule: 'color-contrast', impact: 'serious', message: 'Insufficient color contrast', element }; } return null; } static checkKeyboardNavigation(element) { const focusable = element.querySelectorAll('a, button, input, select, textarea, [tabindex]'); for (const el of Array.from(focusable)) { const tabIndex = el.getAttribute('tabindex'); if (tabIndex && parseInt(tabIndex) > 0) { return { rule: 'tabindex', impact: 'moderate', message: 'Avoid positive tabindex values', element: el }; } } return null; } static checkHeadingStructure(element) { const headings = element.querySelectorAll('h1, h2, h3, h4, h5, h6'); let lastLevel = 0; for (const heading of Array.from(headings)) { const level = parseInt(heading.tagName.charAt(1)); if (level > lastLevel + 1) { return { rule: 'heading-order', impact: 'moderate', message: 'Heading levels should not skip levels', element: heading }; } lastLevel = level; } return null; } } function testAccessibility(element, options) { return AccessibilityTester.testAccessibility(element, options); } class PerformanceTester { static async measureRenderPerformance(renderFn, options) { const iterations = options?.iterations || 1; const renderTimes = []; for (let i = 0; i < iterations; i++) { const startTime = performance.now(); await renderFn(); const endTime = performance.now(); renderTimes.push(endTime - startTime); } const averageRenderTime = renderTimes.reduce((sum, time) => sum + time, 0) / renderTimes.length; const metrics = { renderTime: averageRenderTime, iterations }; if ('memory' in performance) { metrics.memoryUsage = performance.memory.usedJSHeapSize; } return metrics; } static async measureRerenderPerformance(rerenderFn, options) { const iterations = options?.iterations || 10; const rerenderTimes = []; for (let i = 0; i < iterations; i++) { const startTime = performance.now(); await rerenderFn(); const endTime = performance.now(); rerenderTimes.push(endTime - startTime); } return rerenderTimes.reduce((sum, time) => sum + time, 0) / rerenderTimes.length; } static measureMemoryUsage() { if ('memory' in performance) { return performance.memory.usedJSHeapSize; } return undefined; } static startPerformanceProfile(label) { if (performance.mark) { performance.mark(label + '-start'); } } static endPerformanceProfile(label) { if (performance.mark && performance.measure) { performance.mark(label + '-end'); performance.measure(label, label + '-start', label + '-end'); const entries = performance.getEntriesByName(label); if (entries.length > 0) { return entries[entries.length - 1].duration; } } return undefined; } static async waitForStablePerformance(testFn, options) { const maxAttempts = options?.maxAttempts || 10; const threshold = options?.threshold || 0.1; const measurements = []; for (let i = 0; i < maxAttempts; i++) { const startTime = performance.now(); await testFn(); const endTime = performance.now(); measurements.push(endTime - startTime); if (measurements.length >= 3) { const recent = measurements.slice(-3); const avg = recent.reduce((sum, val) => sum + val, 0) / recent.length; const variance = recent.every(val => Math.abs(val - avg) / avg < threshold); if (variance) { return; } } } console.warn('Performance did not stabilize within maximum attempts'); } } function measureRenderPerformance(renderFn, options) { return PerformanceTester.measureRenderPerformance(renderFn, options); } function measureRerenderPerformance(rerenderFn, options) { return PerformanceTester.measureRerenderPerformance(rerenderFn, options); } class ComponentAssertions { static toBeInDocument(element) { if (!element || !document.body.contains(element)) { throw new Error('Expected element to be in the document'); } } static toBeVisible(element) { if (!element) { throw new Error('Expected element to exist'); } const style = window.getComputedStyle(element); const isVisible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; if (!isVisible) { throw new Error('Expected element to be visible'); } } static toHaveTextContent(element, text) { if (!element) { throw new Error('Expected element to exist'); } const textContent = element.textContent || ''; if (!textContent.includes(text)) { throw new Error('Expected element to have text content ' + text + ', but got ' + textContent); } } static toHaveAttribute(element, attr, value) { if (!element) { throw new Error('Expected element to exist'); } if (!element.hasAttribute(attr)) { throw new Error('Expected element to have attribute ' + attr); } if (value !== undefined) { const actualValue = element.getAttribute(attr); if (actualValue !== value) { throw new Error('Expected attribute ' + attr + ' to have value ' + value + ', but got ' + actualValue); } } } static toHaveClass(element, className) { if (!element) { throw new Error('Expected element to exist'); } if (!element.classList.contains(className)) { throw new Error('Expected element to have class ' + className); } } static toHaveStyle(element, styles) { if (!element) { throw new Error('Expected element to exist'); } const computedStyle = window.getComputedStyle(element); for (const [property, expectedValue] of Object.entries(styles)) { const actualValue = computedStyle.getPropertyValue(property); if (actualValue !== expectedValue.toString()) { throw new Error('Expected element to have style ' + property + ': ' + expectedValue + ', but got ' + actualValue); } } } static toBeDisabled(element) { if (!element) { throw new Error('Expected element to exist'); } const isDisabled = element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true'; if (!isDisabled) { throw new Error('Expected element to be disabled'); } } static toBeEnabled(element) { if (!element) { throw new Error('Expected element to exist'); } const isDisabled = element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true'; if (isDisabled) { throw new Error('Expected element to be enabled'); } } static toHaveFocus(element) { if (!element) { throw new Error('Expected element to exist'); } if (document.activeElement !== element) { throw new Error('Expected element to have focus'); } } static toBeChecked(element) { if (!element) { throw new Error('Expected element to exist'); } const input = element; if (input.type !== 'checkbox' && input.type !== 'radio') { throw new Error('Expected element to be a checkbox or radio input'); } if (!input.checked) { throw new Error('Expected element to be checked'); } } static toHaveValue(element, value) { if (!element) { throw new Error('Expected element to exist'); } const input = element; if (input.value !== value.toString()) { throw new Error('Expected element to have value ' + value + ', but got ' + input.value); } } } const customMatchers = { toBeInDocument(received) { try { ComponentAssertions.toBeInDocument(received); return { pass: true, message: () => 'Expected element not to be in document' }; } catch (error) { return { pass: false, message: () => error.message }; } }, toBeVisible(received) { try { ComponentAssertions.toBeVisible(received); return { pass: true, message: () => 'Expected element not to be visible' }; } catch (error) { return { pass: false, message: () => error.message }; } }, toHaveTextContent(received, text) { try { ComponentAssertions.toHaveTextContent(received, text); return { pass: true, message: () => 'Expected element not to have text content ' + text }; } catch (error) { return { pass: false, message: () => error.message }; } }, toHaveAttribute(received, attr, value) { try { ComponentAssertions.toHaveAttribute(received, attr, value); return { pass: true, message: () => 'Expected element not to have attribute ' + attr }; } catch (error) { return { pass: false, message: () => error.message }; } }, toHaveClass(received, className) { try { ComponentAssertions.toHaveClass(received, className); return { pass: true, message: () => 'Expected element not to have class ' + className }; } catch (error) { return { pass: false, message: () => error.message }; } }, toHaveStyle(received, styles) { try { ComponentAssertions.toHaveStyle(received, styles); return { pass: true, message: () => 'Expected element not to have specified styles' }; } catch (error) { return { pass: false, message: () => error.message }; } } }; export { AccessibilityTester, AngularAdapter, ComponentAssertions, PerformanceTester, ReactAdapter, TestRenderer, VanillaAdapter, VueAdapter, createTestUtils, customMatchers, measureRenderPerformance, measureRerenderPerformance, testAccessibility }; //# sourceMappingURL=index.esm.js.map