Unified testing framework for Reynard packages with comprehensive utilities, mocks, and configurations.
graph TB
subgraph "đź§Ş Testing Framework"
A[Test Utils] --> B[Config System]
A --> C[Mock System]
A --> D[Setup System]
A --> E[Assertion Utils]
subgraph "Configuration System"
B --> B1[Vitest Base Config]
B --> B2[Component Config]
B --> B3[Integration Config]
B --> B4[E2E Config]
B --> B5[Package Configs]
end
subgraph "Mock System"
C --> C1[Browser Mocks]
C --> C2[SolidJS Mocks]
C --> C3[External Mocks]
C --> C4[Media Mocks]
C --> C5[Canvas Mocks]
end
subgraph "Setup System"
D --> D1[Base Setup]
D --> D2[Component Setup]
D --> D3[AI Setup]
D --> D4[3D Setup]
D --> D5[Media Setup]
D --> D6[Gallery Setup]
end
subgraph "Assertion Utils"
E --> E1[Component Assertions]
E --> E2[Async Assertions]
E --> E3[DOM Utils]
E --> E4[Mock Utils]
end
end
subgraph "🌍 Monorepo Integration"
F[Vitest Workspace] --> G[95+ Packages]
F --> H[Global Queue System]
F --> I[Happy-DOM Environment]
F --> J[Coverage Reports]
end
subgraph "đź”§ i18n Testing"
K[i18n Testing] --> L[Package Orchestrator]
K --> M[ESLint Plugin]
K --> N[CI Checks]
K --> O[Validation]
end
A -->|Configures| F
A -->|Supports| K
flowchart TD
A[Package Test] --> B{Test Type?}
B -->|Component| C[Component Config]
B -->|Integration| D[Integration Config]
B -->|E2E| E[E2E Config]
B -->|Base| F[Base Config]
C --> G[Happy-DOM Setup]
D --> G
E --> G
F --> G
G --> H[Mock System]
H --> I[Browser APIs]
H --> J[SolidJS]
H --> K[External Libs]
H --> L[Media APIs]
I --> M[Test Execution]
J --> M
K --> M
L --> M
M --> N[Assertion Utils]
N --> O[Component Checks]
N --> P[Async Validation]
N --> Q[DOM Queries]
O --> R[Coverage Report]
P --> R
Q --> R
R --> S[Global Queue]
S --> T[95+ Packages]
T --> U[Monorepo Results]
- Unified Configuration: Standardized Vitest configs for all package types
- Unified DOM API: Consistent DOM testing across Vitest and Playwright environments
- Test Utilities: Common testing helpers and custom render functions
- Comprehensive Mocks: Browser APIs, SolidJS, and external libraries
- Assertion Utilities: Custom assertions for common testing scenarios
- Advanced Testing: Accessibility, performance monitoring, and complex interactions
- TypeScript First: Full type safety with excellent IntelliSense
npm install reynard-testing --save-dev// vitest.config.ts
import { createComponentTestConfig } from "@entropy-tamer/reynard-testing/config";
export default createComponentTestConfig("my-package");// my-component.test.tsx
import { renderWithProviders, expectComponentToRender } from '@entropy-tamer/reynard-testing';
test('renders without errors', () => {
const MyComponent = () => <div>Hello World</div>;
expectComponentToRender(MyComponent);
});// vitest-test.ts
import {
createVitestDOMAssertionsById,
setupVitestDOMFixture,
cleanupVitestDOMFixture,
DOM_TEST_DATA,
} from "@entropy-tamer/reynard-testing/dom";
describe("My Component", () => {
beforeEach(() => {
setupVitestDOMFixture();
});
afterEach(() => {
cleanupVitestDOMFixture();
});
test("should test DOM elements", async () => {
const element = await createVitestDOMAssertionsById(DOM_TEST_DATA.elements.visible);
await element.toBeVisible();
await element.toHaveTextContent("Visible Element");
});
});// playwright-test.ts
import { createPlaywrightDOMAssertionsById, DOM_TEST_PAGE_HTML, DOM_TEST_DATA } from "@entropy-tamer/reynard-testing/dom";
test("should test DOM elements in e2e", async ({ page }) => {
await page.setContent(DOM_TEST_PAGE_HTML);
const element = await createPlaywrightDOMAssertionsById(page, DOM_TEST_DATA.elements.visible);
await element.toBeVisible();
await element.toHaveTextContent("Visible Element");
});// my-test.ts
import { mockFetch, mockLocalStorage } from "@entropy-tamer/reynard-testing/mocks";
test("uses fetch", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: "test" }),
});
const result = await fetch("/api/test");
expect(result.ok).toBe(true);
});import { createBaseVitestConfig } from "@entropy-tamer/reynard-testing/config";
export default createBaseVitestConfig({
packageName: "my-package",
setupFiles: ["./src/test-setup.ts"],
coverageThresholds: {
branches: 90,
functions: 95,
lines: 95,
statements: 95,
},
});import { createComponentTestConfig } from "@entropy-tamer/reynard-testing/config";
export default createComponentTestConfig("my-component-package");import { createIntegrationTestConfig } from "@entropy-tamer/reynard-testing/config";
export default createIntegrationTestConfig("my-integration-package");The unified DOM API provides consistent DOM testing across both Vitest and Playwright environments, with shared test fixtures and advanced testing capabilities.
- Environment Agnostic: Works seamlessly in both Vitest (Happy DOM) and Playwright
- Shared Test Fixtures: Consistent HTML test pages across all environments
- Advanced Testing: Accessibility, performance monitoring, and complex interactions
- Type Safety: Full TypeScript support with comprehensive error messages
import {
createVitestDOMAssertionsById,
createPlaywrightDOMAssertionsById,
setupVitestDOMFixture,
cleanupVitestDOMFixture,
DOM_TEST_PAGE_HTML,
DOM_TEST_DATA,
} from "@entropy-tamer/reynard-testing/dom";describe("My Component Tests", () => {
beforeEach(() => {
setupVitestDOMFixture();
});
afterEach(() => {
cleanupVitestDOMFixture();
});
test("should test DOM elements", async () => {
const element = await createVitestDOMAssertionsById(DOM_TEST_DATA.elements.visible);
await element.toBeVisible();
await element.toHaveTextContent("Visible Element");
});
});test.describe("My E2E Tests", () => {
test.beforeEach(async ({ page }) => {
await page.setContent(DOM_TEST_PAGE_HTML);
});
test("should test DOM elements", async ({ page }) => {
const element = await createPlaywrightDOMAssertionsById(page, DOM_TEST_DATA.elements.visible);
await element.toBeVisible();
await element.toHaveTextContent("Visible Element");
});
});import { toHaveAccessibleName, toHaveAccessibleDescription, toHaveRole } from "@entropy-tamer/reynard-testing/dom";
test("should test accessibility", async () => {
const element = await createVitestDOMAssertionsById(DOM_TEST_DATA.buttons.named);
await toHaveAccessibleName(element, "Submit Form");
await toHaveRole(element, "button");
});import { measurePerformance, trackDOMMutations } from "@entropy-tamer/reynard-testing/dom";
test("should monitor performance", async () => {
const duration = await measurePerformance(async () => {
// Your operation
});
const mutationCount = await trackDOMMutations(async () => {
// Your DOM manipulation
});
});import { simulateDragAndDrop, simulateKeyPress } from "@entropy-tamer/reynard-testing/dom";
test("should handle complex interactions", async () => {
const draggable = await createVitestDOMAssertionsById(DOM_TEST_DATA.interactions.dragItem);
const dropZone = await createVitestDOMAssertionsById(DOM_TEST_DATA.interactions.dropZone);
await simulateDragAndDrop(draggable, dropZone);
await simulateKeyPress(draggable, "Enter");
});The unified API provides comprehensive test fixtures:
// Elements
DOM_TEST_DATA.elements.visible;
DOM_TEST_DATA.elements.hidden;
DOM_TEST_DATA.elements.invisible;
DOM_TEST_DATA.elements.transparent;
// Forms
DOM_TEST_DATA.forms.textInput;
DOM_TEST_DATA.forms.disabledInput;
DOM_TEST_DATA.forms.requiredInput;
// Buttons
DOM_TEST_DATA.buttons.focusable;
DOM_TEST_DATA.buttons.disabled;
DOM_TEST_DATA.buttons.named;
// Interactions
DOM_TEST_DATA.interactions.toggleTarget;
DOM_TEST_DATA.interactions.dragItem;
DOM_TEST_DATA.interactions.dropZone;
// Accessibility
DOM_TEST_DATA.accessibility.liveRegion;For detailed migration instructions from existing e2e tests, see MIGRATION_GUIDE.md.
import {
renderWithProviders,
renderWithTheme,
renderWithRouter
} from '@entropy-tamer/reynard-testing/utils';
// Render with all providers
renderWithProviders(() => <MyComponent />);
// Render with theme
renderWithTheme(() => <MyComponent />, { name: 'dark' });
// Render with router
renderWithRouter(() => <MyComponent />, '/dashboard');import { createMockFn, createMockResponse, createMockFile } from "@entropy-tamer/reynard-testing/utils";
// Create mock function
const mockFn = createMockFn();
// Create mock response
const response = createMockResponse({ data: "test" });
// Create mock file
const file = createMockFile("test.txt", "content");import { expectComponentToRender, expectPromiseToResolve, expectElementToHaveClass } from "@entropy-tamer/reynard-testing/utils";
// Component assertions
expectComponentToRender(MyComponent);
// Promise assertions
await expectPromiseToResolve(fetch("/api"));
// DOM assertions
expectElementToHaveClass(element, "active");import { mockFetch, mockLocalStorage, mockWebSocket } from "@entropy-tamer/reynard-testing/mocks";
// Mock fetch
mockFetch.mockResolvedValueOnce({ ok: true });
// Mock localStorage
mockLocalStorage.getItem.mockReturnValue("value");
// Mock WebSocket
const ws = new mockWebSocket();import { mockFabric, mockD3 } from "@entropy-tamer/reynard-testing/mocks";
// Mock Fabric.js
const canvas = new mockFabric.Canvas();
// Mock D3.js
const selection = mockD3.select("body");{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
},
"devDependencies": {
"reynard-testing": "workspace:*",
"vitest": "^3.0.0"
}
}{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"reynard-testing": "workspace:*",
"vitest": "^3.0.0"
}
}- Component packages: Use
createComponentTestConfig - Utility packages: Use
createUtilityTestConfig - Integration packages: Use
createIntegrationTestConfig
// Good: Use custom render functions
renderWithProviders(() => <MyComponent />);
// Avoid: Manual provider setup
render(() => (
<ThemeProvider>
<RouterProvider>
<MyComponent />
</RouterProvider>
</ThemeProvider>
));// Good: Use provided mocks
import { mockFetch } from "@entropy-tamer/reynard-testing/mocks";
// Avoid: Manual fetch mocking
global.fetch = vi.fn();// Good: Descriptive test names
test("should render component with correct theme colors", () => {
// test implementation
});
// Avoid: Vague test names
test("works", () => {
// test implementation
});import { renderWithProviders } from '@entropy-tamer/reynard-testing';
import { ReynardProvider } from 'reynard-themes';
describe('ThemedComponent', () => {
test('renders with light theme', () => {
renderWithProviders(
() => <ThemedComponent />,
{
providers: [
[ReynardProvider, { defaultTheme: 'light' }]
]
}
);
expect(screen.getByTestId('component')).toHaveClass('theme-light');
});
test('switches theme on button click', async () => {
const themeModule = createTheme({ defaultTheme: 'light' });
renderWithProviders(
() => <ThemeToggle />,
{
providers: [
[ThemeProvider, { value: themeModule }]
]
}
);
const toggleButton = screen.getByRole('button', { name: /toggle theme/i });
await userEvent.click(toggleButton);
expect(screen.getByTestId('component')).toHaveClass('theme-dark');
});
});import { waitFor } from '@solidjs/testing-library';
import { mockFetch } from '@entropy-tamer/reynard-testing/mocks';
describe('AsyncComponent', () => {
test('loads data and displays it', async () => {
const mockData = { id: 1, name: 'Test Item' };
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockData)
});
render(() => <AsyncComponent />);
// Wait for loading to complete
await waitFor(() => {
expect(screen.getByText('Test Item')).toBeInTheDocument();
});
expect(mockFetch).toHaveBeenCalledWith('/api/data');
});
test('handles loading states', () => {
mockFetch.mockImplementation(() =>
new Promise(resolve => setTimeout(resolve, 100))
);
render(() => <AsyncComponent />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('handles error states', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
render(() => <AsyncComponent />);
await waitFor(() => {
expect(screen.getByText('Error loading data')).toBeInTheDocument();
});
});
});import { userEvent } from '@testing-library/user-event';
describe('ContactForm', () => {
test('submits form with valid data', async () => {
const user = userEvent.setup();
const mockSubmit = vi.fn();
render(() => <ContactForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/name/i), 'John Doe');
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
await user.type(screen.getByLabelText(/message/i), 'Hello world');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(mockSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello world'
});
});
test('shows validation errors', async () => {
const user = userEvent.setup();
render(() => <ContactForm />);
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText('Name is required')).toBeInTheDocument();
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
});import { formatDate, validateEmail, sanitizeInput } from "reynard-core";
describe("formatDate", () => {
test("formats date correctly", () => {
const date = new Date("2024-01-15T10:30:00Z");
expect(formatDate(date, "YYYY-MM-DD")).toBe("2024-01-15");
expect(formatDate(date, "MM/DD/YYYY")).toBe("01/15/2024");
expect(formatDate(date, "relative")).toBe("2 hours ago");
});
test("handles invalid dates", () => {
expect(formatDate(new Date("invalid"), "YYYY-MM-DD")).toBe("Invalid Date");
});
});
describe("validateEmail", () => {
test.each([
["user@example.com", true],
["test.email@domain.co.uk", true],
["invalid-email", false],
["@domain.com", false],
["user@", false],
])("validates email %s as %s", (email, expected) => {
expect(validateEmail(email)).toBe(expected);
});
});import { retryWithBackoff, batchExecute } from "reynard-core";
describe("retryWithBackoff", () => {
test("retries failed operations", async () => {
let attempts = 0;
const failingOperation = async () => {
attempts++;
if (attempts < 3) throw new Error("Temporary failure");
return "success";
};
const result = await retryWithBackoff(failingOperation, 3, 100);
expect(result).toBe("success");
expect(attempts).toBe(3);
});
test("fails after max retries", async () => {
const failingOperation = async () => {
throw new Error("Permanent failure");
};
await expect(retryWithBackoff(failingOperation, 2, 100)).rejects.toThrow("Permanent failure");
});
});
describe("batchExecute", () => {
test("executes operations in batches", async () => {
const operations = [
() => Promise.resolve("result1"),
() => Promise.resolve("result2"),
() => Promise.resolve("result3"),
() => Promise.resolve("result4"),
];
const results = await batchExecute(operations, 2);
expect(results).toEqual(["result1", "result2", "result3", "result4"]);
});
});describe('UserDashboard Integration', () => {
test('complete user workflow', async () => {
const user = userEvent.setup();
// Mock API responses
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ user: { id: 1, name: 'John' } })
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ posts: [{ id: 1, title: 'Post 1' }] })
});
renderWithProviders(() => <UserDashboard />);
// Wait for user data to load
await waitFor(() => {
expect(screen.getByText('Welcome, John')).toBeInTheDocument();
});
// Wait for posts to load
await waitFor(() => {
expect(screen.getByText('Post 1')).toBeInTheDocument();
});
// Test user interaction
await user.click(screen.getByRole('button', { name: /create post/i }));
await user.type(screen.getByLabelText(/title/i), 'New Post');
await user.type(screen.getByLabelText(/content/i), 'Post content');
await user.click(screen.getByRole('button', { name: /save/i }));
// Verify post was created
await waitFor(() => {
expect(screen.getByText('New Post')).toBeInTheDocument();
});
});
});describe('ErrorBoundary', () => {
test('catches and displays errors', () => {
const ThrowError = () => {
throw new Error('Test error');
};
render(() => (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<ThrowError />
</ErrorBoundary>
));
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
test('recovers from errors', async () => {
const [shouldThrow, setShouldThrow] = createSignal(true);
const ConditionalError = () => {
if (shouldThrow()) throw new Error('Test error');
return <div>Recovered</div>;
};
render(() => (
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ConditionalError />
</ErrorBoundary>
));
expect(screen.getByText('Error occurred')).toBeInTheDocument();
setShouldThrow(false);
await waitFor(() => {
expect(screen.getByText('Recovered')).toBeInTheDocument();
});
});
});import { performance } from 'perf_hooks';
describe('Performance Tests', () => {
test('component renders within time limit', () => {
const start = performance.now();
render(() => <LargeDataTable data={largeDataset} />);
const end = performance.now();
const renderTime = end - start;
expect(renderTime).toBeLessThan(100); // Should render in under 100ms
});
test('handles large datasets efficiently', () => {
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random()
}));
render(() => <VirtualizedList items={largeDataset} />);
// Should only render visible items
const renderedItems = screen.getAllByTestId(/list-item/);
expect(renderedItems.length).toBeLessThan(100);
});
});describe('Memory Leak Tests', () => {
test('cleans up event listeners', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
const { unmount } = render(() => <ComponentWithListeners />);
expect(addEventListenerSpy).toHaveBeenCalled();
unmount();
expect(removeEventListenerSpy).toHaveBeenCalled();
});
test('cleans up timers', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout');
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
const { unmount } = render(() => <ComponentWithTimers />);
expect(setTimeoutSpy).toHaveBeenCalled();
unmount();
expect(clearTimeoutSpy).toHaveBeenCalled();
});
});import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Accessibility Tests', () => {
test('has no accessibility violations', async () => {
const { container } = render(() => <AccessibleComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('supports keyboard navigation', async () => {
const user = userEvent.setup();
render(() => <KeyboardNavigableComponent />);
const firstButton = screen.getByRole('button', { name: /first/i });
firstButton.focus();
await user.keyboard('{Tab}');
expect(screen.getByRole('button', { name: /second/i })).toHaveFocus();
});
test('announces changes to screen readers', () => {
render(() => <AnnouncingComponent />);
const statusRegion = screen.getByRole('status');
expect(statusRegion).toHaveAttribute('aria-live', 'polite');
});
});import {
mockFetch,
mockLocalStorage,
mockWebSocket,
mockIntersectionObserver
} from '@entropy-tamer/reynard-testing/mocks';
describe('Component with External Dependencies', () => {
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
mockLocalStorage.clear();
});
test('works with mocked dependencies', async () => {
// Setup fetch mock
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: 'test' })
});
// Setup localStorage mock
mockLocalStorage.setItem('user', JSON.stringify({ id: 1 }));
// Setup WebSocket mock
const mockWs = mockWebSocket.createMock();
mockWs.onmessage({ data: JSON.stringify({ type: 'update' }) });
render(() => <ComponentWithDependencies />);
// Test interactions
await waitFor(() => {
expect(screen.getByText('test')).toBeInTheDocument();
});
});
});import { createMockUser, createMockPost } from '@entropy-tamer/reynard-testing/fixtures';
describe('User Components', () => {
test('renders user profile', () => {
const mockUser = createMockUser({
name: 'John Doe',
email: 'john@example.com',
role: 'admin'
});
render(() => <UserProfile user={mockUser} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
});
test('renders user posts', () => {
const mockPosts = [
createMockPost({ title: 'Post 1', author: 'John' }),
createMockPost({ title: 'Post 2', author: 'John' })
];
render(() => <UserPosts posts={mockPosts} />);
expect(screen.getByText('Post 1')).toBeInTheDocument();
expect(screen.getByText('Post 2')).toBeInTheDocument();
});
});- Components: 85%+ branches, 90%+ functions/lines/statements
- Utilities: 90%+ branches, 95%+ functions/lines/statements
- Integration: 75%+ branches, 80%+ functions/lines/statements
- Mock not working: Ensure mocks are imported before use
- Coverage too low: Check if test files are excluded from coverage
- Type errors: Ensure TypeScript types are properly configured
- Check existing package configurations for examples
- Review test utilities documentation
- Consult Vitest documentation for advanced usage