Implementation:DevExpress Testcafe AssertionExecutor
| Knowledge Sources | |
|---|---|
| Domains | Testing, Web_Automation |
| Last Updated | 2026-02-12 04:00 GMT |
Overview
Concrete implementation of assertion retry logic in TestCafe through the `AssertionExecutor` class, which handles automatic re-evaluation of assertions with dynamic values.
Description
The `AssertionExecutor` class implements the core retry mechanism for TestCafe assertions. It detects when assertion operands are re-executable (Selector properties, ClientFunctions), enters a retry loop with 200ms delays between attempts, and uses configurable timeouts (defaulting to the `assertionTimeout` configuration option). Internally, it uses Chai assertion functions for the actual comparison logic. This class is not exposed to users; it operates behind the scenes when assertions are executed.
Usage
Internal framework component. Users don't interact with `AssertionExecutor` directly; it's invoked automatically when assertions created via `t.expect()` are executed.
Code Reference
Source Location
- Repository: testcafe
- File: src/assertions/executor.ts
- Lines: 12-103
Signature
class AssertionExecutor {
constructor(
command: AssertionCommand,
timeout: number,
callsite: object
)
async execute(): Promise<void>
private async _evaluateOperands(): Promise<{ actual: any, expected: any }>
private async _executeAssertion(actual: any, expected: any): Promise<void>
private _shouldRetry(error: Error): boolean
private async _retryAssertion(): Promise<void>
}
// Internal configuration
const ASSERTION_RETRY_DELAY = 200 // milliseconds
Import
// Internal class, not exported
// Used internally by TestCafe when executing assertion commands
// Users interact with assertions via t.expect():
test('Example', async t => {
// AssertionExecutor runs behind the scenes
await t.expect(Selector('h1').textContent).eql('Welcome');
});
I/O Contract
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
| command | AssertionCommand | Yes | Command object containing assertion details (type, operands, options) |
| timeout | number | Yes | Maximum time in milliseconds to retry the assertion |
| callsite | object | Yes | Source location information for error reporting |
Outputs
| Name | Type | Description |
|---|---|---|
| result | Promise<void> | Resolves if assertion passes, rejects with error if fails |
Usage Examples
Internal Operation (Conceptual)
// This is how AssertionExecutor works internally (not user code)
// User writes:
await t.expect(Selector('.item').count).gte(5);
// Framework internally:
const command = {
type: 'assertion',
assertionType: 'gte',
actual: SelectorPromise { /* .count property */ },
expected: 5,
message: null,
options: { timeout: 3000 }
};
const executor = new AssertionExecutor(
command,
3000, // timeout
callsite
);
await executor.execute();
// Internally retries every 200ms for up to 3000ms
// until Selector('.item').count >= 5
Retry Behavior Example
// User test code
fixture`Retry Logic Demo`
.page`https://example.com`;
test('Wait for items to load', async t => {
// Click triggers AJAX request to load items
await t.click('#load-items-button');
// AssertionExecutor automatically retries this assertion
// Timeline:
// t=0ms: count = 0, assertion fails, schedule retry
// t=200ms: count = 0, assertion fails, schedule retry
// t=400ms: count = 3, assertion fails, schedule retry
// t=600ms: count = 5, assertion PASSES ✓
await t.expect(Selector('.item').count).gte(5);
});
Static vs Re-executable Values
test('Retry behavior differences', async t => {
const staticValue = 10;
const dynamicValue = Selector('.counter').textContent;
// Static comparison - fails immediately, no retry
// (both operands are static values)
try {
await t.expect(staticValue).eql(20);
} catch (e) {
// Fails instantly
}
// Dynamic comparison - retries until passes or timeout
// (actual is a Selector property - re-executable)
await t.expect(dynamicValue).eql('10');
// Retries every 200ms until counter text equals '10'
});
Timeout Configuration
test('Custom timeout affects retry duration', async t => {
// Default timeout (e.g., 3000ms from config)
await t.expect(Selector('.fast-element').visible).ok();
// Retries for up to 3000ms with 200ms delays
// Custom timeout (10000ms)
await t.expect(Selector('.slow-element').visible).ok('', {
timeout: 10000
});
// Retries for up to 10000ms with 200ms delays
// Maximum ~50 retry attempts
});
Error Reporting
test('Assertion failure after timeout', async t => {
// If element never appears, AssertionExecutor fails after timeout
try {
await t.expect(Selector('.non-existent').visible).ok('', {
timeout: 2000
});
} catch (error) {
// Error includes:
// - Expected: true
// - Actual: false (last evaluated value)
// - Timeout: 2000ms
// - Callsite: line number in test
console.log(error.message);
// "Expected .non-existent to be visible, but it was not.
// Assertion failed after 2000ms."
}
});
Integration with Chai
// AssertionExecutor uses Chai internally for assertion logic
// User writes:
await t.expect(selector.count).eql(5);
// AssertionExecutor internally:
// 1. Evaluates: actualValue = await selector.count
// 2. Calls Chai: chai.expect(actualValue).to.equal(5)
// 3. If Chai throws, catches error and retries
// 4. If Chai succeeds, assertion complete
// This provides rich assertion types:
await t.expect(value).eql(expected); // deep equality
await t.expect(value).typeOf('string'); // type check
await t.expect(value).within(1, 10); // range check
await t.expect(value).match(/pattern/); // regex match
// All implemented via Chai assertion library
Retry Delay Constant
// The 200ms retry delay is hardcoded in AssertionExecutor
// For 3000ms timeout:
// - Maximum retry attempts: 3000 / 200 = 15 attempts
// - Actual attempts may be fewer if assertion passes early
// - Last attempt made even if timeout expired (always try at least once)
// Example timeline with 1000ms timeout:
// t=0ms: Attempt 1 - FAIL
// t=200ms: Attempt 2 - FAIL
// t=400ms: Attempt 3 - FAIL
// t=600ms: Attempt 4 - FAIL
// t=800ms: Attempt 5 - FAIL
// t=1000ms: Attempt 6 (final) - FAIL → throw TimeoutError