Skip to main content

Test Package (@schwab/test)

Overview

The @schwab/test package provides comprehensive testing utilities, configurations, and frameworks for the Charles Schwab monorepo. It includes Jest configurations, mock utilities, test helpers, and automated testing infrastructure that ensures consistent testing practices across all applications and packages.

Architecture

Core Testing Configurations

1. Jest Base Configuration

Jest Base Configuration
// jest.base.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@schwab/test/setup'],
moduleNameMapping: {
'^@schwab/(.*)$': '<rootDir>/packages/$1/src',
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', {
tsconfig: {
jsx: 'react-jsx'
}
}],
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};

2. Mock Utilities

Comprehensive Mock System
// src/mock/base/Mockery.ts
export class Mockery {
/**
* Mocks the evaluateStaticFlag response
*/
static mockEvaluateStaticFlag(flags: Record<string, boolean> | undefined): void {
jest.doMock('@schwab/utilities/flags', () => ({
evaluateStaticFlag: () => Promise.resolve(flags),
}));
}

/**
* Mocks the evaluateDynamicFlag response
*/
static mockEvaluateDynamicFlag(flags: Record<string, boolean> | undefined): void {
jest.doMock('@schwab/utilities/flags', () => ({
evaluateDynamicFlag: () => Promise.resolve(flags),
}));
}

/**
* Mock Next.js router
*/
static mockNextRouter(overrides = {}) {
const mockRouter = {
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
pathname: '/',
query: {},
asPath: '/',
...overrides,
};

jest.doMock('next/router', () => ({
useRouter: () => mockRouter,
}));

return mockRouter;
}

/**
* Mock API responses
*/
static mockApiResponse<T>(data: T, isOk: boolean = true) {
return {
isOk,
data: isOk ? data : null,
error: isOk ? null : 'Mock API Error',
cacheTags: ['mock-tag'],
};
}
}

Test Helper Utilities

1. Component Testing Helpers

React Component Test Utilities
// src/helpers/componentTestHelpers.ts
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';

interface CustomRenderOptions extends RenderOptions {
withProviders?: boolean;
routerProps?: Record<string, any>;
}

export function renderWithProviders(
ui: ReactElement,
options: CustomRenderOptions = {}
) {
const { withProviders = true, routerProps = {}, ...renderOptions } = options;

function Wrapper({ children }: { children: React.ReactNode }) {
if (!withProviders) {
return <>{children}</>;
}

return (
<MockProviders routerProps={routerProps}>
{children}
</MockProviders>
);
}

return {
...render(ui, { wrapper: Wrapper, ...renderOptions }),
};
}

export function MockProviders({
children,
routerProps = {}
}: {
children: React.ReactNode;
routerProps?: Record<string, any>;
}) {
const mockRouter = {
pathname: '/',
query: {},
asPath: '/',
push: jest.fn(),
replace: jest.fn(),
...routerProps,
};

return (
<RouterContext.Provider value={mockRouter}>
<ThemeProvider>
{children}
</ThemeProvider>
</RouterContext.Provider>
);
}

2. API Testing Utilities

API Test Helpers
// src/helpers/apiTestHelpers.ts
export class ApiTestHelper {
/**
* Create mock fetch response
*/
static createMockResponse<T>(
data: T,
status: number = 200,
headers: Record<string, string> = {}
): Promise<Response> {
return Promise.resolve({
ok: status >= 200 && status < 300,
status,
headers: new Headers(headers),
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
} as Response);
}

/**
* Mock global fetch
*/
static mockGlobalFetch(responses: Record<string, any>) {
const mockFetch = jest.fn().mockImplementation((url: string) => {
const response = responses[url];
if (response) {
return this.createMockResponse(response.data, response.status);
}
return this.createMockResponse(null, 404);
});

global.fetch = mockFetch;
return mockFetch;
}

/**
* Restore global fetch
*/
static restoreGlobalFetch() {
global.fetch = originalFetch;
}
}

3. Form Testing Utilities

Form Test Helpers
// src/helpers/formTestHelpers.ts
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

export class FormTestHelper {
/**
* Fill form fields by label
*/
static async fillFormField(labelText: string, value: string) {
const user = userEvent.setup();
const field = screen.getByLabelText(labelText);
await user.clear(field);
await user.type(field, value);
}

/**
* Submit form and wait for completion
*/
static async submitForm(formTestId?: string) {
const form = formTestId
? screen.getByTestId(formTestId)
: screen.getByRole('form');

fireEvent.submit(form);

await waitFor(() => {
// Wait for form submission to complete
});
}

/**
* Fill complete form data
*/
static async fillForm(formData: Record<string, string>) {
const user = userEvent.setup();

for (const [label, value] of Object.entries(formData)) {
await this.fillFormField(label, value);
}
}

/**
* Test form validation
*/
static async testFormValidation(
invalidData: Record<string, string>,
expectedErrors: string[]
) {
await this.fillForm(invalidData);
await this.submitForm();

for (const error of expectedErrors) {
expect(screen.getByText(error)).toBeInTheDocument();
}
}
}

Custom Jest Matchers

1. Component-Specific Matchers

Custom Jest Matchers
// src/matchers/componentMatchers.ts
expect.extend({
toHaveAriaLabel(received, expected) {
const pass = received.getAttribute('aria-label') === expected;

if (pass) {
return {
message: () => `Expected element not to have aria-label "${expected}"`,
pass: true,
};
} else {
return {
message: () => `Expected element to have aria-label "${expected}"`,
pass: false,
};
}
},

toHaveValidSEO(received) {
const title = received.querySelector('title');
const metaDescription = received.querySelector('meta[name="description"]');

const pass = title && metaDescription && title.textContent.length > 0;

if (pass) {
return {
message: () => 'Expected element not to have valid SEO',
pass: true,
};
} else {
return {
message: () => 'Expected element to have valid SEO (title and meta description)',
pass: false,
};
}
},

toBeAccessible(received) {
// Simplified accessibility check
const hasAriaLabel = received.getAttribute('aria-label');
const hasRole = received.getAttribute('role');
const hasTabIndex = received.getAttribute('tabindex') !== '-1';

const pass = hasAriaLabel || hasRole || hasTabIndex;

return {
message: () => pass
? 'Expected element not to be accessible'
: 'Expected element to be accessible (needs aria-label, role, or proper tabindex)',
pass,
};
},
});

2. API Response Matchers

API Response Matchers
expect.extend({
toBeValidApiResponse(received) {
const hasIsOk = typeof received.isOk === 'boolean';
const hasData = 'data' in received;
const hasError = 'error' in received;

const pass = hasIsOk && hasData && hasError;

return {
message: () => pass
? 'Expected not to be a valid API response'
: 'Expected to be a valid API response with isOk, data, and error properties',
pass,
};
},

toHaveSuccessfulApiResponse(received) {
const pass = received.isOk === true && received.data !== null;

return {
message: () => pass
? 'Expected API response not to be successful'
: 'Expected API response to be successful (isOk: true, data not null)',
pass,
};
},
});

Integration Test Patterns

1. Database Integration Tests

Database Integration Testing
// src/integration/databaseTests.ts
export class DatabaseTestHelper {
/**
* Setup test database
*/
static async setupTestDatabase() {
// Initialize test database
await initializeTestDB();
}

/**
* Cleanup test data
*/
static async cleanupTestData() {
// Clean up test data after each test
await cleanupDB();
}

/**
* Seed test data
*/
static async seedTestData(fixtures: Record<string, any[]>) {
for (const [table, data] of Object.entries(fixtures)) {
await insertTestData(table, data);
}
}

/**
* Test database operations
*/
static async testDatabaseOperation<T>(
operation: () => Promise<T>,
expectedResult: T
) {
const result = await operation();
expect(result).toEqual(expectedResult);
}
}

2. E2E Test Utilities

End-to-End Test Helpers
// src/e2e/e2eTestHelpers.ts
export class E2ETestHelper {
/**
* Page object model base
*/
static createPageObject(page: any) {
return {
goto: (url: string) => page.goto(url),

fillForm: async (formData: Record<string, string>) => {
for (const [selector, value] of Object.entries(formData)) {
await page.fill(`[name="${selector}"]`, value);
}
},

submitForm: async (formSelector: string = 'form') => {
await page.click(`${formSelector} [type="submit"]`);
},

waitForNavigation: () => page.waitForNavigation(),

takeScreenshot: (name: string) =>
page.screenshot({ path: `screenshots/${name}.png` }),
};
}

/**
* Test user flows
*/
static async testUserFlow(
page: any,
steps: Array<{
action: string;
selector?: string;
value?: string;
assertion?: string;
}>
) {
for (const step of steps) {
switch (step.action) {
case 'click':
await page.click(step.selector);
break;
case 'fill':
await page.fill(step.selector, step.value);
break;
case 'navigate':
await page.goto(step.value);
break;
case 'assert':
await expect(page.locator(step.selector)).toBeVisible();
break;
}
}
}
}

Performance Testing

1. Performance Test Utilities

Performance Testing
// src/performance/performanceTests.ts
export class PerformanceTestHelper {
/**
* Measure function execution time
*/
static async measureExecutionTime<T>(
operation: () => Promise<T>,
maxTimeMs: number = 1000
): Promise<{ result: T; executionTime: number }> {
const startTime = performance.now();
const result = await operation();
const executionTime = performance.now() - startTime;

expect(executionTime).toBeLessThan(maxTimeMs);

return { result, executionTime };
}

/**
* Memory usage testing
*/
static measureMemoryUsage<T>(operation: () => T): T {
const initialMemory = process.memoryUsage();
const result = operation();
const finalMemory = process.memoryUsage();

const memoryDiff = {
rss: finalMemory.rss - initialMemory.rss,
heapUsed: finalMemory.heapUsed - initialMemory.heapUsed,
};

console.log('Memory usage difference:', memoryDiff);
return result;
}

/**
* Load testing simulation
*/
static async simulateLoad(
operation: () => Promise<any>,
concurrentRequests: number = 10,
iterations: number = 100
) {
const results = [];

for (let i = 0; i < iterations; i++) {
const promises = Array(concurrentRequests)
.fill(null)
.map(() => this.measureExecutionTime(operation));

const batchResults = await Promise.all(promises);
results.push(...batchResults);
}

const averageTime = results.reduce((sum, r) => sum + r.executionTime, 0) / results.length;
const maxTime = Math.max(...results.map(r => r.executionTime));

return { averageTime, maxTime, totalRequests: results.length };
}
}

Test Data Factories

1. Mock Data Factories

Test Data Factories
// src/factories/testDataFactories.ts
export class TestDataFactory {
/**
* Create test user data
*/
static createUser(overrides: Partial<User> = {}): User {
return {
id: '123',
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '(555) 123-4567',
...overrides,
};
}

/**
* Create test component props
*/
static createComponentProps<T>(
baseProps: T,
overrides: Partial<T> = {}
): T {
return {
...baseProps,
...overrides,
};
}

/**
* Create test API response
*/
static createApiResponse<T>(
data: T,
isOk: boolean = true,
error: string | null = null
) {
return {
isOk,
data: isOk ? data : null,
error: isOk ? null : error,
cacheTags: ['test-tag'],
};
}

/**
* Create test form data
*/
static createFormData(data: Record<string, string | string[]>): FormData {
const formData = new FormData();

Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => formData.append(key, v));
} else {
formData.append(key, value);
}
});

return formData;
}
}

Test Coverage and Reporting

1. Coverage Configuration

Coverage Configuration
// jest.coverage.config.js
module.exports = {
...baseConfig,
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html', 'json'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
'!src/**/__tests__/**',
'!src/**/__mocks__/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'src/components/': {
branches: 85,
functions: 85,
lines: 85,
statements: 85,
},
},
};

Continuous Integration

1. CI Test Configuration

GitHub Actions Test Workflow
# .github/workflows/test.yml
name: Test Suite

on:
pull_request:
push:
branches: [main, develop]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run linter
run: pnpm lint

- name: Run type checking
run: pnpm type-check

- name: Run unit tests
run: pnpm test --coverage

- name: Run integration tests
run: pnpm test:integration

- name: Upload coverage reports
uses: codecov/codecov-action@v3

The Test package provides comprehensive testing infrastructure that ensures code quality, reliability, and maintainability across the Charles Schwab monorepo through standardized testing practices, utilities, and automation.