Skip to content

Commit

Permalink
Fix previously ignored view-clipping
Browse files Browse the repository at this point in the history
  • Loading branch information
d4vidi committed Jul 22, 2020
1 parent 883661b commit 6f60834
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.wix.detox.common

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import java.io.ByteArrayOutputStream

Expand All @@ -16,9 +17,15 @@ class ScreenshotResult(private val bitmap: Bitmap) {

class ViewScreenshot() {
fun takeOf(view: View): ScreenshotResult {
val visibleRect = Rect()
if (!view.getLocalVisibleRect(visibleRect)) {
throw IllegalStateException("Cannot take screenshot of a view that's out of screen's bounds")
}

val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
return ScreenshotResult(bitmap)
view.draw(Canvas(bitmap))

val clippedBitmap = Bitmap.createBitmap(bitmap, visibleRect.left, visibleRect.top, visibleRect.width(), visibleRect.height())
return ScreenshotResult(clippedBitmap)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.wix.detox.espresso.action

import android.graphics.Rect
import android.util.Base64
import android.view.View
import androidx.test.espresso.UiController
Expand All @@ -16,14 +15,7 @@ class TakeViewScreenshotAction(private val viewScreenshot: ViewScreenshot = View
private var result: Any? = null

override fun perform(uiController: UiController?, view: View?) {
view!!

val visibleRect = Rect()
if (!view.getLocalVisibleRect(visibleRect)) {
throw IllegalStateException("Cannot take screenshot of a view that's out of screen's bounds")
}

val rawResult = viewScreenshot.takeOf(view).asRawBytes()
val rawResult = viewScreenshot.takeOf(view!!).asRawBytes()
result = Base64.encodeToString(rawResult, Base64.NO_WRAP or Base64.NO_PADDING)
}

Expand Down
43 changes: 30 additions & 13 deletions detox/test/e2e/26.element-screenshots.test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
const fs = require('fs');
const { expectToThrow } = require('./utils/custom-expects');

describe(':android: Element screenshots', () => {
const screenshotAssetPath = './e2e/assets/elementScreenshot-android.png';

beforeEach(async () => {
await device.reloadReactNative();
await element(by.text('Matchers')).tap();
await element(by.text('Element-Screenshots')).tap();
});

it('should take a screenshot of an element', async () => {
const bitmapPath = await element(by.id('Grandfather883')).takeScreenshot();
it('should take a screenshot of a vertically-clipped element', async () => {
const screenshotAssetPath = './e2e/assets/elementScreenshot.android.vert.png';

const bitmapPath = await element(by.id('fancyElement')).takeScreenshot();
expectBitmapsToBeEqual(bitmapPath, screenshotAssetPath);
});

it('should take a screenshot of a horizontally-clipped element', async () => {
const screenshotAssetPath = './e2e/assets/elementScreenshot.android.horiz.png';

await element(by.id('switchOrientation')).tap();

const bitmapPath = await element(by.id('fancyElement')).takeScreenshot();
expectBitmapsToBeEqual(bitmapPath, screenshotAssetPath);
});

it('should fail to take a screenshot of an off-screen element', async () => {
await expectToThrow(
() => element(by.id('offscreenElement')).takeScreenshot(),
`Cannot take screenshot of a view that's out of screen's bounds`,
);
});

function expectBitmapsToBeEqual(bitmapPath, expectedBitmapPath) {
const bitmapBuffer = fs.readFileSync(bitmapPath);
const expectedBitmapBuffer = fs.readFileSync(screenshotAssetPath);
const expectedBitmapBuffer = fs.readFileSync(expectedBitmapPath);
if (!bitmapBuffer.equals(expectedBitmapBuffer)) {
throw new Error([
`Bitmaps at (1) ${bitmapPath} and (2) ${screenshotAssetPath} are different!`,
'(1): ' + JSON.stringify(bitmapBuffer),
'VS.',
'(2): ' + JSON.stringify(expectedBitmapBuffer),
].join('\n'));
}
});
throw new Error(`Expected bitmap at ${bitmapPath} to be equal to ${expectedBitmapPath}, but it is different!`);
}
}
});
Binary file removed detox/test/e2e/assets/elementScreenshot-android.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 5 additions & 8 deletions detox/test/e2e/utils/custom-expects.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
async function expectToThrow(testBlock) {

let error;
async function expectToThrow(testBlock, withMessage) {
try {
await testBlock();
fail('Expected an error but nothing was thrown');
} catch (e) {
error = e;
if (withMessage && !e.message.includes(withMessage)) {
throw new Error(`Caught an expected error but message was different:\nExpected: ${withMessage}\nReceived: ${e.message}`)
}
console.log('Caught an expected error:', e.message.split('\n', 1)[0]);
}

if (!error) {
throw new Error('Expected an error but nothing was thrown');
}
}

module.exports = {
Expand Down
72 changes: 72 additions & 0 deletions detox/test/src/Screens/ElementScreenshotScreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { Component } from 'react';
import { View, Dimensions, TouchableHighlight } from 'react-native';

const screenWidth = Dimensions.get('window').width;
const screenHeight = Dimensions.get('window').height;

class ArtisticRectangle extends Component {
static defaultProps = {
borderSizeV: 12,
borderSizeH: 12,
}

render() {
const paddingHorizontal = this.props.borderSizeH;
const paddingVertical = this.props.borderSizeV;
return (
<View testID={this.props.testID}>
<View style={{paddingHorizontal, paddingVertical, backgroundColor: 'cyan'}}>
<View style={{paddingHorizontal, paddingVertical, backgroundColor: 'magenta'}}>
<View style={{paddingHorizontal, paddingVertical, backgroundColor: 'yellow'}}>
<View style={{paddingHorizontal, paddingVertical, backgroundColor: 'black'}} />
</View>
</View>
</View>
</View>
);
}
}

export default class ElementScreenshotScreen extends Component {
static orientations = {
'vertical': {
borderSizeH: 12,
borderSizeV: screenHeight * 1.5 / 8,
},
'horizontal': {
borderSizeH: screenWidth * 1.5 / 8,
borderSizeV: 12,
}
};

constructor(props) {
super(props);
this.switchOrientation = this.switchOrientation.bind(this);

this.state = {
orientation: 'vertical',
};
}

render() {
const { borderSizeH, borderSizeV } = ElementScreenshotScreen.orientations[this.state.orientation];

return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<TouchableHighlight testID='switchOrientation' onPress={this.switchOrientation}>
<ArtisticRectangle testID='fancyElement' borderSizeH={borderSizeH} borderSizeV={borderSizeV} />
</TouchableHighlight>

<View style={{position: 'absolute', left: -100, top: -100}}>
<ArtisticRectangle testID='offscreenElement' />
</View>
</View>
);
}

switchOrientation() {
this.setState({
orientation: this.state.orientation === 'vertical' ? 'horizontal' : 'vertical',
});
}
}
4 changes: 3 additions & 1 deletion detox/test/src/Screens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import LaunchArgsScreen from './LaunchArgsScreen';
import LaunchNotificationScreen from './LaunchNotificationScreen';
import PickerViewScreen from './PickerViewScreen';
import DeviceScreen from './DeviceScreen';
import ElementScreenshotScreen from './ElementScreenshotScreen';

export {
SanityScreen,
Expand All @@ -45,5 +46,6 @@ export {
LanguageScreen,
LaunchArgsScreen,
LaunchNotificationScreen,
DeviceScreen
DeviceScreen,
ElementScreenshotScreen,
};
6 changes: 5 additions & 1 deletion detox/test/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import * as Screens from './Screens';

const isAndroid = Platform.OS === 'android';
const isIos = Platform.OS === 'ios';

const { NativeModule } = NativeModules;

Expand Down Expand Up @@ -125,7 +126,10 @@ class example extends Component {
})}
</View>

{this.renderScreenButton('Shake', Screens.ShakeScreen)}
{isIos && this.renderScreenButton('Shake', Screens.ShakeScreen)}

{isAndroid && this.renderScreenButton('Element-Screenshots', Screens.ElementScreenshotScreen)}

<View style={{flexDirection: 'row', justifyContent: 'center'}}>
{isAndroid && this.renderScreenButton('Launch Args', Screens.LaunchArgsScreen)}
{isAndroid && this.renderInlineSeparator()}
Expand Down

0 comments on commit 6f60834

Please sign in to comment.