Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Element screenshot (Android) #2222

Merged
merged 9 commits into from
Jul 23, 2020
Merged

Element screenshot (Android) #2222

merged 9 commits into from
Jul 23, 2020

Conversation

d4vidi
Copy link
Collaborator

@d4vidi d4vidi commented Jul 21, 2020


Description:
Relates to #2012: introduces the fully-blown (almost*) element screenshot taking solution for Android, using an interaction API.

Implementation notes:
After some research, files on Android are a bit of a headache, and really - there's no need for a file on the device. I therefore took @valentynberehovyi's advice and skipped file saving altogether, and instead return the bitmap as the invocation's result (base-64 encoded, of course). I tested this even with an element that takes up almost the entire screen on a full HD device (1920x1080), and it went smooth.

P.S.
PR 2222, best ever


* see caveats in documentation


public static ViewInteraction perform(ViewInteraction interaction, ViewAction action) {
return interaction.perform(action);
Copy link
Collaborator Author

@d4vidi d4vidi Jul 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This never made sense! interaction.peform() always returns the interaction itself. What was the point here, to begin with? Effectively, invoke cannot parse an object, and therefore the final returned invoke-result has always been null.

With this new implementation, it's possible to return a result from the action itself. In the future, this should be useful for implementing getAttributes() @rotemmiz

class Interaction {
constructor(invocationManager) {
this._call = undefined;
this._invocationManager = invocationManager;
}

async execute() {
await this._invocationManager.execute(this._call);
const resultObj = await this._invocationManager.execute(this._call);
return resultObj ? resultObj.result : undefined;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enables effectively using the result returned from invoke action on the device

await e.element(e.by.id('ScrollView799')).swipe('left', 'fast', 0.9);
await e.element(e.by.id('ScrollView799')).swipe('right', 'slow', 0.9);
await e.element(e.by.id('ScrollView799')).atIndex(1);
describe('element interactions', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more boyscouting here than really related to this feature, except that I wanted more specific tests related to this specific interaction (screenshot taking)

@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 21, 2020

@ethanshar

@d4vidi d4vidi changed the title Element screenshot2 Element screenshot (Android) Jul 21, 2020
Copy link
Member

@rotemmiz rotemmiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good IMO, the only thing that bothers me is the API . element(matcher).takeScreenshot() is confusing with device.taakeScreenshot().
Also, this is a view bitmap, not a screen-shot, so I suggest chaging the name to element(matcher).takeViewBitamp(), element(matcher).getViewBitamp(), element(matcher).captureView() or something similar.

@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 21, 2020

@rotemmiz how about element(matcher).captureBitmap()?

@rotemmiz
Copy link
Member

Also great

@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 21, 2020

Duly noted!

@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 21, 2020

@rotemmiz done!

@LeoNatan
Copy link
Contributor

@rotemmiz As opposed to screenshot, which is not a bitmap?

“Bitmap” is a bad terminology. It should be “snapshot”.

@rotemmiz
Copy link
Member

That's why I suggested that view will be a part of the function name. We want to get a bitmap of the view. I think it should be with little ambiguity as possible.
Why do you think snapshot is a better terminology?

@LeoNatan
Copy link
Contributor

I think we should be using the artifact system for this, and so “bitmap” is a very specific term. Images won’t be bmp, probably.

@LeoNatan
Copy link
Contributor

LeoNatan commented Jul 21, 2020

View is also something we avoid everywhere, so we shouldn’t use that. snapshot() is enough, and perhaps provide file name as a parameter. Just like we don't use tapView() terminology.

function saveRawBase64Data(dataBase64, { filePath, fileSuffix }) {
const _filePath = filePath || tempfile(fileSuffix);
const data = Buffer.from(dataBase64, 'base64');
fs.writeFileSync(_filePath, data);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you clean up temporary files after the test session ends?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any good sense to bother with Buffer conversions?

fs.writeFileSync('hello.txt', 'SGVsbG8sIHdvcmxkIQ==', 'base64')
fs.readFileSync('hello.txt', 'utf8'); // 'Hello, world!'

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😻 😻 😻

@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 22, 2020

@rotemmiz I tend to agree with @LeoNatan:

  1. View = implementation details. Our API's offer an abstraction.
  2. Bitmap is good, but might be a bit too technical. I favoured it because it hints of that we do not effectively grab pixels off the screen - but rather redraw the element onto a bitmap. We should probably come up with something between bitmap and screenshot. I'm open to suggestions.

As per snapshot - I'm highly against that because the term is used in snapshot testing. Moreover, this feature in particular, aims at providing a means for app-devs to take screenshots in order to perform snapshot tests. We should really avoid the term here.

@LeoNatan
Copy link
Contributor

So the feature is intended for snapshot testing, but snapshot is not a good term? I don't understand.

@LeoNatan
Copy link
Contributor

LeoNatan commented Jul 22, 2020

uut = require('./easy-file-io');
});

it('should save raw data to a given file', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you use this scenario, Amit? @d4vidi

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I'll remove support for that.

async captureBitmap() {
// TODO this should be moved to a lower-layer handler of this use-case
const resultBase64 = await new ActionInteraction(this._invocationManager, this, new CaptureBitmapAction()).execute();
return ezFile.saveRawBase64Data(resultBase64, { fileSuffix: '.detox.element.png' });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not we just move detox/src/artifacts/utils/temporaryPath.js to detox/src/utils, import it:

const temporaryPath = require('../utils/temporaryPath');

and just use in captureBitmap:

const resultBase64 = await new ActionInteraction(this._invocationManager, this, new CaptureBitmapAction()).execute();
const pngPath = temporaryPath.for.png();
await fs.writeFile(pngPath, resultBase64, 'base64');
return pngPath;

and not create yet another mini-module, please?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered doing that a number of times, but temporaryPath seemed very artifact oriented, seeing that the API set basically pairs 1:1 with the artifacts types we have (image, log, video, perf recording). If you find it apt nevertheless, I will use it here.

The mini-module was a compromise as we do not have a lower-layer logic-provider for this, just yet. I wanted to keep the logic here down to the bare minimum. 'Makes it easier to test (when in a separate module), too. wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't see what it is here to test. 🤷‍♂️

The test idea here would be to check whether we write a received (mocked) base64 string to some temporary file.

If we extract logic to a separate file, it feels like we are going to write very similar tests (hence, 2 times the same) - for expect.js's takeScreenshot and that minimodule.

I don't feel like expanding the test logic by extra 3-4 lines is going to hurt its legibility:

ezFileIo = require('../utils/easy-file-io');
ezFileIo.saveRawBase64Data.mockReturnValue('/mock/file/path');
// ...
jest.mock('fs-extra').mock('../utils/temporaryPath');
require('../utils/temporaryPath').for.png.mockReturnValue('mock.png');
// ...
expect(require('fs-extra').writeFile).toHaveBeenCalledWith('mock.png', invokeResultInBase64, 'base64');

Copy link
Collaborator

@noomorph noomorph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d4vidi, if you are hesitant to change that ez pz unit stuff, I am not happy about it, but whatever you decide - it's up to you.

You have my approval.

@d4vidi d4vidi force-pushed the element-screenshot2 branch 2 times, most recently from 8508450 to 6f60834 Compare July 22, 2020 13:40
@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 22, 2020

@noomorph I'll take your advice on this and get rid of easy-file-io :)

@noomorph
Copy link
Collaborator

@d4vidi, 🙏 I sincerely hope I have managed to convey why I considered easy-file-io impractical a bit in a sense of inflating code and unit tests base with minimal benefits in return. Good luck with the field testing of the feature! Hope it works out as our guys expect.

@d4vidi d4vidi force-pushed the element-screenshot2 branch from ad10941 to 10a6171 Compare July 22, 2020 14:33
@d4vidi
Copy link
Collaborator Author

d4vidi commented Jul 22, 2020

@noomorph I generally agree. Again, in this case, this came up trying to work-around the fact we don't have proper abstractions in the expectation/matching realms, namely - with a proper place to put this logic in (I still don't think the Element.takeScreenshot() is the right place, at least no as-is).

But after pointing this out to me, I've come to terms that this can only be worked-out using proper refactoring.

@d4vidi d4vidi force-pushed the element-screenshot2 branch from 022da93 to f3a6ceb Compare July 22, 2020 14:54
@d4vidi d4vidi merged commit a4383ed into master Jul 23, 2020
@d4vidi d4vidi deleted the element-screenshot2 branch July 23, 2020 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants