Skip to content

Commit e215ef2

Browse files
committed
feat: add "screenshot" wrapper to correctly screen web elements
1 parent 4d26e97 commit e215ef2

File tree

23 files changed

+1106
-89
lines changed

23 files changed

+1106
-89
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ module.exports = {
3838
safari13: {
3939
commands: [
4040
'url',
41+
'screenshot',
42+
'orientation',
4143
'swipe',
4244
'touch',
4345
'dragAndDrop'
@@ -55,6 +57,8 @@ module.exports = {
5557

5658
Wrappers over existing commands:
5759
* **url** - wrapper over wdio "url" in order to wait until the page is completely open (used timeout from [`hermione.pageLoadTimeout`](https://github.com/gemini-testing/hermione#pageloadtimeout) or `30000` ms). In [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver) page is open with using the `xcrun` utility - `xcrun simctl openurl` which just tells the simulator to open the page and does not wait anything;
60+
* **screenshot** - wrapper of wdio "screenshot" in order to cut the native elements from the final image ([calibration](https://github.com/gemini-testing/hermione#calibrate) must be turned off);
61+
* **orientation** - wrapper of wdio "orientation" in order to recalculate size of native elements for "screenshot" command (turns on automatically when you specify a screenshot command);
5862
* **swipe** - replaces wdio "swipe" in order to perform swipe by coordinates in native context;
5963
* **touch** - replaces wdio "touch" in order to perform touch click by coordinates in native context;
6064
* **dragAndDrop** - replaces wdio "dragAndDrop" in order to perform drag and drop elements by coordinates in native context.
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
'use strict';
22

3+
const {getTestContext, IS_NATIVE_CTX, WEB_VIEW_CTX} = require('./test-context');
34
const {NATIVE_CONTEXT} = require('../constants');
45

56
exports.runInNativeContext = async function(browser, action, testCtx) {
6-
if (!testCtx.webViewContext) {
7+
if (!testCtx) {
8+
testCtx = getTestContext(browser.executionContext);
9+
}
10+
11+
if (testCtx[IS_NATIVE_CTX]) {
12+
return action.fn.call(browser, ...[].concat(action.args));
13+
}
14+
15+
if (!testCtx[WEB_VIEW_CTX]) {
716
const {value: contexts} = await browser.contexts();
8-
testCtx.webViewContext = contexts[1];
17+
testCtx[WEB_VIEW_CTX] = contexts[1];
918
}
1019

1120
await browser.context(NATIVE_CONTEXT);
21+
testCtx[IS_NATIVE_CTX] = true;
22+
1223
const result = await action.fn.call(browser, ...[].concat(action.args));
13-
await browser.context(testCtx.webViewContext);
24+
25+
await browser.context(testCtx[WEB_VIEW_CTX]);
26+
testCtx[IS_NATIVE_CTX] = false;
1427

1528
return result;
1629
};

lib/command-helpers/element-utils.js

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
const {getTestContext} = require('../test-context');
5+
const {runInNativeContext} = require('../context-switcher');
6+
7+
/**
8+
* Decorator which checks that locator is existing on the page before perform action.
9+
* @param {Object} action - action which should be done after check that element exists.
10+
* @param {Function} action.fn - async/sync function.
11+
* @param {(*|*[])} action.args - arguments that passed to `action.fn`.
12+
* @param {*} action.default - default value that returned if locator does not exist.
13+
*
14+
* @returns {Promise}
15+
*/
16+
exports.withExisting = async function(action) {
17+
const locator = _.isArray(action.args) ? action.args[0] : action.args;
18+
const isExisting = await this.isExisting(locator);
19+
20+
if (!isExisting) {
21+
return action.default;
22+
}
23+
24+
return action.fn.call(this, ...[].concat(action.args));
25+
};
26+
27+
/**
28+
* Decorator which run action in native context.
29+
* @param {Object} action - action which should be done in native context.
30+
* @param {Function} action.fn - async/sync function.
31+
* @param {(*|*[])} action.args - arguments that passed to `action.fn`.
32+
*
33+
* @returns {Promise}
34+
*/
35+
exports.withNativeCtx = async function(action) {
36+
return runInNativeContext(this, action);
37+
};
38+
39+
/**
40+
* Decorator which memoize result of calling action and use it on subsequent calls.
41+
* @param {Object} action - action which should be done if result is not yet memoized.
42+
* @param {Function} action.fn - async/sync function.
43+
* @param {(*|*[])} action.args - arguments that passed to `action.fn`.
44+
* @param {string} key - key name by which the result is memoized.
45+
*
46+
* @returns {Promise}
47+
*/
48+
exports.withTestCtxMemo = async function(action, key) {
49+
const testCtx = getTestContext(this.executionContext);
50+
51+
if (!testCtx[key]) {
52+
testCtx[key] = await action.fn.call(this, ...[].concat(action.args));
53+
}
54+
55+
return testCtx[key];
56+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
3+
const {withExisting, withNativeCtx, withTestCtxMemo} = require('./decorators');
4+
const {TOP_TOOLBAR_SIZE, BOTTOM_TOOLBAR_LOCATION, WEB_VIEW_SIZE, PIXEL_RATIO} = require('../test-context');
5+
const {TOP_TOOLBAR, BOTTOM_TOOLBAR, WEB_VIEW} = require('../../native-locators');
6+
7+
exports.getTopToolbarHeight = async (browser) => {
8+
const action = {fn: browser.getElementSize, args: TOP_TOOLBAR, default: {width: 0, height: 0}};
9+
const existingWrapper = {fn: withExisting, args: action};
10+
const inNativeCtxWrapper = {fn: withNativeCtx, args: existingWrapper};
11+
12+
return (await withTestCtxMemo.call(browser, inNativeCtxWrapper, TOP_TOOLBAR_SIZE)).height;
13+
};
14+
15+
exports.getBottomToolbarY = async (browser) => {
16+
const action = {fn: browser.getLocation, args: BOTTOM_TOOLBAR, default: {x: 0, y: 0}};
17+
const existingWrapper = {fn: withExisting, args: action};
18+
const inNativeCtxWrapper = {fn: withNativeCtx, args: existingWrapper};
19+
20+
return (await withTestCtxMemo.call(browser, inNativeCtxWrapper, BOTTOM_TOOLBAR_LOCATION)).y;
21+
};
22+
23+
exports.getWebViewSize = async (browser) => {
24+
const action = {fn: browser.getElementSize, args: WEB_VIEW};
25+
const inNativeCtxWrapper = {fn: withNativeCtx, args: action};
26+
27+
return await withTestCtxMemo.call(browser, inNativeCtxWrapper, WEB_VIEW_SIZE);
28+
};
29+
30+
exports.getElemCoords = async (browser, selector) => {
31+
const [{width, height}, {x, y}] = await Promise.all([browser.getElementSize(selector), browser.getLocation(selector)]);
32+
const topToolbarHeight = await exports.getTopToolbarHeight(browser);
33+
34+
return {width, height, x, y: y + topToolbarHeight};
35+
};
36+
37+
exports.getElemCenterLocation = async (browser, selector) => {
38+
const {width, height, x, y} = await exports.getElemCoords(browser, selector);
39+
40+
return {
41+
x: Math.round(x + width / 2),
42+
y: Math.round(y + height / 2)
43+
};
44+
};
45+
46+
exports.getPixelRatio = async (browser) => {
47+
const action = {fn: async () => (await browser.execute(() => window.devicePixelRatio)).value};
48+
49+
return await withTestCtxMemo.call(browser, action, PIXEL_RATIO);
50+
};
51+
52+
exports.calcWebViewCoords = async (browser, {bodyWidth, pixelRatio = 1} = {}) => {
53+
const [topToolbarHeight, bottomToolbarY, webViewSize] = await Promise.all([
54+
exports.getTopToolbarHeight(browser),
55+
exports.getBottomToolbarY(browser),
56+
exports.getWebViewSize(browser)
57+
]);
58+
const bottomToolbarHeight = bottomToolbarY > 0 ? webViewSize.height - bottomToolbarY : 0;
59+
60+
return {
61+
width: Math.ceil(bodyWidth * pixelRatio),
62+
height: Math.ceil((webViewSize.height - topToolbarHeight - bottomToolbarHeight) * pixelRatio),
63+
left: Math.floor((webViewSize.width - bodyWidth) / 2 * pixelRatio),
64+
top: Math.floor(topToolbarHeight * pixelRatio)
65+
};
66+
};
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
'use strict';
22

3-
exports.getTestContext = (context) => {
3+
const getTestContext = (context) => {
44
return context.type === 'hook' && /^"before each"/.test(context.title)
55
? context.ctx.currentTest
66
: context;
77
};
8+
9+
const resetTestContextValues = (context, keys = []) => {
10+
const testCtx = getTestContext(context);
11+
12+
[].concat(keys).forEach((key) => {
13+
delete testCtx[key];
14+
});
15+
};
16+
17+
module.exports = {
18+
getTestContext,
19+
resetTestContextValues,
20+
21+
TOP_TOOLBAR_SIZE: 'topToolbarSize',
22+
BOTTOM_TOOLBAR_LOCATION: 'bottomToolbarLocation',
23+
WEB_VIEW_SIZE: 'webViewSize',
24+
IS_NATIVE_CTX: 'isNativeCtx',
25+
WEB_VIEW_CTX: 'webViewContext',
26+
PIXEL_RATIO: 'pixelRatio'
27+
};

lib/commands/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ module.exports = {
44
url: require('./url'),
55
swipe: require('./swipe'),
66
touch: require('./touch'),
7-
dragAndDrop: require('./dragAndDrop')
7+
dragAndDrop: require('./dragAndDrop'),
8+
screenshot: require('./screenshot'),
9+
orientation: require('./orientation')
810
};

lib/commands/orientation.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
const {resetTestContextValues, TOP_TOOLBAR_SIZE, BOTTOM_TOOLBAR_LOCATION, WEB_VIEW_SIZE} = require('../command-helpers/test-context');
4+
5+
module.exports = (browser) => {
6+
const baseOrientationFn = browser.orientation;
7+
8+
browser.addCommand('orientation', async (orientation) => {
9+
if (orientation) {
10+
resetTestContextValues(browser.executionContext, [TOP_TOOLBAR_SIZE, BOTTOM_TOOLBAR_LOCATION, WEB_VIEW_SIZE]);
11+
}
12+
13+
return baseOrientationFn.call(browser, orientation);
14+
}, true);
15+
};

lib/commands/screenshot.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
const {PNG} = require('pngjs');
4+
const concat = require('concat-stream');
5+
const streamifier = require('streamifier');
6+
const {calcWebViewCoords, getPixelRatio} = require('../command-helpers/element-utils');
7+
const {runInNativeContext} = require('../command-helpers/context-switcher');
8+
9+
module.exports = (browser) => {
10+
const baseScreenshotFn = browser.screenshot;
11+
12+
browser.addCommand('screenshot', async () => {
13+
const {width: bodyWidth} = await browser.getElementSize('body');
14+
const pixelRatio = await getPixelRatio(browser);
15+
const cropCoords = await runInNativeContext(browser, {fn: calcWebViewCoords, args: [browser, {bodyWidth, pixelRatio}]});
16+
17+
const {value: base64} = await baseScreenshotFn.call(this);
18+
19+
return new Promise((resolve, reject) => {
20+
const handleError = (msg) => (err) => reject(`Error occured while ${msg}: ${err.message}`);
21+
22+
streamifier.createReadStream(Buffer.from(base64, 'base64'))
23+
.on('error', handleError('converting buffer to readable stream'))
24+
.pipe(new PNG())
25+
.on('error', handleError('writing buffer to png data'))
26+
.on('parsed', function() {
27+
const destination = new PNG({width: cropCoords.width, height: cropCoords.height});
28+
29+
try {
30+
this.bitblt(destination, cropCoords.left, cropCoords.top, cropCoords.width, cropCoords.height);
31+
} catch (err) {
32+
reject(`Error occured while copying pixels from source to destination png: ${err.message}`);
33+
}
34+
35+
destination.pack()
36+
.on('error', handleError('packing png data to buffer'))
37+
.pipe(concat((buffer) => resolve({value: buffer.toString('base64')})))
38+
.on('error', handleError('concatenating png data to a single buffer'));
39+
});
40+
});
41+
}, true);
42+
};

lib/commands/swipe.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const _ = require('lodash');
4-
const {getTestContext} = require('../command-helpers/test-context');
4+
const {resetTestContextValues, TOP_TOOLBAR_SIZE, BOTTOM_TOOLBAR_LOCATION} = require('../command-helpers/test-context');
55
const {getElemCenterLocation} = require('../command-helpers/element-utils');
66
const {WAIT_BETWEEN_ACTIONS_IN_MS} = require('../constants');
77

@@ -29,7 +29,8 @@ module.exports = (browser) => {
2929
'release'
3030
]);
3131

32-
const testCtx = getTestContext(browser.executionContext);
33-
testCtx.isVerticalSwipePerformed = Boolean(yOffset);
32+
if (yOffset) {
33+
resetTestContextValues(browser.executionContext, [TOP_TOOLBAR_SIZE, BOTTOM_TOOLBAR_LOCATION]);
34+
}
3435
}, true);
3536
};

0 commit comments

Comments
 (0)