Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I screenshot tests in React Native?

I would like to test my React Native application using screenshots. The UIAutomation javascript file will be executed by fastlane and should deliver me all subviews I need. This part works fine.

My main problem is that I don't understand how I may have an element clicked. Every example I found was plain objective-c and used standard elements for navigation like a tab bar. My application has a Burger Icon, which has a click event on the TouchableHighlight which opens a menu. I am searching for a possibility to reference a single TouchableHighlightelement in order to interact with it.

Bonus points for such answers, which don't have me to write Objective-C.

like image 889
Daniel Schmidt Avatar asked Oct 22 '15 15:10

Daniel Schmidt


3 Answers

Fastlane (more specific snapshot) has deprecated UI Automation for UI Tests. In case you need to update the gems, your UIA javascript won't work for UI Tests (which are written in Obj C or Swift)

Why change to UI Tests?

UI Automation is deprecated UI Tests will evolve and support even more features in the future UI Tests are much easier to debug UI Tests are written in Swift or Objective C UI Tests can be executed in a much cleaner and better way

https://github.com/fastlane/snapshot

Looks like someone else using React Native made a little progress with UI Testing and Snapshot: https://github.com/fastlane/snapshot/issues/267

like image 180
eertl Avatar answered Nov 12 '22 03:11

eertl


I'm not familiar with fastlane, but you might want to give Jest a try since it's officially supported. They admittedly don't have full coverage, and it's quite possible you'll have to roll your own solution in some cases given how young react native is, but this ought to get you started on the right foot Snapshot Tests (iOS only)

like image 43
Chris Geirman Avatar answered Nov 12 '22 03:11

Chris Geirman


Note: we're using detox for our tests, so I'm using device.getPlatform() to test for iOS or Android.

What I ended up doing is a mixture of JavaScript libs (pixelmatch and pngjs), using fs and using command line commands (xcrun simctl and adb).

const {device} = require('detox');
const {execSync} = require('child_process');
const fs = require('fs');
const {existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync} = fs;
const PNG = require('pngjs').PNG;
const pixelmatch = require('pixelmatch');

const IOS_SCREENSHOT_OPTIONS = {
  timeout: 1000,
  killSignal: 'SIGKILL'
};

function getScreenShotDirectory() { ... }
function getActualFileName(testName) { ... }
function getExpectedFileName(testName) { ... }
function getDiffFileName(testName) { ... }

async function takeScreenshot(testName) {
  const actualFileName = getActualFileName(testName);
  const directoryName = getScreenShotDirectory();
  if (!existsSync(directoryName)) {
    mkdirSync(directoryName, {recursive: true});
  }

  if (device.getPlatform() === 'ios') {
    execSync(`xcrun simctl io booted screenshot "${actualFileName}"`, IOS_SCREENSHOT_OPTIONS);
    await removeIosStatusBar(actualFileName);
  } else {
    execSync(`adb exec-out screencap -p > "${actualFileName}"`);
  }
}

const compareScreenshot = async testName => {
  const actualFileName = getActualFileName(testName);
  await takeScreenshot(testName);
  const expectedFileName = getExpectedFileName(testName);
  const actualImage = PNG.sync.read(readFileSync(actualFileName));
  if (!existsSync(expectedFileName)) {
    console.warn(`No expected image for ${testName} @ ${expectedFileName}`);
    return false;
  }

  const expectedImage = PNG.sync.read(readFileSync(getExpectedFileName(testName)));
  const {width, height} = actualImage;
  const diffImage = new PNG({width, height});
  const numDiffPixels = pixelmatch(actualImage.data, expectedImage.data, diffImage.data, width, height);

  if (numDiffPixels === 0) {
    unlinkSync(actualFileName);
    return true;
  } else {
    const percentDiffPixels = numDiffPixels / (width * height);
    console.warn(
      `Images are different ${testName} numDiffPixels=${numDiffPixels} percentDiffPixels=${percentDiffPixels}`
    );
    writeFileSync(getDiffFileName(testName), PNG.sync.write(diffImage));
    return false;
  }
};

To improve your testing results you should use Android's demo mode, for example:

execSync('adb shell settings put global sysui_demo_allowed 1');
execSync('adb shell am broadcast -a com.android.systemui.demo -e command ...');
execSync('adb shell am broadcast -a com.android.systemui.demo -e command exit');

And from xcode 11 you have:

execSync('xcrun simctl status_bar <device> override ...')

I removed the status bar from iOS using the following code (but it reduces performance):

const IOS_STATUS_BAR_HEIGHT = 40;
async function removeIosStatusBar(imageFileName) {
  return new Promise((resolve, reject) => {
    const image = PNG.sync.read(readFileSync(imageFileName));
    let {width, height} = image;
    height -= IOS_STATUS_BAR_HEIGHT;
    const dst = new PNG({width, height});
    fs.createReadStream(imageFileName)
      .pipe(new PNG())
      .on('error', error => reject(error))
      .on('parsed', function () {
        this.bitblt(dst, 0, IOS_STATUS_BAR_HEIGHT, width, height, 0, 0);
        dst
          .pack()
          .pipe(fs.createWriteStream(imageFileName))
          .on('error', error => reject(error))
          .on('finish', () => resolve(imageFileName));
      });
  });
}
like image 2
MikeL Avatar answered Nov 12 '22 03:11

MikeL