@wdio/visual-service
Version:
Image comparison / visual regression testing for WebdriverIO
108 lines (107 loc) • 5.23 kB
JavaScript
import { getBrowserObject } from './utils.js';
const DEFAULT_EXPECTED_RESULT = 0;
const asymmetricMatcher = typeof Symbol === 'function' && Symbol.for
? Symbol.for('jest.asymmetricMatcher')
: 0x13_57_a5;
function isAsymmetricMatcher(expected) {
return Boolean(expected && typeof expected === 'object' && '$$typeof' in expected && expected.$$typeof === asymmetricMatcher && 'asymmetricMatch' in expected);
}
function evaluateResult(result, expected, instanceName) {
if (isAsymmetricMatcher(expected)) {
const pass = expected.asymmetricMatch(result.misMatchPercentage);
const message = `${instanceName !== 'default' ? `Instance "${instanceName}": ` : ''}Expected image to match with the given asymmetric matcher but did not pass!`;
return {
pass,
message: () => message,
};
}
if (typeof expected === 'number') {
const pass = result.misMatchPercentage <= expected;
return {
pass,
message: () => (instanceName !== 'default' ? `Instance "${instanceName}":\n` : '') +
`Expected image mismatch percentage to be at most ${expected}%, but was ${result.misMatchPercentage}%.\n` +
'If this is acceptable, you may need to adjust the threshold or update the baseline image if the changes are intentional.\n' +
`\nBaseline: ${result.folders.baseline}\n` +
`Actual Screenshot: ${result.folders.actual}\n` +
`Difference: ${result.folders.diff}\n` +
'\nFor guidance on handling visual discrepancies, refer to: https://webdriver.io/docs/api/visual-regression.html'
};
}
throw new Error(`Invalid matcher for instance "${instanceName}", expect either a number or an asymmetric matcher, but found ${expected}`);
}
function isMultiremoteResult(result) {
return typeof result === 'object' && Object.values(result)[0]?.misMatchPercentage !== undefined;
}
function compareResult(result, expected) {
const isMultiremote = isMultiremoteResult(result);
const results = isMultiremote
? Object.entries(result).map(([instanceName, instanceResult]) => ({
instanceName,
result: instanceResult,
}))
: [{ instanceName: 'default', result }];
const failureMessages = [];
let overallPass = true;
for (const { instanceName, result: instanceResult } of results) {
const { pass, message } = evaluateResult(instanceResult, expected, instanceName);
if (!pass) {
overallPass = false;
failureMessages.push(message());
}
}
return {
pass: overallPass,
message: () => failureMessages.join('\n\n') || 'All instances passed the visual comparison test.',
};
}
function parseMatcherParams(tag, expectedResult, options) {
/**
* throw if `tag` is not a string
*/
if (typeof tag !== 'string') {
throw new Error(`Expected a snapshot tag as a string but received "${typeof tag}"`);
}
/**
* if `expectedResult` is an object, it is an options object
* ```ts
* expect(browser).toMatchScreenSnapshot('foo', { hideAfterFirstScroll: [element] })
* ```
*/
if (typeof expectedResult === 'object' && !isAsymmetricMatcher(expectedResult)) {
options = expectedResult;
expectedResult = DEFAULT_EXPECTED_RESULT;
}
/**
* make sure `options` is an object
*/
if (typeof options !== 'object') {
options = {};
}
/**
* overwrite `returnAllCompareData` to allow us to provide a better assertion message
*/
options.returnAllCompareData = true;
return { expectedResult, options };
}
export async function toMatchScreenSnapshot(browser, tag, expectedResultOrOptions, optionsOrUndefined) {
const { expectedResult, options } = parseMatcherParams(tag, expectedResultOrOptions, optionsOrUndefined);
const result = await browser.checkScreen(tag, options);
return compareResult(result, expectedResult || DEFAULT_EXPECTED_RESULT);
}
export async function toMatchFullPageSnapshot(browser, tag, expectedResultOrOptions, optionsOrUndefined) {
const { expectedResult, options } = parseMatcherParams(tag, expectedResultOrOptions, optionsOrUndefined);
const result = await browser.checkFullPageScreen(tag, options);
return compareResult(result, expectedResult || DEFAULT_EXPECTED_RESULT);
}
export async function toMatchElementSnapshot(element, tag, expectedResultOrOptions, optionsOrUndefined) {
const { expectedResult, options } = parseMatcherParams(tag, expectedResultOrOptions, optionsOrUndefined);
const browser = getBrowserObject(await element);
const result = await browser.checkElement(await element, tag, options);
return compareResult(result, expectedResult || DEFAULT_EXPECTED_RESULT);
}
export async function toMatchTabbablePageSnapshot(browser, tag, expectedResultOrOptions, optionsOrUndefined) {
const { expectedResult, options } = parseMatcherParams(tag, expectedResultOrOptions, optionsOrUndefined);
const result = await browser.checkTabbablePage(tag, options);
return compareResult(result, expectedResult || DEFAULT_EXPECTED_RESULT);
}