Core Concepts
Understanding these core concepts will help you get the most out of E2E Wrapper.
Architecture Overview
E2E Wrapper follows a layered architecture that abstracts framework-specific implementations behind a common interface:
┌─────────────────────────────────────┐
│ Your Tests │
│ (Framework-agnostic test code) │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ E2E Wrapper │
│ (Unified Interface) │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ Element Drivers │
│ (Framework-specific adapters) │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ Testing Frameworks │
│ (Detox, Appium, Playwright) │
└─────────────────────────────────────┘
Element Drivers
Element drivers are the bridge between E2E Wrapper's generic interface and framework-specific implementations.
IElementDriver Interface
All drivers implement the IElementDriver
interface:
interface IElementDriver {
isVisible(): Promise<boolean>
isEnabled(): Promise<boolean>
exists(): Promise<boolean>
waitForState(conditions: WaitCondition[], options: WaitOptions): Promise<boolean>
getFramework(): TestFramework
}
Built-in Drivers
DetoxElementDriver
- Purpose: Integrates with Detox for React Native testing
- Selector Support:
testId
,text
,accessibility
- Framework Methods: Access to Detox element methods via
getDriver()
const driver = new DetoxElementDriver({ testId: 'my-button' })
await driver.getDriver().tap() // Detox-specific method
AppiumElementDriver
- Purpose: Integrates with Appium/WebDriver for mobile automation
- Selector Support:
xpath
,id
,accessibility
,text
,className
- Framework Methods: Access to WebDriver element methods
const driver = new AppiumElementDriver({ xpath: '//button' }, webDriver)
await driver.getDriver().click() // WebDriver method
Custom Drivers
You can create custom drivers by extending BaseElementDriver
:
class MyCustomDriver extends BaseElementDriver {
constructor(private selector: ElementSelector, private myFramework: any) {
super()
}
async isVisible(): Promise<boolean> {
// Implement using your framework's API
return this.myFramework.isElementVisible(this.selector)
}
async isEnabled(): Promise<boolean> {
// Implement using your framework's API
return this.myFramework.isElementEnabled(this.selector)
}
// ... implement other required methods
getFramework(): TestFramework {
return TestFramework.CUSTOM
}
}
Builder Pattern
The builder pattern allows for flexible, readable test composition through method chaining.
Wait Builder
The WaitBuilder
class implements the builder pattern for wait conditions:
interface IWaitBuilder {
forVisible(options?: WaitOptions): IWaitBuilder
forEnabled(options?: WaitOptions): IWaitBuilder
forExists(options?: WaitOptions): IWaitBuilder
execute(): Promise<boolean>
}
Method Chaining
Methods can be chained in any order:
// All of these are equivalent
await element.wait().forVisible().forEnabled().execute()
await element.wait().forEnabled().forVisible().execute()
await element.wait().forExists().forVisible().forEnabled().execute()
Execution
The execute()
method:
- Processes all chained conditions
- Waits for each condition to be met
- Returns
true
if all conditions pass,false
if timeout occurs - May throw errors for framework-specific issues
Wait Conditions
Wait conditions define what state an element should be in before proceeding.
Built-in Conditions
VisibleCondition
Waits for an element to be visible on screen.
await element.wait().forVisible({ timeout: 10000 }).execute()
EnabledCondition
Waits for an element to be enabled/interactable.
await element.wait().forEnabled({ timeout: 5000 }).execute()
ExistsCondition
Waits for an element to exist in the DOM/view hierarchy.
await element.wait().forExists({ timeout: 3000 }).execute()
Custom Conditions
Create custom wait conditions by extending WaitCondition
:
class TextContainsCondition extends WaitCondition {
constructor(private expectedText: string) {
super()
}
async check(driver: IElementDriver): Promise<boolean> {
// Implement custom check logic
const actualText = await driver.getText() // Assuming this method exists
return actualText.includes(this.expectedText)
}
}
Element Selectors
Selectors define how to find elements in the UI.
Unified Selector Format
The ElementSelector
type supports multiple selector strategies:
type ElementSelector = {
testId?: string // Test identifier (preferred)
id?: string // Element ID
xpath?: string // XPath expression
text?: string // Visible text content
accessibility?: string // Accessibility identifier
className?: string // CSS class name
}
Framework Compatibility
Selector Type | Detox | Appium | Playwright | Cypress |
---|---|---|---|---|
testId | ✅ | ⚠️ | ✅ | ✅ |
id | ⚠️ | ✅ | ✅ | ✅ |
xpath | ❌ | ✅ | ✅ | ✅ |
text | ✅ | ✅ | ✅ | ✅ |
accessibility | ✅ | ✅ | ✅ | ✅ |
className | ❌ | ✅ | ✅ | ✅ |
Best Practices
- Prefer testId: Most reliable across frameworks
- Use accessibility: Good for cross-platform compatibility
- Avoid text: Can change and break tests
- XPath as last resort: Brittle and framework-dependent
Wait Options
Customize wait behavior with WaitOptions
:
interface WaitOptions {
timeout?: number // Maximum wait time in milliseconds (default: 5000)
interval?: number // Check interval in milliseconds (default: 100)
}
Usage Examples
// Custom timeout
await element.wait().forVisible({ timeout: 15000 }).execute()
// Custom interval (check more frequently)
await element.wait().forEnabled({ interval: 50 }).execute()
// Both options
await element.wait().forExists({
timeout: 10000,
interval: 200
}).execute()
Default Behavior
- Default timeout: 5000ms (5 seconds)
- Default interval: 100ms
- Timeout behavior: Returns
false
(does not throw)
Error Handling
E2E Wrapper uses different strategies for different types of errors:
Wait Timeouts
const result = await element.wait().forVisible().execute()
if (!result) {
console.log('Element did not become visible within timeout')
}
Framework Errors
try {
await element.wait().forVisible().execute()
} catch (error) {
console.error('Framework-specific error:', error)
}
Best Practices
- Always check return values from
execute()
- Use try-catch for framework-specific operations
- Implement retries for flaky tests
- Log useful debugging information
Type Safety
E2E Wrapper is built with TypeScript and provides full type safety:
Generic Type Parameters
// Type-safe access to framework-specific methods
const detoxWrapper: E2EWrapper<DetoxElementDriver> = createDetoxWrapper({...})
const appiumWrapper: E2EWrapper<AppiumElementDriver> = createAppiumWrapper({...})
// TypeScript ensures correct method calls
await detoxWrapper.getDriver().tap() // ✅ Detox method
await appiumWrapper.getDriver().click() // ✅ Appium method
Interface Benefits
- IntelliSense: Auto-completion in IDEs
- Compile-time checks: Catch errors before runtime
- Refactoring safety: Rename operations across codebase
- Documentation: Types serve as documentation
Framework Detection
E2E Wrapper can detect which framework is being used:
import { TestFramework } from 'e2e-wrapper'
const wrapper = createDetoxWrapper({ testId: 'button' })
const framework = wrapper.getFramework()
switch (framework) {
case TestFramework.DETOX:
// Detox-specific optimizations
break
case TestFramework.APPIUM:
// Appium-specific handling
break
case TestFramework.CUSTOM:
// Custom driver handling
break
}
This enables framework-specific optimizations while maintaining the unified interface.
Wait Builder Pattern
The library uses a fluent builder pattern for waiting conditions. This allows you to chain multiple conditions together:
await wrapper
.wait()
.forVisible()
.forEnabled()
.execute();
Basic Wait Conditions
Standard Conditions
forVisible()
- Wait for element to be visibleforEnabled()
- Wait for element to be enabledforExists()
- Wait for element to exist
Custom Conditions
The library provides powerful custom condition support that allows you to wait for specific element states that go beyond the basic visibility, enabled, and existence checks.
Built-in Custom Conditions
Wait for Class Name
Wait for an element to have a specific CSS class:
await wrapper
.wait()
.forCustom({ hasClassName: 'active' })
.execute();
This checks if the element's class
attribute contains the specified class name.
Wait for Attribute
Wait for an element to have a specific attribute, with optional value checking:
// Wait for attribute to exist (any value)
await wrapper
.wait()
.forCustom({ hasAttribute: { name: 'disabled' } })
.execute();
// Wait for attribute to have specific value
await wrapper
.wait()
.forCustom({ hasAttribute: { name: 'data-status', value: 'active' } })
.execute();
Wait for Text Content
Wait for an element to contain specific text:
await wrapper
.wait()
.forCustom({ hasText: 'Click me' })
.execute();
This checks if the element's text content contains the specified string.
Wait for Property
Wait for an element to have a specific property with a given value:
await wrapper
.wait()
.forCustom({ hasProperty: { name: 'checked', value: true } })
.execute();
This is useful for checking form element states like checkbox checked status.
Custom Predicate Functions
For maximum flexibility, you can provide your own custom predicate function:
await wrapper
.wait()
.forCustom({
custom: async (element, driver) => {
// Custom logic using the element and driver
const count = await driver.getAttribute(selector, 'data-count');
return parseInt(count || '0') > 5;
}
})
.execute();
The predicate function receives:
element
: The actual DOM/native elementdriver
: The element driver instance for additional operations
Chaining Custom Conditions
Custom conditions can be chained with standard conditions:
await wrapper
.wait()
.forVisible()
.forCustom({ hasClassName: 'loaded' })
.forEnabled()
.forCustom({ hasAttribute: { name: 'data-ready', value: 'true' } })
.execute();
Real-world Examples
Wait for Loading State
// Wait for loading spinner to disappear
await wrapper
.wait()
.forCustom({ hasClassName: 'loading' })
.execute();
// Then wait for content to be ready
await wrapper
.wait()
.forCustom({ hasAttribute: { name: 'data-loaded', value: 'true' } })
.execute();
Wait for Form Validation
// Wait for form field to be valid
await wrapper
.wait()
.forCustom({ hasClassName: 'valid' })
.forCustom({ hasAttribute: { name: 'aria-invalid', value: 'false' } })
.execute();
Wait for Dynamic Content
// Wait for element to contain specific text after API call
await wrapper
.wait()
.forCustom({ hasText: 'Data loaded successfully' })
.execute();
Complex Custom Conditions
// Wait for element to have multiple classes and specific data attribute
await wrapper
.wait()
.forCustom({
custom: async (element, driver) => {
const classes = await driver.getAttribute(selector, 'class');
const status = await driver.getAttribute(selector, 'data-status');
return classes?.includes('active') &&
classes?.includes('ready') &&
status === 'completed';
}
})
.execute();
Error Handling
Custom conditions include built-in error handling:
- If the element doesn't exist, the condition will fail
- If attribute/property access fails, the condition will fail gracefully
- Network timeouts and other errors are handled automatically
Configuration
Custom conditions support the same timeout and retry options as standard conditions:
await wrapper
.wait()
.forCustom(
{ hasClassName: 'loaded' },
{ timeout: 10000, interval: 500 }
)
.execute();
Element Selectors
The library supports various selector types:
id
- Element IDtestId
- Test ID (data-testid)text
- Text contentxpath
- XPath expressionclassName
- CSS class nameaccessibility
- Accessibility labelcustom
- Custom selector object
Framework Support
The library abstracts the underlying testing framework, supporting:
- Detox - React Native testing
- Appium - Mobile app testing
- Future: Playwright, Cypress, and other frameworks
Configuration Options
Wait Options
interface WaitOptions {
timeout?: number; // Maximum wait time (default: 5000ms)
interval?: number; // Polling interval (default: 100ms)
retries?: number; // Number of retries (default: 3)
}
Usage Examples
// Quick timeout
await wrapper.wait().forVisible({ timeout: 2000 }).execute();
// Slower polling
await wrapper.wait().forEnabled({ interval: 500 }).execute();
// More retries
await wrapper.wait().forExists({ retries: 5 }).execute();