CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-testcafe

Automated browser testing for the modern web development stack.

Pending
Overview
Eval results
Files

client-functions.mddocs/

Client Functions

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.

Capabilities

Basic Client Functions

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);
});

Client Functions with Dependencies

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);
});

DOM Manipulation

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();
});

Browser API Access

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);
});

Async Client Functions

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();
});

Error Handling

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');
});

Performance and Optimization

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

docs

assertions.md

browser-automation.md

client-functions.md

element-selection.md

index.md

programmatic-api.md

request-interception.md

user-roles.md

tile.json