/**
* Common Test Patterns and Setup Utilities
*
* Centralized utilities for common test setup patterns, mock configurations,
* and reusable test components across the WooCommerce Shipping test suite.
*/
import React from 'react';
import { render } from '@testing-library/react';
import type { RenderResult } from '@testing-library/react';
import { LabelPurchaseContext } from 'context/label-purchase';
import CurrencyFactory from '@woocommerce/currency';
import { SHIPMENT_TYPE } from 'utils/shipment';
import { address, destinationAddress, orderTestData } from './test-utils';
/**
* Common mock configurations for different test scenarios
*/
export interface MockConfiguration {
storeCurrency?: {
formatMoney?: ( amount: string ) => string;
[ key: string ]: unknown;
};
rates?: {
getSelectedRate: jest.Mock;
getSelectedRateOptions: jest.Mock;
selectRateOption: jest.Mock;
getCurrentRateOptions: jest.Mock;
isFetching: boolean;
errors: string[];
};
labels?: {
hasPurchasedLabel: jest.Mock;
getCurrentShipmentLabel: jest.Mock;
isPurchasing: boolean;
errors: string[];
};
shipment?: {
getShipmentOrigin: jest.Mock;
getShipmentPurchaseOrigin: jest.Mock;
getShipmentDestination: jest.Mock;
getCurrentShipmentDate: jest.Mock;
getCurrentShipmentIsReturn: jest.Mock;
isReturnShipment: jest.Mock;
getShipmentType: jest.Mock;
};
addresses?: {
origin?: Record< string, unknown >;
destination?: Record< string, unknown >;
};
order?: {
total: string;
total_shipping: string;
total_line_items_quantity: number;
line_items: {
id: number;
name: string;
quantity: number;
}[];
};
isReturn?: boolean;
isDomestic?: boolean;
hasValidAddresses?: boolean;
}
/**
* Enhanced mock factory for consistent test setups
*/
export class TestMockFactory {
private static defaultConfig: Required< MockConfiguration > = {
storeCurrency: CurrencyFactory( { code: 'USD' } ),
rates: {
getSelectedRate: jest.fn( () => null ),
getSelectedRateOptions: jest.fn( () => [] ),
selectRateOption: jest.fn(),
getCurrentRateOptions: jest.fn( () => [] ),
isFetching: false,
errors: [],
},
labels: {
hasPurchasedLabel: jest.fn( () => false ),
getCurrentShipmentLabel: jest.fn( () => ( {
isLegacy: false,
status: 'purchased',
} ) ),
isPurchasing: false,
errors: [],
},
shipment: {
getShipmentOrigin: jest.fn( () => address ),
getShipmentPurchaseOrigin: jest.fn( () => address ),
getShipmentDestination: jest.fn( () => destinationAddress ),
getCurrentShipmentDate: jest.fn( () => ( {
shippingDate: null,
estimatedDeliveryDate: null,
} ) ),
getCurrentShipmentIsReturn: jest.fn( () => false ),
isReturnShipment: jest.fn( () => false ),
getShipmentType: jest.fn( () => SHIPMENT_TYPE.OUTBOUND ),
},
addresses: {
origin: address,
destination: destinationAddress,
},
order: orderTestData,
isReturn: false,
isDomestic: true,
hasValidAddresses: true,
};
/**
* Create mock configuration for specific test scenarios
*/
static createMocks(
overrides: Partial< MockConfiguration > = {}
): Required< MockConfiguration > {
const config = {
...this.defaultConfig,
...overrides,
};
// Apply contextual overrides based on scenario flags
if ( config.isReturn ) {
config.shipment = {
...config.shipment,
getCurrentShipmentIsReturn: jest.fn( () => true ),
isReturnShipment: jest.fn( () => true ),
getShipmentType: jest.fn( () => SHIPMENT_TYPE.RETURN ),
};
// Return shipments don't have shipping dates
config.shipment.getCurrentShipmentDate = jest.fn( () => ( {
shippingDate: null,
estimatedDeliveryDate: null,
} ) );
}
if ( ! config.isDomestic ) {
config.addresses = {
origin: { ...address, country: 'US' },
destination: { ...destinationAddress, country: 'CA' },
};
}
if ( ! config.hasValidAddresses ) {
config.addresses = {
origin: { ...config.addresses.origin, isVerified: false },
destination: {
...config.addresses.destination,
isVerified: false,
},
};
}
return config;
}
/**
* Create mocks for outbound shipment scenarios
*/
static createOutboundMocks(
overrides: Partial< MockConfiguration > = {}
): Required< MockConfiguration > {
return this.createMocks( {
isReturn: false,
isDomestic: true,
...overrides,
} );
}
/**
* Create mocks for return shipment scenarios
*/
static createReturnMocks(
overrides: Partial< MockConfiguration > = {}
): Required< MockConfiguration > {
return this.createMocks( {
isReturn: true,
isDomestic: true,
...overrides,
} );
}
/**
* Create mocks for international shipment scenarios
*/
static createInternationalMocks(
overrides: Partial< MockConfiguration > = {}
): Required< MockConfiguration > {
return this.createMocks( {
isReturn: false,
isDomestic: false,
...overrides,
} );
}
/**
* Create mocks for error scenarios
*/
static createErrorMocks(
errorType: 'network' | 'api' | 'validation' = 'api'
): Required< MockConfiguration > {
const baseConfig = this.createMocks();
switch ( errorType ) {
case 'network':
return {
...baseConfig,
rates: {
...baseConfig.rates,
isFetching: false,
errors: [ 'Network connection failed' ],
},
};
case 'api':
return {
...baseConfig,
labels: {
...baseConfig.labels,
errors: [ 'API service unavailable' ],
},
};
case 'validation':
return {
...baseConfig,
hasValidAddresses: false,
addresses: {
origin: { ...address, address: '', isVerified: false },
destination: {
...destinationAddress,
address: '',
isVerified: false,
},
},
};
default:
return baseConfig;
}
}
}
/**
* Common test component patterns
*/
export class TestComponentPatterns {
/**
* Render component with LabelPurchaseContext
*/
static renderWithLabelPurchaseContext(
component: React.ReactElement,
contextValue?: Partial< MockConfiguration >
): RenderResult {
const mocks = TestMockFactory.createMocks( contextValue );
const contextWrapper = React.createElement(
LabelPurchaseContext.Provider,
{
value: {
storeCurrency: mocks.storeCurrency,
rates: mocks.rates,
labels: mocks.labels,
shipment: mocks.shipment,
orderItems: [],
hazmat: { hasHazmat: false },
packages: { getSelectedPackage: jest.fn() },
weight: { getTotalWeight: jest.fn() },
order: mocks.order ?? {},
dimensions: { getTotalDimensions: jest.fn() },
insurance: { getInsuranceValue: jest.fn() },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
},
component
);
return render( contextWrapper );
}
/**
* Render return shipment component with appropriate context
*/
static renderReturnShipmentComponent(
component: React.ReactElement,
overrides?: Partial< MockConfiguration >
): RenderResult {
const mocks = TestMockFactory.createReturnMocks( overrides );
return this.renderWithLabelPurchaseContext( component, mocks );
}
/**
* Render outbound shipment component with appropriate context
*/
static renderOutboundShipmentComponent(
component: React.ReactElement,
overrides?: Partial< MockConfiguration >
): RenderResult {
const mocks = TestMockFactory.createOutboundMocks( overrides );
return this.renderWithLabelPurchaseContext( component, mocks );
}
/**
* Create error boundary test wrapper
*/
static createErrorBoundaryWrapper(
onError: jest.Mock = jest.fn()
): React.ComponentType< { children: React.ReactNode } > {
return ( { children }: { children: React.ReactNode } ) => {
try {
return React.createElement( React.Fragment, null, children );
} catch ( error ) {
onError( error );
return React.createElement( 'div', null, 'Error caught' );
}
};
}
}
/**
* Common assertion patterns for tests
*/
export class TestAssertionPatterns {
/**
* Assert address field ordering for shipment types
*/
static assertAddressFieldOrder(
container: HTMLElement,
shipmentType: 'outbound' | 'return'
): void {
const fromLabel = container.querySelector( 'label[for*="from"]' );
const toLabel = container.querySelector( 'label[for*="to"]' );
expect( fromLabel ).toBeInTheDocument();
expect( toLabel ).toBeInTheDocument();
if ( shipmentType === 'return' ) {
expect( toLabel ).toHaveTextContent( 'Return to' );
} else {
expect( toLabel ).toHaveTextContent( 'Ship to' );
}
}
/**
* Assert shipping date field visibility
*/
static assertShippingDateVisibility(
container: HTMLElement,
shouldBeVisible: boolean
): void {
const shippingDateLabel = container.querySelector(
'label[for*="shipping-date"]'
);
if ( shouldBeVisible ) {
expect( shippingDateLabel ).toBeInTheDocument();
} else {
expect( shippingDateLabel ).not.toBeInTheDocument();
}
}
/**
* Assert error message display
*/
static assertErrorMessage(
container: HTMLElement,
expectedMessage: string | RegExp
): void {
const errorElement = container.querySelector(
'[role="alert"], .error, .notice-error'
);
expect( errorElement ).toBeInTheDocument();
expect( errorElement ).toHaveTextContent( expectedMessage );
}
/**
* Assert button state and accessibility
*/
static assertButtonState(
button: HTMLElement,
expectedState: {
enabled?: boolean;
text?: string | RegExp;
ariaLabel?: string;
}
): void {
if ( expectedState.enabled !== undefined ) {
if ( expectedState.enabled ) {
expect( button ).toBeEnabled();
} else {
expect( button ).toBeDisabled();
}
}
if ( expectedState.text ) {
expect( button ).toHaveTextContent( expectedState.text );
}
if ( expectedState.ariaLabel ) {
expect( button ).toHaveAttribute(
'aria-label',
expectedState.ariaLabel
);
}
}
/**
* Assert form validation errors
*/
static assertFormValidationErrors(
container: HTMLElement,
expectedErrors: Record< string, string | RegExp >
): void {
Object.entries( expectedErrors ).forEach(
( [ fieldName, errorMessage ] ) => {
const errorElement = container.querySelector(
`[data-field="${ fieldName }"] .error, [data-field="${ fieldName }"] + .error`
);
expect( errorElement ).toBeInTheDocument();
expect( errorElement ).toHaveTextContent( errorMessage );
}
);
}
}
/**
* Test data generators for consistent test data
*/
export class TestDataGenerators {
/**
* Generate address data with specific characteristics
*/
static createAddress(
overrides: {
country?: string;
isVerified?: boolean;
isResidential?: boolean;
hasCompany?: boolean;
} = {}
): Record< string, unknown > {
const baseAddress = {
...address,
name: 'Test Person',
company: overrides.hasCompany ? 'Test Company' : '',
address: '123 Test Street',
city: 'Test City',
state: 'CA',
postcode: '90210',
country: overrides.country ?? 'US',
email: 'test@example.com',
phone: '5551234567',
isVerified: overrides.isVerified ?? true,
};
if ( overrides.isResidential === false ) {
baseAddress.company = 'Test Business LLC';
baseAddress.name = 'Business Contact';
}
return baseAddress;
}
/**
* Generate order data for testing
*/
static createOrder(
overrides: {
itemCount?: number;
totalValue?: string;
shippingCost?: string;
isInternational?: boolean;
} = {}
): {
total: string;
total_shipping: string;
total_line_items_quantity: number;
line_items: {
id: number;
name: string;
quantity: number;
}[];
} & Record< string, unknown > {
return {
...orderTestData,
total: overrides.totalValue ?? '100.00',
total_shipping: overrides.shippingCost ?? '10.00',
total_line_items_quantity: overrides.itemCount ?? 2,
line_items: Array.from(
{ length: overrides.itemCount ?? 2 },
( _, i ) => ( {
id: i + 1,
name: `Test Item ${ i + 1 }`,
quantity: 1,
} )
),
};
}
/**
* Generate rate data for testing
*/
static createRate(
overrides: {
isReturn?: boolean;
carrier?: string;
service?: string;
price?: number;
} = {}
): {
id: string;
carrier: string;
service: string;
rate: number;
isReturn: boolean;
delivery_days: number;
currency: string;
} {
return {
id: 'test_rate_1',
carrier: overrides.carrier ?? 'USPS',
service: overrides.service ?? 'Ground',
rate: overrides.price ?? 9.99,
isReturn: overrides.isReturn ?? false,
delivery_days: 3,
currency: 'USD',
};
}
/**
* Generate shipment data for testing
*/
static createShipment(
overrides: {
id?: string;
type?: 'outbound' | 'return';
items?: {
id: number;
name: string;
quantity: number;
}[];
addresses?: {
origin?: Record< string, unknown >;
destination?: Record< string, unknown >;
};
} = {}
): Record< string, unknown > {
return {
id: overrides.id ?? 'shipment_1',
type: overrides.type ?? 'outbound',
items: overrides.items ?? [
{ id: 1, name: 'Test Item', quantity: 1 },
],
origin: overrides.addresses?.origin ?? this.createAddress(),
destination:
overrides.addresses?.destination ?? this.createAddress(),
status: 'ready',
};
}
}
/**
* Test cleanup utilities
*/
export class TestCleanupUtils {
/**
* Clear all mocks and restore original implementations
*/
static clearAllMocks(): void {
jest.clearAllMocks();
}
/**
* Reset mock implementations to defaults
*/
static resetMockImplementations(): void {
jest.resetAllMocks();
}
/**
* Clean up timers and async operations
*/
static cleanupTimers(): void {
jest.clearAllTimers();
jest.useRealTimers();
}
/**
* Full test cleanup
*/
static fullCleanup(): void {
this.clearAllMocks();
this.cleanupTimers();
}
}
/**
* Common test scenario configurations
*/
export const TestScenarios = {
/**
* Return label creation scenario
*/
returnLabelCreation: {
description: 'Return label creation with domestic addresses',
mocks: TestMockFactory.createReturnMocks(),
expectations: {
addressSwapped: true,
noShippingDate: true,
domesticOnly: true,
},
},
/**
* International shipment scenario (should be disabled for returns)
*/
internationalReturn: {
description: 'International return attempt (should be disabled)',
mocks: TestMockFactory.createInternationalMocks( { isReturn: true } ),
expectations: {
buttonDisabled: true,
showTooltip: true,
errorMessage: /domestic shipments/i,
},
},
/**
* Outbound shipment scenario
*/
outboundShipment: {
description: 'Standard outbound shipment',
mocks: TestMockFactory.createOutboundMocks(),
expectations: {
addressNormal: true,
hasShippingDate: true,
internationalAllowed: true,
},
},
/**
* Error handling scenario
*/
errorHandling: {
description: 'API error handling',
mocks: TestMockFactory.createErrorMocks( 'api' ),
expectations: {
showError: true,
allowRetry: true,
errorBoundaryTriggered: false,
},
},
};
/**
* Export commonly used test setup functions
* Note: This function should be called at the describe block level, not inside tests
*/
export const setupCommonTest = ( scenario: keyof typeof TestScenarios ) => {
const config = TestScenarios[ scenario ];
return config;
};
/**
* Common test hooks that should be used in describe blocks
*/
export const useCommonTestHooks = () => {
beforeEach( () => {
TestCleanupUtils.clearAllMocks();
} );
afterEach( () => {
TestCleanupUtils.fullCleanup();
} );
};