@percy/appium-app
Version:
Appium client library for visual testing with Percy
300 lines (269 loc) • 8.85 kB
JavaScript
const utils = require('@percy/sdk-utils');
const tmp = require('tmp');
const fs = require('fs/promises');
const { Tile } = require('../util/tile');
const { MetadataResolver } = require('../metadata/metadataResolver');
const { TimeIt } = require('../util/timing');
const log = require('../util/log');
// Collect client and environment information
const sdkPkg = require('../../package.json');
const CLIENT_INFO = `${sdkPkg.name}/${sdkPkg.version}`;
let clientWdPkg = null;
try {
clientWdPkg = require('wd/package.json');
} catch { }
try {
clientWdPkg = require('webdriverio/package.json');
} catch { }
let ENV_INFO = `(${clientWdPkg?.name}/${clientWdPkg?.version})`;
class GenericProvider {
constructor(driver) {
this.driver = driver;
this.metadata = null;
this.debugUrl = null;
}
static supports(_driver) {
return true;
}
async screenshot(name, {
fullscreen,
deviceName,
osVersion,
orientation,
statusBarHeight,
navigationBarHeight,
fullPage,
screenLengths,
ignoreRegionXpaths,
ignoreRegionAccessibilityIds,
ignoreRegionAppiumElements,
customIgnoreRegions,
considerRegionXpaths,
considerRegionAccessibilityIds,
considerRegionAppiumElements,
customConsiderRegions,
scrollableXpath,
topScrollviewOffset,
bottomScrollviewOffset,
scrollableId,
sync,
testCase,
labels,
thTestCaseExecutionId,
androidScrollAreaPercentage,
scrollSpeed
}) {
fullscreen = fullscreen || false;
sync = sync || null;
this.metadata = await MetadataResolver.resolve(this.driver, {
deviceName,
osVersion,
orientation,
statusBarHeight,
navigationBarHeight
});
const tag = await this.getTag();
const tiles = await this.getTiles(
fullscreen,
fullPage,
screenLengths,
scrollableXpath,
topScrollviewOffset,
bottomScrollviewOffset,
scrollableId,
androidScrollAreaPercentage,
scrollSpeed
);
const ignoreRegions = await this.findRegions(
ignoreRegionXpaths, ignoreRegionAccessibilityIds, ignoreRegionAppiumElements, customIgnoreRegions
);
const considerRegions = await this.findRegions(
considerRegionXpaths, considerRegionAccessibilityIds, considerRegionAppiumElements, customConsiderRegions
);
log.debug(`${name} : Tag ${JSON.stringify(ignoreRegions)}`);
log.debug(`${name} : Tag ${JSON.stringify(tag)}`);
log.debug(`${name} : Tiles ${JSON.stringify(tiles)}`);
log.debug(`${name} : Debug url ${this.debugUrl}`);
log.debug(`${name} : sync ${sync}`);
return await utils.postComparison({
name,
tag,
tiles,
externalDebugUrl: await this.getDebugUrl(),
ignoredElementsData: {
ignoreElementsData: ignoreRegions
},
consideredElementsData: {
considerElementsData: considerRegions
},
environmentInfo: ENV_INFO,
clientInfo: CLIENT_INFO,
sync,
testCase,
labels,
thTestCaseExecutionId
});
}
async getTiles(fullscreen, _fullPage, _screenLengths) {
if (_fullPage === true) {
log.warn('Full page screeshot is only supported on App Automate.' +
' Falling back to single page screenshot.');
}
const base64content = await this.driver.takeScreenshot();
const path = await this.writeTempImage(base64content);
return [
new Tile({
filepath: path,
statusBarHeight: await this.metadata.statusBarHeight(),
navBarHeight: await this.metadata.navigationBarHeight(),
headerHeight: 0,
footerHeight: 0,
fullscreen
})
];
}
async getTag() {
const { width, height } = await this.metadata.screenSize();
return {
name: await this.metadata.deviceName(),
osName: await this.metadata.osName(),
osVersion: await this.metadata.osVersion(),
width,
height,
orientation: await this.metadata.orientation()
};
}
async writeTempImage(base64content) {
return await TimeIt.run('writeTempImage', async () => {
const path = await this.tempFile();
const buffer = Buffer.from(base64content, 'base64');
await fs.writeFile(path, buffer);
return path;
});
}
// this creates a temp file and closes descriptor
async tempFile() {
const percyTmpDir = process.env.PERCY_TMP_DIR;
if (percyTmpDir) {
// this does not throw for existing directory if recursive is true
await fs.mkdir(percyTmpDir, { recursive: true });
}
return await TimeIt.run('tempFile', async () => {
return await new Promise((resolve, reject) => {
tmp.file({
mode: 0o644,
tmpdir: percyTmpDir,
prefix: 'percy-',
postfix: '.png',
discardDescriptor: true
}, (err, path) => {
/* istanbul ignore next */ // hard to test
if (err) reject(err);
resolve(path);
});
});
});
}
async getDebugUrl() {
return this.debugUrl;
}
async findRegions(xpaths, accessibilityIds, appiumElements, customLocations) {
const regionsArray = [];
await this.getRegionsByXpath(regionsArray, xpaths || []);
await this.getRegionsByIds(regionsArray, accessibilityIds || []);
await this.getRegionsByElements(regionsArray, appiumElements || []);
await this.getRegionsByLocation(regionsArray, customLocations || []);
return regionsArray;
}
async getRegionObject(selector, element) {
const scaleFactor = await this.metadata.scaleFactor();
const location = await element.getLocation();
const size = await element.getSize();
const coOrdinates = {
top: location.y * scaleFactor,
bottom: (location.y + size.height) * scaleFactor,
left: location.x * scaleFactor,
right: (location.x + size.width) * scaleFactor
};
const jsonObject = {
selector,
coOrdinates
};
return jsonObject;
}
async getRegionsByXpath(elementsArray, xpaths) {
for (const xpath of xpaths) {
try {
const element = await this.driver.elementByXPath(xpath);
const selector = `xpath: ${xpath}`;
const ignoredRegion = await this.getRegionObject(selector, element);
elementsArray.push(ignoredRegion);
} catch (e) {
log.info(`Appium Element with xpath: ${xpath} not found. Ignoring this xpath.`);
log.debug(e.toString());
}
}
}
async getRegionsByIds(elementsArray, ids) {
for (const id of ids) {
try {
const element = await this.driver.elementByAccessibilityId(id);
const selector = `id: ${id}`;
const ignoredRegion = await this.getRegionObject(selector, element);
elementsArray.push(ignoredRegion);
} catch (e) {
log.info(`Appium Element with id: ${id} not found. Ignoring this id.`);
log.debug(e.toString());
}
}
}
async getRegionsByElements(elementsArray, elements) {
for (let index = 0; index < elements.length; index++) {
try {
let identifier;
const element = elements[index];
const capabilities = await this.driver.getCapabilities();
const platformName = capabilities.platformName.toLowerCase();
if (platformName === 'android') {
// Android identifiers
identifier = await element.getAttribute('resource-id') ||
await element.getAttribute('class');
} else if (platformName === 'ios') {
// iOS identifiers
identifier = await element.getAttribute('name') ||
await element.getAttribute('type');
}
const selector = `element: ${index} ${identifier ? `${identifier}` : ''}`.trim();
const ignoredRegion = await this.getRegionObject(selector, element);
elementsArray.push(ignoredRegion);
} catch (e) {
log.info(`Correct Mobile Element not passed at index ${index}.`);
log.debug(e.toString());
}
}
}
async getRegionsByLocation(elementsArray, customLocations) {
const { width, height } = await this.metadata.screenSize();
for (let index = 0; index < customLocations.length; index++) {
const customLocation = customLocations[index];
if (customLocation.isValid(height, width)) {
const selector = `custom ignore region ${index}`;
const ignoredRegion = {
selector,
coOrdinates: {
top: customLocation.top,
bottom: customLocation.bottom,
left: customLocation.left,
right: customLocation.right
}
};
elementsArray.push(ignoredRegion);
} else {
log.info(`Values passed in custom ignored region at index: ${index} is not valid`);
}
}
}
}
module.exports = {
GenericProvider
};