How do I screenshot tests in React Native?

喜夏-厌秋 提交于 2019-12-06 04:49:39

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

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)

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));
      });
  });
}

Create a new project.

$ react-native -v
react-native-cli: 2.0.1

$ react-native init NativeSnapshots

$ cd NativeSnapshots

$ react-native run-ios

Test it works, launch welcome screen.

$ cd ios

$ fastlane snapshot init

fastlane output:

[14:37:56]: For more information, check out https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
✅  Successfully created SnapshotHelper.swift './SnapshotHelper.swift'
✅  Successfully created new Snapfile at './Snapfile'
-------------------------------------------------------
Open your Xcode project and make sure to do the following:
1) Add a new UI Test target to your project
2) Add the ./fastlane/SnapshotHelper.swift to your UI Test target
   You can move the file anywhere you want
3) Call `setupSnapshot(app)` when launching your app

  let app = XCUIApplication()
  setupSnapshot(app)
  app.launch()

4) Add `snapshot("0Launch")` to wherever you want to create the screenshots

More information on GitHub: https://github.com/fastlane/fastlane/tree/master/snapshot

Step 1: Add a new UI Test target to your project

Xcode Version 8.3.3 > Open NativeSnapshots.xcodeproj

File > New > Target > iOS UI Testing Bundle

Step 2: Add the ./fastlane/SnapshotHelper.swift to your UI Test target

Highlight NativeSnapshotsUITests
File > Add Files to NativeSnapshots
Select ./fastlane/SnapshotHelper.swift, Enter

Step 3: Call setupSnapshot(app) when launching your app

Open NativeSnapshotsUITests/NativeSnapshotsUITests.swift in Xcode.

Replace:

    XCUIApplication().launch()

With:

    let app = XCUIApplication()
    setupSnapshot(app)
    app.launch()

Step 4: Add snapshot("0Launch") to wherever you want to create the screenshots

Add snapshot call in testExample() in UI Tests.

func testExample() {
    snapshot("0Launch")
}

Edit the Snapfile to avoid a huge matrix.

devices([
  "iPhone 6"
])

languages([
  "en-US"
])

scheme "NativeSnapshots"

It should be ready to go.

$ cd ios && fastlane snapshot

Copied from aj0strow

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!