如何在 React Native 中截图测试?

How do I screenshot tests in React Native?

我想使用屏幕截图测试我的 React Native 应用程序。 UIAutomation javascript 文件将由 fastlane 执行,并且应该为我提供我需要的所有子视图。这部分工作正常。

我的主要问题是我不明白我是如何点击元素的。我找到的每个示例都是简单的 objective-c 并使用标准元素进行导航,例如标签栏。我的应用程序有一个汉堡图标,它在 TouchableHighlight 上有一个点击事件,可以打开一个菜单。我正在寻找引用单个 TouchableHighlight 元素以便与之交互的可能性。

这样的回答加分,不用我写了Objective-C。

Fastlane(更具体的快照)已弃用 UI UI 测试的自动化。如果您需要更新 gem,您的 UIA javascript 将无法用于 UI 测试(用 Obj C 或 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

看起来使用 React Native 的其他人在 UI 测试和快照方面取得了一些进展:https://github.com/fastlane/snapshot/issues/267

我对 fastlane 不熟悉,但你可能想试试 Jest,因为它得到了官方支持。诚然,他们没有完全覆盖,并且在某些情况下你很可能必须推出自己的解决方案,因为 react native 还很年轻,但这应该让你从右脚开始 Snapshot Tests (iOS only)

创建一个新项目。

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

$ react-native init NativeSnapshots

$ cd NativeSnapshots

$ react-native run-ios

测试它是否有效,启动欢迎屏幕。

$ cd ios

$ fastlane snapshot init

快车道输出:

[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

第 1 步:向您的项目添加一个新的 UI 测试目标

Xcode 版本 8.3.3 > 打开 NativeSnapshots.xcodeproj

File > New > Target > iOS UI Testing Bundle

第 2 步:将 ./fastlane/SnapshotHelper.swift 添加到您的 UI 测试目标

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

第 3 步:启动应用程序时调用 setupSnapshot(app)

在 Xcode 中打开 NativeSnapshotsUITests/NativeSnapshotsUITests.swift

替换:

    XCUIApplication().launch()

有:

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

第 4 步:将 snapshot("0Launch") 添加到您要创建屏幕截图的任何位置

在 UI 测试中的 testExample() 中添加快照调用。

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

编辑 Snapfile 以避免巨大的矩阵。

devices([
  "iPhone 6"
])

languages([
  "en-US"
])

scheme "NativeSnapshots"

它应该准备好了。

$ cd ios && fastlane snapshot

复制自aj0strow

注意: 我们正在使用 detox 进行测试,所以我使用 device.getPlatform() 来测试 iOS 或 Android .

我最终做的是 JavaScript 库(pixelmatch and pngjs), using fs 和使用命令行命令(xcrun simctladb)的混合。

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

要改善您的测试结果,您应该使用 Android 的 demo mode,例如:

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

从 xcode 11 开始,您有:

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

我使用以下代码从 iOS 中删除了状态栏(但它会降低性能):

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