Automated browser testing for the modern web development stack.
—
TestCafe's ClientFunction feature allows you to execute custom JavaScript code in the browser context and return results to the test context, enabling access to browser APIs and DOM manipulation that goes beyond standard TestCafe actions.
Execute JavaScript code in the browser and return results to tests.
/**
* Creates a function that executes in browser context
* @param fn - JavaScript function to execute in browser
* @param options - Configuration options for the client function
* @returns ClientFunction that can be called from tests
*/
function ClientFunction(fn: Function, options?: ClientFunctionOptions): ClientFunction;
interface ClientFunction {
/** Execute the client function and return result */
(): Promise<any>;
/** Create a new client function with additional options */
with(options: ClientFunctionOptions): ClientFunction;
}
interface ClientFunctionOptions {
/** Object containing dependencies for the client function */
dependencies?: {[key: string]: any};
/** Execution timeout in milliseconds */
timeout?: number;
}Usage Examples:
import { ClientFunction } from 'testcafe';
fixture('Client Functions')
.page('https://example.com');
// Simple client function
const getPageTitle = ClientFunction(() => document.title);
const getWindowSize = ClientFunction(() => ({
width: window.innerWidth,
height: window.innerHeight
}));
const getCurrentUrl = ClientFunction(() => window.location.href);
test('Basic client functions', async t => {
const title = await getPageTitle();
const windowSize = await getWindowSize();
const url = await getCurrentUrl();
await t.expect(title).contains('Example');
await t.expect(windowSize.width).gt(0);
await t.expect(url).contains('example.com');
});
// Client function with return value
const countElements = ClientFunction((selector) => {
return document.querySelectorAll(selector).length;
});
test('Count elements', async t => {
const divCount = await countElements('div');
const buttonCount = await countElements('button');
await t.expect(divCount).gt(0);
await t.expect(buttonCount).gte(1);
});Pass data from test context to client functions using dependencies.
/**
* Client function with dependencies
* @param fn - Function with parameters matching dependency keys
* @param options - Options including dependencies object
* @returns ClientFunction with injected dependencies
*/
function ClientFunction(
fn: (...args: any[]) => any,
options: { dependencies: {[key: string]: any} }
): ClientFunction;Usage Examples:
// Client function with dependencies
const searchElements = ClientFunction((searchText, tagName) => {
const elements = document.querySelectorAll(tagName);
const matches = [];
for (let element of elements) {
if (element.textContent.includes(searchText)) {
matches.push({
text: element.textContent,
tagName: element.tagName,
id: element.id
});
}
}
return matches;
}, {
dependencies: {
searchText: 'example text',
tagName: 'div'
}
});
test('Client function with dependencies', async t => {
const results = await searchElements;
await t.expect(results.length).gte(0);
if (results.length > 0) {
await t.expect(results[0].text).contains('example text');
}
});
// Dynamic dependencies
const dynamicFunction = ClientFunction((className, attribute) => {
const elements = document.querySelectorAll(`.${className}`);
return Array.from(elements).map(el => el.getAttribute(attribute));
});
test('Dynamic dependencies', async t => {
// Different calls with different dependencies
const classNames = await dynamicFunction.with({
dependencies: { className: 'menu-item', attribute: 'data-id' }
})();
const urls = await dynamicFunction.with({
dependencies: { className: 'external-link', attribute: 'href' }
})();
await t.expect(classNames.length).gte(0);
await t.expect(urls.length).gte(0);
});Use client functions to manipulate the DOM directly.
// Client functions for DOM manipulation
const setElementValue = ClientFunction((selector, value) => {
const element = document.querySelector(selector);
if (element) {
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
});
const triggerCustomEvent = ClientFunction((selector, eventType, eventData) => {
const element = document.querySelector(selector);
if (element) {
const event = new CustomEvent(eventType, { detail: eventData });
element.dispatchEvent(event);
}
});
const addCSSClass = ClientFunction((selector, className) => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => el.classList.add(className));
});Usage Examples:
test('DOM manipulation', async t => {
// Set input value directly
await setElementValue('#hidden-input', 'test-value');
// Verify value was set
const inputValue = await ClientFunction(() =>
document.querySelector('#hidden-input').value
)();
await t.expect(inputValue).eql('test-value');
// Trigger custom events
await triggerCustomEvent('#custom-element', 'customEvent', {
data: 'test-data'
});
// Add CSS classes
await addCSSClass('.highlight-target', 'highlighted');
// Verify class was added
const hasClass = await ClientFunction(() =>
document.querySelector('.highlight-target').classList.contains('highlighted')
)();
await t.expect(hasClass).ok();
});Access browser APIs not available through standard TestCafe actions.
// Access various browser APIs
const getLocalStorage = ClientFunction((key) => {
return localStorage.getItem(key);
});
const setLocalStorage = ClientFunction((key, value) => {
localStorage.setItem(key, value);
});
const getSessionStorage = ClientFunction((key) => {
return sessionStorage.getItem(key);
});
const getCookies = ClientFunction(() => {
return document.cookie;
});
const getGeolocation = ClientFunction(() => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation not supported'));
return;
}
navigator.geolocation.getCurrentPosition(
position => resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude
}),
error => reject(error)
);
});
});Usage Examples:
test('Browser API access', async t => {
// Test localStorage
await setLocalStorage('testKey', 'testValue');
const storedValue = await getLocalStorage('testKey');
await t.expect(storedValue).eql('testValue');
// Test sessionStorage
const sessionValue = await getSessionStorage('existingKey');
console.log('Session value:', sessionValue);
// Test cookies
const cookies = await getCookies();
await t.expect(cookies).typeOf('string');
// Test geolocation (with user permission)
try {
const location = await getGeolocation();
await t.expect(location.latitude).typeOf('number');
await t.expect(location.longitude).typeOf('number');
} catch (error) {
console.log('Geolocation not available:', error.message);
}
});
// Test browser capabilities
const getBrowserInfo = ClientFunction(() => ({
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
screenWidth: screen.width,
screenHeight: screen.height
}));
test('Browser information', async t => {
const browserInfo = await getBrowserInfo();
await t.expect(browserInfo.userAgent).typeOf('string');
await t.expect(browserInfo.cookieEnabled).typeOf('boolean');
await t.expect(browserInfo.screenWidth).gt(0);
});Handle asynchronous operations in client functions.
// Async client function
const waitForElement = ClientFunction((selector, timeout = 5000) => {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element.textContent);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element.textContent);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
});
const fetchData = ClientFunction((url) => {
return fetch(url)
.then(response => response.json())
.then(data => data)
.catch(error => ({ error: error.message }));
});Usage Examples:
test('Async client functions', async t => {
// Wait for dynamic element
try {
await t.click('#load-content-button');
const elementText = await waitForElement('.dynamic-content', 10000);
await t.expect(elementText).contains('loaded');
} catch (error) {
console.log('Dynamic element not loaded:', error.message);
}
// Fetch data in browser context
const apiData = await fetchData('/api/test-data');
if (apiData.error) {
console.log('API error:', apiData.error);
} else {
await t.expect(apiData).typeOf('object');
}
});
// Async client function with polling
const waitForCondition = ClientFunction((checkFn, timeout = 5000) => {
return new Promise((resolve, reject) => {
const start = Date.now();
function check() {
try {
const result = checkFn();
if (result) {
resolve(result);
return;
}
} catch (error) {
// Continue polling on error
}
if (Date.now() - start > timeout) {
reject(new Error('Condition not met within timeout'));
return;
}
setTimeout(check, 100);
}
check();
});
});
test('Polling client function', async t => {
await t.click('#start-process-button');
const result = await waitForCondition(() => {
const status = document.querySelector('.process-status');
return status && status.textContent === 'complete';
}, 15000);
await t.expect(result).ok();
});Handle errors in client functions and provide fallbacks.
// Client function with error handling
const safeExecute = ClientFunction((operation) => {
try {
return operation();
} catch (error) {
return { error: error.message };
}
});
const robustGetElement = ClientFunction((selector) => {
try {
const element = document.querySelector(selector);
if (!element) {
return { error: 'Element not found', selector };
}
return {
exists: true,
text: element.textContent,
visible: element.offsetParent !== null,
tagName: element.tagName
};
} catch (error) {
return { error: error.message, selector };
}
});Usage Examples:
test('Client function error handling', async t => {
// Safe execution with error handling
const result1 = await safeExecute(() => {
return document.querySelector('#existing-element').textContent;
});
if (result1.error) {
console.log('Error accessing element:', result1.error);
} else {
await t.expect(result1).typeOf('string');
}
// Robust element access
const elementInfo = await robustGetElement('#maybe-missing-element');
if (elementInfo.error) {
console.log('Element access failed:', elementInfo.error);
// Fallback behavior
await t.navigateTo('/alternative-page');
} else {
await t.expect(elementInfo.exists).ok();
await t.expect(elementInfo.text).typeOf('string');
}
});
// Retry logic for client functions
const retryClientFunction = async (clientFn, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
return await clientFn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
};
test('Client function retry', async t => {
const unstableFunction = ClientFunction(() => {
// Simulate intermittent failure
if (Math.random() < 0.7) {
throw new Error('Random failure');
}
return 'success';
});
const result = await retryClientFunction(unstableFunction, 5);
await t.expect(result).eql('success');
});Best practices for client function performance and reliability.
// Optimized client functions
const batchElementInfo = ClientFunction((selectors) => {
return selectors.map(selector => {
const element = document.querySelector(selector);
return element ? {
selector,
text: element.textContent,
visible: element.offsetParent !== null,
bounds: element.getBoundingClientRect()
} : { selector, exists: false };
});
});
const cachedClientFunction = (() => {
let cache = new Map();
return ClientFunction((key, computeFn) => {
if (cache.has(key)) {
return cache.get(key);
}
const result = computeFn();
cache.set(key, result);
return result;
});
})();Usage Examples:
test('Optimized client functions', async t => {
// Batch multiple element queries
const selectors = ['.header', '.content', '.footer'];
const elementsInfo = await batchElementInfo(selectors);
elementsInfo.forEach(info => {
if (info.exists) {
console.log(`${info.selector}: ${info.text}`);
}
});
// Cached computation
const expensiveResult = await cachedClientFunction('heavy-calc', () => {
// Simulate expensive calculation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.sqrt(i);
}
return result;
});
await t.expect(expensiveResult).gt(0);
// Second call uses cached result
const cachedResult = await cachedClientFunction('heavy-calc', () => {
throw new Error('Should not execute');
});
await t.expect(cachedResult).eql(expensiveResult);
});Install with Tessl CLI
npx tessl i tessl/npm-testcafe