jest-e2e
Version:
A powerful Jest + Puppeteer E2E testing framework with built-in device automation, data builders, and CLI
270 lines (244 loc) • 9.89 kB
JavaScript
// Device wrapper for clean data-testid interactions and CSS selectors
import { stepLogger } from './step-logger.js';
const smartSelector = (selector) => {
// Common HTML element names that should be treated as CSS selectors
const htmlElements = ['html', 'body', 'head', 'div', 'span', 'p', 'a', 'img', 'ul', 'li', 'ol',
'table', 'tr', 'td', 'th', 'form', 'input', 'button', 'textarea', 'select',
'option', 'label', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'nav', 'header',
'footer', 'section', 'article', 'main', 'aside'];
// If it starts with common CSS selector patterns, use as-is
if (selector.startsWith('.') || // Class selector (.class)
selector.startsWith('#') || // ID selector (#id)
selector.startsWith('[') || // Attribute selector ([attr="value"])
selector.includes('>') || // Child combinator (div > span)
selector.includes(' ') || // Descendant combinator (div span)
selector.includes(':') || // Pseudo selectors (input:focus)
selector.includes('*') || // Universal selector
htmlElements.includes(selector.toLowerCase())) { // HTML element names
return selector;
}
// Otherwise, treat as data-testid value
return `[data-testid="${selector}"]`;
};
const device = {
// Navigation
navigate: async (url, options = {}) => {
stepLogger.step('Navigating', `to ${url}`);
const result = await page.goto(url, options);
return result;
},
// Interactions - supports both data-testid values and CSS selectors
click: async (selector, options = {}) => {
const displaySelector = selector.length > 30 ? selector.substring(0, 30) + '...' : selector;
stepLogger.step('Clicking', `"${displaySelector}"`);
const result = await page.click(smartSelector(selector), options);
return result;
},
type: async (selector, text, options = {}) => {
const displaySelector = selector.length > 30 ? selector.substring(0, 30) + '...' : selector;
const displayText = text.length > 20 ? text.substring(0, 20) + '...' : text;
stepLogger.step('Typing', `"${displayText}" into "${displaySelector}"`);
const result = await page.type(smartSelector(selector), text, options);
return result;
},
select: async (selector, value, options = {}) => {
const displaySelector = selector.length > 30 ? selector.substring(0, 30) + '...' : selector;
stepLogger.step('Selecting', `"${value}" from "${displaySelector}"`);
const result = await page.select(smartSelector(selector), value, options);
return result;
},
// Waiting
waitFor: async (selector, options = {}) => {
const displaySelector = selector.length > 30 ? selector.substring(0, 30) + '...' : selector;
stepLogger.step('Waiting for', `"${displaySelector}"`);
const result = await page.waitForSelector(smartSelector(selector), options);
return result;
},
// Element queries
get: (selector) => {
stepLogger.step('Getting element', `"${selector}"`);
return page.$(smartSelector(selector));
},
getAll: (selector) => {
stepLogger.step('Getting all elements', `"${selector}"`);
return page.$$(smartSelector(selector));
},
getText: async (selector) => {
stepLogger.step('Getting text from', `"${selector}"`);
return page.$eval(smartSelector(selector), el => el.textContent);
},
getValue: async (selector) => {
stepLogger.step('Getting value from', `"${selector}"`);
return page.$eval(smartSelector(selector), el => el.value);
},
// Element state checks
exists: async (selector) => {
stepLogger.step('Checking if exists', `"${selector}"`);
const element = await page.$(smartSelector(selector));
return element !== null;
},
isVisible: async (selector) => {
stepLogger.step('Checking if visible', `"${selector}"`);
const element = await page.$(smartSelector(selector));
return element ? await element.isIntersectingViewport() : false;
},
// Fluent expectation API with .not support
expect: (selector) => {
const resolvedSelector = smartSelector(selector);
const createAssertions = (negate = false) => ({
// Text content assertions
toContain: async (expectedText) => {
stepLogger.step('Verifying text', `"${selector}" ${negate ? 'does not contain' : 'contains'} "${expectedText}"`);
const text = await page.$eval(resolvedSelector, el => el.textContent);
if (negate) {
expect(text).not.toContain(expectedText);
} else {
expect(text).toContain(expectedText);
}
},
toHaveText: async (expectedText) => {
stepLogger.step('Verifying exact text', `"${selector}" ${negate ? 'does not equal' : 'equals'} "${expectedText}"`);
const text = await page.$eval(resolvedSelector, el => el.textContent.trim());
if (negate) {
expect(text).not.toBe(expectedText);
} else {
expect(text).toBe(expectedText);
}
},
// Visibility assertions
toBeVisible: async () => {
stepLogger.step('Verifying', `"${selector}" ${negate ? 'is not visible' : 'is visible'}`);
const element = await page.$(resolvedSelector);
if (negate) {
if (element) {
const isVisible = await element.isIntersectingViewport();
expect(isVisible).toBe(false);
} else {
expect(element).toBe(null); // Element doesn't exist, so not visible
}
} else {
expect(element).toBeTruthy();
const isVisible = await element.isIntersectingViewport();
expect(isVisible).toBe(true);
}
},
// Existence assertions
toExist: async () => {
stepLogger.step('Verifying', `"${selector}" ${negate ? 'does not exist' : 'exists'}`);
const element = await page.$(resolvedSelector);
if (negate) {
expect(element).toBe(null);
} else {
expect(element).toBeTruthy();
}
},
// Value assertions (for inputs)
toHaveValue: async (expectedValue) => {
stepLogger.step('Verifying value', `"${selector}" ${negate ? 'does not equal' : 'equals'} "${expectedValue}"`);
const value = await page.$eval(resolvedSelector, el => el.value);
if (negate) {
expect(value).not.toBe(expectedValue);
} else {
expect(value).toBe(expectedValue);
}
},
// Attribute assertions
toHaveAttribute: async (attributeName, expectedValue) => {
stepLogger.step('Verifying attribute', `"${selector}" ${attributeName}=${expectedValue}`);
const value = await page.$eval(resolvedSelector, (el, attr) => el.getAttribute(attr), attributeName);
if (negate) {
expect(value).not.toBe(expectedValue);
} else {
expect(value).toBe(expectedValue);
}
},
// Class assertions
toHaveClass: async (className) => {
stepLogger.step('Verifying class', `"${selector}" has class "${className}"`);
const classes = await page.$eval(resolvedSelector, el => el.className);
if (negate) {
expect(classes).not.toContain(className);
} else {
expect(classes).toContain(className);
}
},
// Count assertions
toHaveCount: async (expectedCount) => {
stepLogger.step('Verifying count', `"${selector}" has ${expectedCount} elements`);
const elements = await page.$$(resolvedSelector);
if (negate) {
expect(elements.length).not.toBe(expectedCount);
} else {
expect(elements.length).toBe(expectedCount);
}
}
});
const assertions = createAssertions(false);
// Add the .not modifier
assertions.not = createAssertions(true);
return assertions;
},
// Raw CSS selector methods (for explicit CSS usage)
css: {
click: (selector, options = {}) => {
stepLogger.step('CSS Click', `"${selector}"`);
return page.click(selector, options);
},
type: (selector, text, options = {}) => {
stepLogger.step('CSS Type', `"${text}" into "${selector}"`);
return page.type(selector, text, options);
},
waitFor: (selector, options = {}) => {
stepLogger.step('CSS Wait for', `"${selector}"`);
return page.waitForSelector(selector, options);
},
get: (selector) => {
stepLogger.step('CSS Get', `"${selector}"`);
return page.$(selector);
},
getAll: (selector) => {
stepLogger.step('CSS Get All', `"${selector}"`);
return page.$$(selector);
},
getText: (selector) => {
stepLogger.step('CSS Get Text from', `"${selector}"`);
return page.$eval(selector, el => el.textContent);
},
exists: async (selector) => {
stepLogger.step('CSS Check exists', `"${selector}"`);
const element = await page.$(selector);
return element !== null;
}
},
// Page utilities
url: () => {
stepLogger.step('Getting URL');
return page.url();
},
title: () => {
stepLogger.step('Getting title');
return page.title();
},
content: () => {
stepLogger.step('Getting page content');
return page.content();
},
evaluate: (fn) => {
stepLogger.step('Evaluating JavaScript');
return page.evaluate(fn);
},
screenshot: (options = {}) => {
stepLogger.step('Taking screenshot');
return page.screenshot(options);
},
// Wait utilities
wait: async (ms) => {
stepLogger.step('Waiting', `${ms}ms`);
return new Promise(resolve => setTimeout(resolve, ms));
},
waitForNavigation: (options = {}) => {
stepLogger.step('Waiting for navigation');
return page.waitForNavigation(options);
}
};
export { device };