From bddcd099796246a122b9fb2c3a70a42ac8634445 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Tue, 29 Jun 2021 12:40:54 -0400 Subject: [PATCH] add packages/detox for e2e (#79) --- .circleci/config.yml | 6 +- .gitignore | 3 + e2e/detox-e2e/jest.config.js | 12 ++ e2e/detox-e2e/test-setup.ts | 22 ++++ e2e/detox-e2e/tests/ios.test.ts | 28 ++++ e2e/detox-e2e/tsconfig.e2e.json | 9 ++ e2e/detox-e2e/tsconfig.json | 13 ++ e2e/detox-e2e/tsconfig.spec.json | 9 ++ e2e/react-native-e2e/jest.config.js | 1 + e2e/react-native-e2e/test-setup.ts | 22 ++++ .../tests/react-native.test.ts | 7 +- nx.json | 7 + package.json | 7 +- packages/detox/.eslintrc | 1 + packages/detox/README.md | 78 +++++++++++ packages/detox/builders.json | 14 ++ packages/detox/collection.json | 19 +++ packages/detox/index.ts | 2 + packages/detox/jest.config.js | 12 ++ packages/detox/migrations.json | 1 + packages/detox/package.json | 39 ++++++ .../detox/src/executors/build/build.impl.ts | 71 ++++++++++ packages/detox/src/executors/build/compat.ts | 5 + .../detox/src/executors/build/schema.d.ts | 5 + .../detox/src/executors/build/schema.json | 19 +++ packages/detox/src/executors/test/compat.ts | 5 + packages/detox/src/executors/test/schema.d.ts | 30 +++++ packages/detox/src/executors/test/schema.json | 120 +++++++++++++++++ .../detox/src/executors/test/test.impl.ts | 77 +++++++++++ .../src/generators/application/application.ts | 34 +++++ .../application/files/app/.babelrc.template | 11 ++ .../files/app/.detoxrc.json.template | 58 +++++++++ .../application/files/app/environment.js | 23 ++++ .../application/files/app/jest.config.json | 12 ++ .../files/app/src/app.spec.ts.template | 16 +++ .../files/app/test-setup.ts.template | 5 + .../application/files/app/tsconfig.e2e.json | 10 ++ .../application/files/app/tsconfig.json | 10 ++ .../application/lib/add-git-ignore-entry.ts | 12 ++ .../generators/application/lib/add-linting.ts | 45 +++++++ .../generators/application/lib/add-project.ts | 76 +++++++++++ .../application/lib/create-files.ts | 13 ++ .../application/lib/normalize-options.spec.ts | 77 +++++++++++ .../application/lib/normalize-options.ts | 50 +++++++ .../src/generators/application/schema.d.ts | 11 ++ .../src/generators/application/schema.json | 50 +++++++ .../detox/src/generators/init/init.spec.ts | 19 +++ packages/detox/src/generators/init/init.ts | 47 +++++++ .../detox/src/generators/init/schema.d.ts | 3 + .../detox/src/generators/init/schema.json | 13 ++ packages/detox/src/utils/versions.ts | 5 + packages/detox/tsconfig.json | 13 ++ packages/detox/tsconfig.lib.json | 11 ++ packages/detox/tsconfig.spec.json | 15 +++ packages/react-native/README.md | 68 ++++++++++ packages/react-native/jest.config.js | 1 + packages/react-native/package.json | 1 + .../src/executors/run-android/schema.d.ts | 1 + .../src/executors/run-ios/schema.d.ts | 1 + .../application/application.spec.ts | 3 + .../src/generators/application/application.ts | 3 + .../app/android/app/build.gradle.template | 8 ++ .../__lowerCaseName__/DetoxTest.java.template | 36 +++++ .../app/src/main/AndroidManifest.xml.template | 1 + .../main/res/xml/network_security_config.xml | 7 + .../{build.gradle => build.gradle.template} | 10 +- .../generators/application/lib/add-detox.ts | 18 +++ .../lib/create-application-files.ts | 13 +- .../application/lib/nomalize-options.spec.ts | 8 ++ .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 6 + .../src/generators/init/init.spec.ts | 10 +- .../react-native/src/generators/init/init.ts | 7 +- .../src/generators/init/schema.d.ts | 1 + .../src/generators/init/schema.json | 6 + .../src/generators/library/library.spec.ts | 1 - .../src/generators/library/library.ts | 1 + .../src/utils/pod-install-task.ts | 4 +- .../src/utils/testing-generators.ts | 5 +- packages/react-native/tsconfig.lib.json | 3 +- tsconfig.base.json | 3 +- workspace.json | 123 ++++++++++++++---- 82 files changed, 1584 insertions(+), 49 deletions(-) create mode 100644 e2e/detox-e2e/jest.config.js create mode 100644 e2e/detox-e2e/test-setup.ts create mode 100644 e2e/detox-e2e/tests/ios.test.ts create mode 100644 e2e/detox-e2e/tsconfig.e2e.json create mode 100644 e2e/detox-e2e/tsconfig.json create mode 100644 e2e/detox-e2e/tsconfig.spec.json create mode 100644 e2e/react-native-e2e/test-setup.ts create mode 100644 packages/detox/.eslintrc create mode 100644 packages/detox/README.md create mode 100644 packages/detox/builders.json create mode 100644 packages/detox/collection.json create mode 100644 packages/detox/index.ts create mode 100644 packages/detox/jest.config.js create mode 100644 packages/detox/migrations.json create mode 100644 packages/detox/package.json create mode 100644 packages/detox/src/executors/build/build.impl.ts create mode 100644 packages/detox/src/executors/build/compat.ts create mode 100644 packages/detox/src/executors/build/schema.d.ts create mode 100644 packages/detox/src/executors/build/schema.json create mode 100644 packages/detox/src/executors/test/compat.ts create mode 100644 packages/detox/src/executors/test/schema.d.ts create mode 100644 packages/detox/src/executors/test/schema.json create mode 100644 packages/detox/src/executors/test/test.impl.ts create mode 100644 packages/detox/src/generators/application/application.ts create mode 100644 packages/detox/src/generators/application/files/app/.babelrc.template create mode 100644 packages/detox/src/generators/application/files/app/.detoxrc.json.template create mode 100644 packages/detox/src/generators/application/files/app/environment.js create mode 100644 packages/detox/src/generators/application/files/app/jest.config.json create mode 100644 packages/detox/src/generators/application/files/app/src/app.spec.ts.template create mode 100644 packages/detox/src/generators/application/files/app/test-setup.ts.template create mode 100644 packages/detox/src/generators/application/files/app/tsconfig.e2e.json create mode 100644 packages/detox/src/generators/application/files/app/tsconfig.json create mode 100644 packages/detox/src/generators/application/lib/add-git-ignore-entry.ts create mode 100644 packages/detox/src/generators/application/lib/add-linting.ts create mode 100644 packages/detox/src/generators/application/lib/add-project.ts create mode 100644 packages/detox/src/generators/application/lib/create-files.ts create mode 100644 packages/detox/src/generators/application/lib/normalize-options.spec.ts create mode 100644 packages/detox/src/generators/application/lib/normalize-options.ts create mode 100644 packages/detox/src/generators/application/schema.d.ts create mode 100644 packages/detox/src/generators/application/schema.json create mode 100644 packages/detox/src/generators/init/init.spec.ts create mode 100644 packages/detox/src/generators/init/init.ts create mode 100644 packages/detox/src/generators/init/schema.d.ts create mode 100644 packages/detox/src/generators/init/schema.json create mode 100644 packages/detox/src/utils/versions.ts create mode 100644 packages/detox/tsconfig.json create mode 100644 packages/detox/tsconfig.lib.json create mode 100644 packages/detox/tsconfig.spec.json create mode 100644 packages/react-native/src/generators/application/files/app/android/app/src/androidTest/java/com/__lowerCaseName__/DetoxTest.java.template create mode 100644 packages/react-native/src/generators/application/files/app/android/app/src/main/res/xml/network_security_config.xml rename packages/react-native/src/generators/application/files/app/android/{build.gradle => build.gradle.template} (80%) create mode 100644 packages/react-native/src/generators/application/lib/add-detox.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 2a4afab..81b9c5e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,7 @@ executors: macos: <<: *defaults macos: - xcode: &_XCODE_VERSION '12.1.0' + xcode: &_XCODE_VERSION '12.5.0' commands: setup: @@ -77,6 +77,10 @@ jobs: steps: - setup: os: << parameters.os >> + - run: + name: Build + command: yarn build + no_output_timeout: 30m - run: name: E2E command: yarn e2e diff --git a/.gitignore b/.gitignore index ee5c9d8..d5e7b88 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ testem.log # System Files .DS_Store Thumbs.db + +# Local Circleci +process.yml \ No newline at end of file diff --git a/e2e/detox-e2e/jest.config.js b/e2e/detox-e2e/jest.config.js new file mode 100644 index 0000000..7f46e21 --- /dev/null +++ b/e2e/detox-e2e/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + maxWorkers: 1, + globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, + displayName: 'e2e-detox', + setupFilesAfterEnv: ['/test-setup.ts'], + testTimeout: 600000, +}; diff --git a/e2e/detox-e2e/test-setup.ts b/e2e/detox-e2e/test-setup.ts new file mode 100644 index 0000000..7520ba2 --- /dev/null +++ b/e2e/detox-e2e/test-setup.ts @@ -0,0 +1,22 @@ +import { join } from 'path'; +import { + ensureNxProject, + patchPackageJsonForPlugin, + runPackageManagerInstall, +} from '@nrwl/nx-plugin/testing'; +import { appRootPath } from '@nrwl/workspace/src/utilities/app-root'; +import { readJsonFile, writeJsonFile } from '@nrwl/devkit'; + +beforeAll(() => { + // delete @nrlw/detox form react-native package.json so it will not pull @nrwl/detox from npm registry and use local one + const path = join(appRootPath, 'dist/packages/react-native/package.json'); + const json = readJsonFile(path); + delete json.dependencies['@nrwl/detox']; + writeJsonFile(path, json); +}); + +beforeEach(() => { + ensureNxProject('@nrwl/detox', 'dist/packages/detox'); + patchPackageJsonForPlugin('@nrwl/react-native', 'dist/packages/react-native'); + runPackageManagerInstall(); +}); diff --git a/e2e/detox-e2e/tests/ios.test.ts b/e2e/detox-e2e/tests/ios.test.ts new file mode 100644 index 0000000..2cb7f5f --- /dev/null +++ b/e2e/detox-e2e/tests/ios.test.ts @@ -0,0 +1,28 @@ +import { readJsonFile, writeJsonFile } from '@nrwl/devkit'; +import { runNxCommandAsync, uniq } from '@nrwl/nx-plugin/testing'; +import { platform } from 'os'; + +describe('Detox iOS', () => { + // Currently there is known issue that for react native 0.65rc, Folly dual symbols preventing ios from building successfully. + xtest('should pass detox e2e tests on ios', async () => { + if (platform() !== 'darwin') { + return; + } + const myapp = uniq('myapp'); + await runNxCommandAsync( + `generate @nrwl/react-native:app ${myapp} --e2eTestRunner=detox --linter=eslint` + ); + + const androidBuildResult = await runNxCommandAsync( + `build-ios ${myapp}-e2e` + ); + expect(androidBuildResult.stdout).toContain('BUILD SUCCESS'); + + const androidTestResult = await runNxCommandAsync( + `test-ios ${myapp}-e2e --cleanup` + ); + expect(androidTestResult.stdout).toContain( + 'Running target "test-ios" succeeded' + ); + }); +}); diff --git a/e2e/detox-e2e/tsconfig.e2e.json b/e2e/detox-e2e/tsconfig.e2e.json new file mode 100644 index 0000000..2d3adf6 --- /dev/null +++ b/e2e/detox-e2e/tsconfig.e2e.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.test.ts"] +} diff --git a/e2e/detox-e2e/tsconfig.json b/e2e/detox-e2e/tsconfig.json new file mode 100644 index 0000000..879cca4 --- /dev/null +++ b/e2e/detox-e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.e2e.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/detox-e2e/tsconfig.spec.json b/e2e/detox-e2e/tsconfig.spec.json new file mode 100644 index 0000000..29efa43 --- /dev/null +++ b/e2e/detox-e2e/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/e2e/react-native-e2e/jest.config.js b/e2e/react-native-e2e/jest.config.js index 8e17050..91292a0 100644 --- a/e2e/react-native-e2e/jest.config.js +++ b/e2e/react-native-e2e/jest.config.js @@ -7,5 +7,6 @@ module.exports = { maxWorkers: 1, globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, displayName: 'e2e-react-native', + setupFilesAfterEnv: ['/test-setup.ts'], testTimeout: 600000, }; diff --git a/e2e/react-native-e2e/test-setup.ts b/e2e/react-native-e2e/test-setup.ts new file mode 100644 index 0000000..7520ba2 --- /dev/null +++ b/e2e/react-native-e2e/test-setup.ts @@ -0,0 +1,22 @@ +import { join } from 'path'; +import { + ensureNxProject, + patchPackageJsonForPlugin, + runPackageManagerInstall, +} from '@nrwl/nx-plugin/testing'; +import { appRootPath } from '@nrwl/workspace/src/utilities/app-root'; +import { readJsonFile, writeJsonFile } from '@nrwl/devkit'; + +beforeAll(() => { + // delete @nrlw/detox form react-native package.json so it will not pull @nrwl/detox from npm registry and use local one + const path = join(appRootPath, 'dist/packages/react-native/package.json'); + const json = readJsonFile(path); + delete json.dependencies['@nrwl/detox']; + writeJsonFile(path, json); +}); + +beforeEach(() => { + ensureNxProject('@nrwl/detox', 'dist/packages/detox'); + patchPackageJsonForPlugin('@nrwl/react-native', 'dist/packages/react-native'); + runPackageManagerInstall(); +}); diff --git a/e2e/react-native-e2e/tests/react-native.test.ts b/e2e/react-native-e2e/tests/react-native.test.ts index ff148ee..5228857 100644 --- a/e2e/react-native-e2e/tests/react-native.test.ts +++ b/e2e/react-native-e2e/tests/react-native.test.ts @@ -1,7 +1,6 @@ import { checkFilesExist, - ensureNxProject, - readFile, + readJson, runNxCommandAsync, uniq, updateFile, @@ -10,7 +9,6 @@ import { join } from 'path'; test('create ios and android JS bundles', async () => { const appName = uniq('my-app'); - ensureNxProject('@nrwl/react-native', 'dist/packages/react-native'); await runNxCommandAsync(`generate @nrwl/react-native:application ${appName}`); await expect(runNxCommandAsync(`test ${appName}`)).resolves.toMatchObject({ @@ -34,7 +32,6 @@ test('create ios and android JS bundles', async () => { test('sync npm dependencies for autolink', async () => { const appName = uniq('my-app'); - ensureNxProject('@nrwl/react-native', 'dist/packages/react-native'); await runNxCommandAsync(`generate @nrwl/react-native:application ${appName}`); // Add npm package with native modules updateFile(join('package.json'), (content) => { @@ -53,7 +50,7 @@ test('sync npm dependencies for autolink', async () => { `sync-deps ${appName} --include=react-native-gesture-handler,react-native-safe-area-context` ); - const result = JSON.parse(readFile(join('apps', appName, 'package.json'))); + const result = readJson(join('apps', appName, 'package.json')); expect(result).toMatchObject({ dependencies: { 'react-native-image-picker': '*', diff --git a/nx.json b/nx.json index b5d2871..824fae3 100644 --- a/nx.json +++ b/nx.json @@ -22,6 +22,13 @@ } }, "projects": { + "detox": { + "tags": [] + }, + "detox-e2e": { + "tags": [], + "implicitDependencies": ["detox"] + }, "react-native": { "tags": [] }, diff --git a/package.json b/package.json index 41bd344..78777a3 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "scripts": { "nx": "nx", "start": "nx serve", - "build": "nx build react-native", + "build": "nx build detox && nx build react-native", "test": "nx run-many --target=test --all --parallel", "lint": "nx workspace-lint && nx run-many --target=lint --all --parallel", - "e2e": "nx e2e react-native-e2e", + "e2e": "nx e2e react-native-e2e && nx e2e detox-e2e", + "detox-e2e": "nx e2e detox-e2e", "release": "./tools/scripts/publish.sh", "local-registry": "./tools/scripts/local-registry.sh", "affected:apps": "nx affected:apps", @@ -26,7 +27,7 @@ "update": "nx migrate latest", "dep-graph": "nx dep-graph", "help": "nx help", - "doctoc": "doctoc packages/react-native/README.md", + "doctoc": "doctoc packages/react-native/README.md && doctoc packages/detox/README.md", "workspace-generator": "nx workspace-generator" }, "dependencies": {}, diff --git a/packages/detox/.eslintrc b/packages/detox/.eslintrc new file mode 100644 index 0000000..ab8f383 --- /dev/null +++ b/packages/detox/.eslintrc @@ -0,0 +1 @@ +{ "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] } diff --git a/packages/detox/README.md b/packages/detox/README.md new file mode 100644 index 0000000..fa7989d --- /dev/null +++ b/packages/detox/README.md @@ -0,0 +1,78 @@ +# Detox Plugin for Nx + +[![License](https://img.shields.io/npm/l/@nrwl/workspace.svg?style=flat-square)]() +[![NPM Version](https://badge.fury.io/js/%40nrwl%2Fdetox.svg)](https://www.npmjs.com/@nrwl/detox) +[![Join the chat at https://gitter.im/nrwl-nx/community](https://badges.gitter.im/nrwl-nx/community.svg)](https://gitter.im/nrwl-nx/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join us @nrwl/community on slack](https://img.shields.io/badge/slack-%40nrwl%2Fcommunity-brightgreen)](https://join.slack.com/t/nrwlcommunity/shared_invite/enQtNzU5MTE4OTQwOTk0LTgxY2E0ZWYzMWE0YzA5ZDA2MWM1NDVhNmI2ZWMyYmZhNWJiODk3MjkxZjY3MzU5ZjRmM2NmNWU1OTgyZmE4Mzc) + + + +## Table of Contents + + + + +- [Setup](#setup) + - [Install applesimutils (Mac only)](#install-applesimutils-mac-only) + - [Install Jest Globally](#install-jest-globally) + - [Commands](#commands) + - [Manually Add E2E Folder](#manually-add-e2e-folder) + - [Change Testing Simulator/Emulator](#change-testing-simulatoremulator) +- [Learn more](#learn-more) + + + +## Setup + +#### Install applesimutils (Mac only) + +[applesimutils](https://github.com/wix/AppleSimulatorUtils) is a collection of utils for Apple simulators. + +```sh +brew tap wix/brew +brew install applesimutils +``` + +#### Install Jest Globally + +```sh +npm install -g jest +``` + +### Commands + +A built app must exist before run test commands. + +- `nx build-ios `: build the iOS app (Mac only) +- `nx test-ios `: run e2e tests on the built iOS app (Mac only) +- `nx build-ios --prod` and `nx test-ios --prod`: build and run release version of iOS app. Note: you might need open the xcode project under iOS and choose a team under "Sign & Capabilities". +- `nx build-android `: build the android app +- `nx test-android `: run e2e tests on the built android app +- `nx build-android --prod` and `nx test-android --prod`: build and run release version of android app. + +### Manually Add E2E Folder + +A `` folder is automatically generate when you create a react native app. However, if you want to add e2e folder manually, you need to: + +- Install @nrwl/detox + + ```sh + # Using npm + npm install --save-dev @nrwl/detox + + # Using yarn + yarn add -D @nrwl/detox + ``` + +- Run `nx generate @nrwl/detox:app ` +- Follow instructions https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md to manully change android files. + +### Change Testing Simulator/Emulator + +For iOS, in terminal, run `xcrun simctl list` to view a list of simulators on your Mac. To open your active simulator, `run open -a simulator`. In `/.detoxrc.json`, you could change the simulator under `devices.simulator.device`. + +For Android: in terminal, run `emulator -list-avds` to view a list of emulators installed. To open your emulator, run `emulator -avd `. In `/.detoxrc.json`, you could change the simulator under `devices.emulator.device`. + +## Learn more + +Visit the [Nx Documentation](https://nx.dev) to learn more. diff --git a/packages/detox/builders.json b/packages/detox/builders.json new file mode 100644 index 0000000..6341593 --- /dev/null +++ b/packages/detox/builders.json @@ -0,0 +1,14 @@ +{ + "executors": { + "build": { + "implementation": "./src/executors/build/build.impl", + "schema": "./src/executors/build/schema.json", + "description": "Run the command defined in build property of the specified configuration." + }, + "test": { + "implementation": "./src/executors/test/test.impl", + "schema": "./src/executors/test/schema.json", + "description": "Initiating your detox test suite." + } + } +} diff --git a/packages/detox/collection.json b/packages/detox/collection.json new file mode 100644 index 0000000..7b4717a --- /dev/null +++ b/packages/detox/collection.json @@ -0,0 +1,19 @@ +{ + "name": "Nx Detox", + "version": "0.1", + "extends": ["@nrwl/workspace"], + "schematics": { + "init": { + "factory": "./src/generators/init/init#detoxInitGenerator", + "schema": "./src/generators/init/schema.json", + "description": "Initialize the @nrwl/detox plugin", + "hidden": true + }, + "application": { + "factory": "./src/generators/application/application#detoxApplicationSchematic", + "schema": "./src/generators/application/schema.json", + "aliases": ["app"], + "description": "Create an application" + } + } +} diff --git a/packages/detox/index.ts b/packages/detox/index.ts new file mode 100644 index 0000000..7edbccf --- /dev/null +++ b/packages/detox/index.ts @@ -0,0 +1,2 @@ +export { detoxInitGenerator } from './src/generators/init/init'; +export { detoxApplicationGenerator } from './src/generators/application/application'; diff --git a/packages/detox/jest.config.js b/packages/detox/jest.config.js new file mode 100644 index 0000000..32aef31 --- /dev/null +++ b/packages/detox/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], + globals: { + 'ts-jest': { tsconfig: '/tsconfig.spec.json' }, + }, + displayName: 'react-native', + testEnvironment: 'node', +}; diff --git a/packages/detox/migrations.json b/packages/detox/migrations.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/detox/migrations.json @@ -0,0 +1 @@ +{} diff --git a/packages/detox/package.json b/packages/detox/package.json new file mode 100644 index 0000000..6e6c1ac --- /dev/null +++ b/packages/detox/package.json @@ -0,0 +1,39 @@ +{ + "name": "@nrwl/detox", + "version": "12.4.0", + "description": "Detox Plugin for Nx", + "keywords": [ + "Monorepo", + "React", + "Web", + "Native", + "CLI", + "Detox" + ], + "homepage": "https://nx.dev", + "bugs": { + "url": "https://github.com/nrwl/nx-react-native/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nrwl/nx-react-native.git" + }, + "license": "MIT", + "author": "Victor Savkin", + "main": "index.js", + "types": "index.d.ts", + "dependencies": { + "@nrwl/jest": "12.3.6", + "@nrwl/linter": "12.3.6", + "chalk": "^4.1.0" + }, + "peerDependencies": { + "@nrwl/workspace": "*" + }, + "builders": "./builders.json", + "ng-update": { + "requirements": {}, + "migrations": "./migrations.json" + }, + "schematics": "./collection.json" +} \ No newline at end of file diff --git a/packages/detox/src/executors/build/build.impl.ts b/packages/detox/src/executors/build/build.impl.ts new file mode 100644 index 0000000..8bf8501 --- /dev/null +++ b/packages/detox/src/executors/build/build.impl.ts @@ -0,0 +1,71 @@ +import { ExecutorContext } from '@nrwl/tao/src/shared/workspace'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { DetoxBuildOptions } from './schema'; + +export interface DetoxBuildOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* detoxBuildExecutor( + options: DetoxBuildOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + + try { + await runCliBuild(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliBuild( + workspaceRoot: string, + projectRoot: string, + options: DetoxBuildOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/detox/local-cli/cli.js'), + ['build', ...createDetoxBuildOptions(options)], + { + cwd: projectRoot, + } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +function createDetoxBuildOptions(options) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (k === 'detoxConfiguration') { + acc.push('--configuration', v); + } else if (k === 'configPath') { + acc.push('--config-path', v); + } else acc.push(`--${k}`, options[k]); + return acc; + }, []); +} diff --git a/packages/detox/src/executors/build/compat.ts b/packages/detox/src/executors/build/compat.ts new file mode 100644 index 0000000..87700b8 --- /dev/null +++ b/packages/detox/src/executors/build/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import detoxBuildExecutor from './build.impl'; + +export default convertNxExecutor(detoxBuildExecutor); diff --git a/packages/detox/src/executors/build/schema.d.ts b/packages/detox/src/executors/build/schema.d.ts new file mode 100644 index 0000000..7a8231e --- /dev/null +++ b/packages/detox/src/executors/build/schema.d.ts @@ -0,0 +1,5 @@ +// options from https://github.com/wix/Detox/blob/master/docs/APIRef.DetoxCLI.md#build +export interface DetoxBuildOptions { + detoxConfiguration?: string; + configPath?: string; +} diff --git a/packages/detox/src/executors/build/schema.json b/packages/detox/src/executors/build/schema.json new file mode 100644 index 0000000..c5b9c60 --- /dev/null +++ b/packages/detox/src/executors/build/schema.json @@ -0,0 +1,19 @@ +{ + "title": "Run detox build", + "description": "Run detox build options", + "type": "object", + "cli": "nx", + "properties": { + "detoxConfiguration": { + "type": "string", + "description": "Select a device configuration from your defined configurations, if not supplied, and there's only one configuration, detox will default to it", + "alias": "C" + }, + "configPath": { + "type": "string", + "description": "Specify Detox config file path. If not supplied, detox searches for .detoxrc[.js] or \"detox\" section in package.json", + "alias": "cp" + } + }, + "required": [] +} diff --git a/packages/detox/src/executors/test/compat.ts b/packages/detox/src/executors/test/compat.ts new file mode 100644 index 0000000..1409e42 --- /dev/null +++ b/packages/detox/src/executors/test/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import detoxTestExecutor from './test.impl'; + +export default convertNxExecutor(detoxTestExecutor); diff --git a/packages/detox/src/executors/test/schema.d.ts b/packages/detox/src/executors/test/schema.d.ts new file mode 100644 index 0000000..3ca658e --- /dev/null +++ b/packages/detox/src/executors/test/schema.d.ts @@ -0,0 +1,30 @@ +// options from https://github.com/wix/Detox/blob/master/docs/APIRef.DetoxCLI.md#test +// detox test args: https://github.com/wix/Detox/blob/master/detox/local-cli/utils/testCommandArgs.js +export interface DetoxTestOptions { + configPath: string; + detoxConfiguration: string; + runnerConfig: string; + deviceName: string; + loglevel: string; + debugSynchronization: string; + artifactsLocation: string; + recordLogs: 'failing' | 'all' | 'none'; + takeScreenshots: 'manual' | 'failing' | 'all' | 'none'; + recordVideos: 'failing' | 'all' | 'none'; + recordPerformance: 'all' | 'none'; + recordTimeline: 'all' | 'none'; + captureViewHierarchy: 'enabled' | 'disabled'; + retries: number; + resuse: boolean; + cleanup: boolean; + workers: number; + jestReportSpecs: boolean; + headeless: boolean; + gpu: boolean; + deviceLauchArgs: string; + appLaunchArgs: string; + noColor: boolean; + useCutsomeLogger: boolean; + forceAdbInstall: boolean; + inspectBrk: boolean; +} diff --git a/packages/detox/src/executors/test/schema.json b/packages/detox/src/executors/test/schema.json new file mode 100644 index 0000000..599366f --- /dev/null +++ b/packages/detox/src/executors/test/schema.json @@ -0,0 +1,120 @@ +{ + "title": "Run detox test", + "description": "Run detox test options", + "type": "object", + "cli": "nx", + "properties": { + "detoxConfiguration": { + "type": "string", + "description": "Select a device configuration from your defined configurations, if not supplied, and there's only one configuration, detox will default to it", + "alias": "C" + }, + "configPath": { + "type": "string", + "description": "Specify Detox config file path. If not supplied, detox searches for .detoxrc[.js] or \"detox\" section in package.json", + "alias": "cp" + }, + "runnerConfig": { + "type": "string", + "description": "Test runner config file, defaults to 'e2e/mocha.opts' for mocha and 'e2e/config.json' for jest.", + "alias": "o" + }, + "deviceName": { + "type": "string", + "description": "Override the device name specified in a configuration. Useful for running a single build configuration on multiple devices.", + "alias": "n" + }, + "loglevel": { + "type": "string", + "description": "Log level: fatal, error, warn, info, verbose, trace", + "alias": "l" + }, + "debugSynchronization": { + "type": "string", + "description": "Customize how long an action/expectation can take to complete before Detox starts querying the app why it is busy. By default, the app status will be printed if the action takes more than 10s to complete.", + "alias": "d" + }, + "artifactsLocation": { + "type": "string", + "description": "Artifacts (logs, screenshots, etc) root directory.", + "alias": "a" + }, + "recordLogs": { + "type": "string", + "description": "Save logs during each test to artifacts directory. Pass \"failing\" to save logs of failing tests only." + }, + "takeScreenshots": { + "type": "string", + "description": "Save screenshots before and after each test to artifacts directory. Pass \"failing\" to save screenshots of failing tests only. " + }, + "recordVideos": { + "type": "string", + "description": "Save screen recordings of each test to artifacts directory. Pass \"failing\" to save recordings of failing tests only." + }, + "recordPerformance": { + "type": "string", + "description": "[iOS Only] Save Detox Instruments performance recordings of each test to artifacts directory." + }, + "recordTimeline": { + "type": "string", + "description": "[Jest Only] Record tests and events timeline, for visual display on the chrome://tracing tool." + }, + "captureViewHierarchy": { + "type": "string", + "description": "[iOS Only] Capture *.uihierarchy snapshots on view action errors and device.captureViewHierarchy() calls." + }, + "retries": { + "type": "number", + "description": "[Jest Circus Only] Re-spawn the test runner for individual failing suite files until they pass, or times at least." + }, + "reuse": { + "type": "boolean", + "description": "Reuse existing installed app (do not delete + reinstall) for a faster run." + }, + "cleanup": { + "type": "boolean", + "description": "Shutdown simulator when test is over, useful for CI scripts, to make sure detox exists cleanly with no residue" + }, + "workers": { + "type": "number", + "description": "Specifies number of workers the test runner should spawn, requires a test runner with parallel execution support (Detox CLI currently supports Jest)." + }, + "jestReportSpecs": { + "type": "boolean", + "description": "[Jest Only] Whether to output logs per each running spec, in real-time. By default, disabled with multiple workers." + }, + "headless": { + "type": "boolean", + "description": "Android Only] Launch Emulator in headless mode. Useful when running on CI." + }, + "gpu": { + "type": "boolean", + "description": "[Android Only] Launch Emulator with the specific -gpu [gpu mode] parameter." + }, + "deviceLaunchArgs": { + "type": "string", + "description": "A list of passthrough-arguments to use when (if) devices (Android emulator / iOS simulator) are launched by Detox." + }, + "appLaunchArgs": { + "type": "number", + "description": "Custom arguments to pass (through) onto the app every time it is launched." + }, + "noColor": { + "type": "boolean", + "description": "Disable colors in log output" + }, + "useCustomLogger": { + "type": "boolean", + "description": "Use Detox' custom console-logging implementation, for logging Detox (non-device) logs. Disabling will fallback to node.js / test-runner's implementation (e.g. Jest / Mocha)." + }, + "forceAdbInstall": { + "type": "boolean", + "description": "Due to problems with the adb install command on Android, Detox resorts to a different scheme for install APK's. Setting true will disable that and force usage of adb install, instead." + }, + "inspectBrk": { + "type": "boolean", + "description": "Uses node's --inspect-brk flag to let users debug the jest/mocha test runner" + } + }, + "required": [] +} diff --git a/packages/detox/src/executors/test/test.impl.ts b/packages/detox/src/executors/test/test.impl.ts new file mode 100644 index 0000000..c2931a9 --- /dev/null +++ b/packages/detox/src/executors/test/test.impl.ts @@ -0,0 +1,77 @@ +import { ExecutorContext } from '@nrwl/tao/src/shared/workspace'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { DetoxTestOptions } from './schema'; +import { toFileName } from '@nrwl/workspace'; + +export interface DetoxTestOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* detoxTestExecutor( + options: DetoxTestOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + + try { + await runCliTest(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliTest( + workspaceRoot: string, + projectRoot: string, + options: DetoxTestOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/detox/local-cli/cli.js'), + ['test', ...createDetoxTestOptions(options)], + { + cwd: projectRoot, + } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +function createDetoxTestOptions(options: DetoxTestOptions): string[] { + return Object.keys(options).reduce((acc, k) => { + const propertyName = toFileName(k); // convert camelCase to kebab-case + const propertyValue = options[k]; + if (k === 'detoxConfiguration') { + acc.push('--configuration', propertyValue); + } else if (k === 'deviceLaunchArgs') { + acc.push(`--device-launch-args="${propertyValue}"`); // the value must be specified after an equal sign (=) and inside quotes. + } else if (k === 'appLaunchArgs') { + acc.push(`--app-launch-argss="${propertyValue}"`); // the value must be specified after an equal sign (=) and inside quotes. + } else { + acc.push(`--${propertyName}`, propertyValue); + } + return acc; + }, []); +} diff --git a/packages/detox/src/generators/application/application.ts b/packages/detox/src/generators/application/application.ts new file mode 100644 index 0000000..cc1c60a --- /dev/null +++ b/packages/detox/src/generators/application/application.ts @@ -0,0 +1,34 @@ +import { convertNxGenerator, formatFiles, Tree } from '@nrwl/devkit'; + +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import detoxInitGenerator from '../init/init'; +import { addGitIgnoreEntry } from './lib/add-git-ignore-entry'; +import { addLinting } from './lib/add-linting'; +import { addProject } from './lib/add-project'; +import { createFiles } from './lib/create-files'; +import { normalizeOptions } from './lib/normalize-options'; +import { Schema } from './schema'; + +export async function detoxApplicationGenerator(host: Tree, schema: Schema) { + const options = normalizeOptions(host, schema); + + const initTask = await detoxInitGenerator(host, { + skipFormat: true, + }); + createFiles(host, options); + addProject(host, options); + addGitIgnoreEntry(host, options); + + const lintingTask = await addLinting(host, options); + + if (!options.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial(initTask, lintingTask); +} + +export default detoxApplicationGenerator; +export const detoxApplicationSchematic = convertNxGenerator( + detoxApplicationGenerator +); diff --git a/packages/detox/src/generators/application/files/app/.babelrc.template b/packages/detox/src/generators/application/files/app/.babelrc.template new file mode 100644 index 0000000..61641ec --- /dev/null +++ b/packages/detox/src/generators/application/files/app/.babelrc.template @@ -0,0 +1,11 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic" + } + ] + ], + "plugins": [] +} diff --git a/packages/detox/src/generators/application/files/app/.detoxrc.json.template b/packages/detox/src/generators/application/files/app/.detoxrc.json.template new file mode 100644 index 0000000..cd0ba6a --- /dev/null +++ b/packages/detox/src/generators/application/files/app/.detoxrc.json.template @@ -0,0 +1,58 @@ +{ + "testRunner": "jest", + "runnerConfig": "jest.config.json", + "apps": { + "ios.debug": { + "type": "ios.app", + "build": "cd ../<%= appFileName %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Debug -derivedDataPath ./build -quiet", + "binaryPath": "../<%= appFileName %>/ios/build/Build/Products/Debug-iphonesimulator/<%= appClassName %>.app" + }, + "ios.release": { + "type": "ios.app", + "build": "cd ../<%= appFileName %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Release -derivedDataPath ./build -quiet", + "binaryPath": "../<%= appFileName %>/ios/build/Build/Products/Release-iphonesimulator/<%= appClassName %>.app" + }, + "android.debug": { + "type": "android.apk", + "build": "cd ../<%= appFileName %>/android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug", + "binaryPath": "../<%= appFileName %>/android/app/build/outputs/apk/debug/app-debug.apk" + }, + "android.release": { + "type": "android.apk", + "build": "cd ../<%= appFileName %>/android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release", + "binaryPath": "../<%= appFileName %>/android/app/build/outputs/apk/release/app-release.apk" + } + }, + "devices": { + "simulator": { + "type": "ios.simulator", + "device": { + "type": "iPhone 12" + } + }, + "emulator": { + "type": "android.emulator", + "device": { + "avdName": "Pixel_4a_API_30" + } + } + }, + "configurations": { + "ios.sim.release": { + "device": "simulator", + "app": "ios.release" + }, + "ios.sim.debug": { + "device": "simulator", + "app": "ios.debug" + }, + "android.emu.release": { + "device": "emulator", + "app": "android.release" + }, + "android.emu.debug": { + "device": "emulator", + "app": "android.debug" + } + } +} diff --git a/packages/detox/src/generators/application/files/app/environment.js b/packages/detox/src/generators/application/files/app/environment.js new file mode 100644 index 0000000..7f4fc94 --- /dev/null +++ b/packages/detox/src/generators/application/files/app/environment.js @@ -0,0 +1,23 @@ +const { + DetoxCircusEnvironment, + SpecReporter, + WorkerAssignReporter, +} = require('detox/runners/jest-circus'); + +class CustomDetoxEnvironment extends DetoxCircusEnvironment { + constructor(config, context) { + super(config, context); + + // Can be safely removed, if you are content with the default value (=300000ms) + this.initTimeout = 300000; + + // This takes care of generating status logs on a per-spec basis. By default, Jest only reports at file-level. + // This is strictly optional. + this.registerListeners({ + SpecReporter, + WorkerAssignReporter, + }); + } +} + +module.exports = CustomDetoxEnvironment; diff --git a/packages/detox/src/generators/application/files/app/jest.config.json b/packages/detox/src/generators/application/files/app/jest.config.json new file mode 100644 index 0000000..1d9956f --- /dev/null +++ b/packages/detox/src/generators/application/files/app/jest.config.json @@ -0,0 +1,12 @@ +{ + "preset": "../../jest.preset", + "testEnvironment": "./environment", + "testRunner": "jest-circus/runner", + "testTimeout": 120000, + "reporters": ["detox/runners/jest/streamlineReporter"], + "setupFilesAfterEnv": ["/test-setup.ts"], + "transform": { + "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nrwl/react/plugins/jest", + "^.+\\.[tj]sx?$": "babel-jest" + } +} diff --git a/packages/detox/src/generators/application/files/app/src/app.spec.ts.template b/packages/detox/src/generators/application/files/app/src/app.spec.ts.template new file mode 100644 index 0000000..df78737 --- /dev/null +++ b/packages/detox/src/generators/application/files/app/src/app.spec.ts.template @@ -0,0 +1,16 @@ +import { device, element, by, expect } from 'detox'; + +describe('<%= appClassName %>', () => { + beforeEach(async () => { + await device.reloadReactNative(); + }); + + it('should display welcome message', async () => { + await expect(element(by.id('heading'))).toHaveText('Welcome to <%= appClassName %>'); + }); + + it('should open nx link', async () => { + await expect(element(by.id('nx-link'))).toBeVisible; + element(by.id('nx-link')).tap(); + }); +}); diff --git a/packages/detox/src/generators/application/files/app/test-setup.ts.template b/packages/detox/src/generators/application/files/app/test-setup.ts.template new file mode 100644 index 0000000..a4e12aa --- /dev/null +++ b/packages/detox/src/generators/application/files/app/test-setup.ts.template @@ -0,0 +1,5 @@ +import { device } from 'detox'; + +beforeAll(async () => { + await device.launchApp(); +}); diff --git a/packages/detox/src/generators/application/files/app/tsconfig.e2e.json b/packages/detox/src/generators/application/files/app/tsconfig.e2e.json new file mode 100644 index 0000000..8d2d165 --- /dev/null +++ b/packages/detox/src/generators/application/files/app/tsconfig.e2e.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "allowJs": true, + "types": ["node", "jest", "detox"] + }, + "include": ["src/**/*.ts", "src/**/*.js"] +} diff --git a/packages/detox/src/generators/application/files/app/tsconfig.json b/packages/detox/src/generators/application/files/app/tsconfig.json new file mode 100644 index 0000000..c31c52e --- /dev/null +++ b/packages/detox/src/generators/application/files/app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.e2e.json" + } + ] +} diff --git a/packages/detox/src/generators/application/lib/add-git-ignore-entry.ts b/packages/detox/src/generators/application/lib/add-git-ignore-entry.ts new file mode 100644 index 0000000..57cbe9b --- /dev/null +++ b/packages/detox/src/generators/application/lib/add-git-ignore-entry.ts @@ -0,0 +1,12 @@ +import { logger, Tree } from '@nrwl/devkit'; +import { NormalizedSchema } from './normalize-options'; + +export function addGitIgnoreEntry(host: Tree, options: NormalizedSchema) { + if (host.exists('.gitignore')) { + let content = host.read('.gitignore', 'utf-8'); + content = `${content}\n${options.projectRoot}/artifacts\n`; + host.write('.gitignore', content); + } else { + logger.warn(`Couldn't find .gitignore file to update`); + } +} diff --git a/packages/detox/src/generators/application/lib/add-linting.ts b/packages/detox/src/generators/application/lib/add-linting.ts new file mode 100644 index 0000000..8c88285 --- /dev/null +++ b/packages/detox/src/generators/application/lib/add-linting.ts @@ -0,0 +1,45 @@ +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import { Linter, lintProjectGenerator } from '@nrwl/linter'; +import { + addDependenciesToPackageJson, + joinPathFragments, + updateJson, + Tree, +} from '@nrwl/devkit'; +import { extraEslintDependencies, createReactEslintJson } from '@nrwl/react'; +import { NormalizedSchema } from './normalize-options'; + +export async function addLinting(host: Tree, options: NormalizedSchema) { + const lintTask = await lintProjectGenerator(host, { + linter: options.linter, + project: options.projectName, + tsConfigPaths: [ + joinPathFragments(options.projectRoot, 'tsconfig.app.json'), + ], + eslintFilePatterns: [`${options.projectRoot}/**/*.{ts,tsx,js,jsx}`], + skipFormat: true, + }); + + if (options.linter === Linter.TsLint) { + return () => {}; + } + + const reactEslintJson = createReactEslintJson( + options.projectRoot, + options.setParserOptionsProject + ); + + updateJson( + host, + joinPathFragments(options.projectRoot, '.eslintrc.json'), + () => reactEslintJson + ); + + const installTask = await addDependenciesToPackageJson( + host, + extraEslintDependencies.dependencies, + extraEslintDependencies.devDependencies + ); + + return runTasksInSerial(lintTask, installTask); +} diff --git a/packages/detox/src/generators/application/lib/add-project.ts b/packages/detox/src/generators/application/lib/add-project.ts new file mode 100644 index 0000000..b9f8b31 --- /dev/null +++ b/packages/detox/src/generators/application/lib/add-project.ts @@ -0,0 +1,76 @@ +import { + addProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; +import { NormalizedSchema } from './normalize-options'; + +export function addProject(host: Tree, options: NormalizedSchema) { + addProjectConfiguration(host, options.projectName, { + root: options.projectRoot, + sourceRoot: `${options.projectRoot}/src`, + projectType: 'application', + targets: { ...getTargets(options) }, + }); +} + +function getTargets(options: NormalizedSchema) { + const architect: { [key: string]: TargetConfiguration } = {}; + + architect['build-ios'] = { + executor: '@nrwl/detox:build', + options: { + detoxConfiguration: 'ios.sim.debug', + }, + configurations: { + production: { + detoxConfiguration: 'ios.sim.release', + }, + }, + }; + + architect['test-ios'] = { + executor: '@nrwl/detox:test', + options: { + detoxConfiguration: 'ios.sim.debug', + }, + configurations: { + production: { + detoxConfiguration: 'ios.sim.release', + }, + }, + }; + + architect['build-android'] = { + executor: '@nrwl/detox:build', + options: { + detoxConfiguration: 'android.emu.debug', + }, + configurations: { + production: { + detoxConfiguration: 'android.emu.release', + }, + }, + }; + + architect['test-android'] = { + executor: '@nrwl/detox:test', + options: { + detoxConfiguration: 'android.emu.debug', + }, + configurations: { + production: { + detoxConfiguration: 'android.emu.release', + }, + }, + }; + + architect['lint'] = { + executor: '@nrwl/linter:eslint', + options: { + lintFilePatterns: [`${options.projectRoot}/**/*.{js,ts}`], + }, + }; + + return architect; +} diff --git a/packages/detox/src/generators/application/lib/create-files.ts b/packages/detox/src/generators/application/lib/create-files.ts new file mode 100644 index 0000000..c625f98 --- /dev/null +++ b/packages/detox/src/generators/application/lib/create-files.ts @@ -0,0 +1,13 @@ +import { generateFiles, offsetFromRoot, toJS, Tree } from '@nrwl/devkit'; +import { join } from 'path'; +import { NormalizedSchema } from './normalize-options'; + +export function createFiles(host: Tree, options: NormalizedSchema) { + generateFiles(host, join(__dirname, '../files/app'), options.projectRoot, { + ...options, + offsetFromRoot: offsetFromRoot(options.projectRoot), + }); + if (options.js) { + toJS(host); + } +} diff --git a/packages/detox/src/generators/application/lib/normalize-options.spec.ts b/packages/detox/src/generators/application/lib/normalize-options.spec.ts new file mode 100644 index 0000000..d4ae9e7 --- /dev/null +++ b/packages/detox/src/generators/application/lib/normalize-options.spec.ts @@ -0,0 +1,77 @@ +import { addProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Linter } from '@nrwl/linter'; +import { Schema } from '../schema'; +import { normalizeOptions } from './normalize-options'; + +describe('Normalize Options', () => { + let appTree: Tree; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + }); + + it('should normalize options with name in kebab case', () => { + addProjectConfiguration(appTree, 'my-app', { + root: 'apps/my-app', + targets: {}, + }); + const schema: Schema = { + name: 'my-app-e2e', + project: 'my-app', + linter: Linter.EsLint, + }; + const options = normalizeOptions(appTree, schema); + expect(options).toEqual({ + name: 'my-app-e2e', + projectName: 'my-app-e2e', + projectRoot: 'apps/my-app-e2e', + project: 'my-app', + appFileName: 'my-app', + appClassName: 'MyApp', + linter: Linter.EsLint, + }); + }); + + it('should normalize options with name in camel case', () => { + addProjectConfiguration(appTree, 'my-app', { + root: 'apps/my-app', + targets: {}, + }); + const schema: Schema = { + name: 'myAppE2e', + project: 'myApp', + }; + const options = normalizeOptions(appTree, schema); + expect(options).toEqual({ + appClassName: 'MyApp', + appFileName: 'my-app', + name: 'my-app-e2e', + project: 'myApp', + projectName: 'my-app-e2e', + projectRoot: 'apps/my-app-e2e', + }); + }); + + it('should normalize options with directory', () => { + addProjectConfiguration(appTree, 'my-app', { + root: 'apps/my-app', + targets: {}, + }); + const schema: Schema = { + name: 'my-app-e2e', + project: 'my-app', + directory: 'directory', + }; + const options = normalizeOptions(appTree, schema); + expect(options).toEqual({ + project: 'my-app', + appClassName: 'MyApp', + appFileName: 'my-app', + projectRoot: 'apps/directory/my-app-e2e', + name: 'my-app-e2e', + directory: 'directory', + projectName: 'directory-my-app-e2e', + }); + }); +}); diff --git a/packages/detox/src/generators/application/lib/normalize-options.ts b/packages/detox/src/generators/application/lib/normalize-options.ts new file mode 100644 index 0000000..37cde3d --- /dev/null +++ b/packages/detox/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,50 @@ +import { + getWorkspaceLayout, + joinPathFragments, + names, + Tree, +} from '@nrwl/devkit'; +import { Schema } from '../schema'; + +export interface NormalizedSchema extends Schema { + appFileName: string; // the file name of app to be tested + appClassName: string; // the class name of app to be tested + projectName: string; // the name of e2e project + projectRoot: string; // the root path of e2e project +} + +/** + * if options.name = 'my-app-e2e' with no options.directory + * projectName = 'my-app', projectRoot = 'apps/my-app' + * if options.name = 'my-app' with options.directory = 'my-dir' + * projectName = 'my-dir-my-app', projectRoot = 'apps/my-dir/my-apps' + */ +export function normalizeOptions( + host: Tree, + options: Schema +): NormalizedSchema { + const { appsDir } = getWorkspaceLayout(host); + const fileName = names(options.name).fileName; + const directoryFileName = options.directory + ? names(options.directory).fileName + : ''; + const projectName = directoryFileName + ? `${directoryFileName.replace(new RegExp('/', 'g'), '-')}-${fileName}` + : fileName; + const projectRoot = directoryFileName + ? joinPathFragments(appsDir, directoryFileName, fileName) + : joinPathFragments(appsDir, fileName); + + const { fileName: appFileName, className: appClassName } = names( + options.project + ); + + return { + ...options, + appFileName, + appClassName, + name: fileName, + projectName, + projectRoot, + }; +} diff --git a/packages/detox/src/generators/application/schema.d.ts b/packages/detox/src/generators/application/schema.d.ts new file mode 100644 index 0000000..c8f9019 --- /dev/null +++ b/packages/detox/src/generators/application/schema.d.ts @@ -0,0 +1,11 @@ +import { Linter } from '@nrwl/linter'; + +export interface Schema { + project: string; + name: string; + directory?: string; + linter?: Linter; + js?: boolean; + skipFormat?: boolean; + setParserOptionsProject?: boolean; +} diff --git a/packages/detox/src/generators/application/schema.json b/packages/detox/src/generators/application/schema.json new file mode 100644 index 0000000..2fb2512 --- /dev/null +++ b/packages/detox/src/generators/application/schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/schema", + "title": "Create Detox Configuration for the workspace", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the frontend project to test.", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What is the name of the frontend project to test?" + }, + "name": { + "type": "string", + "description": "Name of the E2E Project", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the e2e project?" + }, + "directory": { + "type": "string", + "description": "A directory where the project is placed" + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "tslint", "none"], + "default": "eslint" + }, + "js": { + "description": "Generate JavaScript files rather than TypeScript files", + "type": "boolean", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false + } + }, + "required": ["name", "project"] +} diff --git a/packages/detox/src/generators/init/init.spec.ts b/packages/detox/src/generators/init/init.spec.ts new file mode 100644 index 0000000..ec9d65d --- /dev/null +++ b/packages/detox/src/generators/init/init.spec.ts @@ -0,0 +1,19 @@ +import { Tree, readJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { detoxInitGenerator } from './init'; + +describe('init', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should add detox dependencies', async () => { + await detoxInitGenerator(tree, {}); + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.devDependencies['@nrwl/detox']).toBeDefined(); + expect(packageJson.devDependencies['@types/detox']).toBeDefined(); + expect(packageJson.devDependencies['detox']).toBeDefined(); + }); +}); diff --git a/packages/detox/src/generators/init/init.ts b/packages/detox/src/generators/init/init.ts new file mode 100644 index 0000000..a92f7be --- /dev/null +++ b/packages/detox/src/generators/init/init.ts @@ -0,0 +1,47 @@ +import { + addDependenciesToPackageJson, + convertNxGenerator, + formatFiles, + removeDependenciesFromPackageJson, + Tree, +} from '@nrwl/devkit'; +import { jestVersion } from '@nrwl/jest/src/utils/versions'; +import { Schema } from './schema'; +import { + detoxVersion, + nxVersion, + testingLibraryJestDom, + typesDetoxVersion, +} from '../../utils/versions'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; + +export async function detoxInitGenerator(host: Tree, schema: Schema) { + const tasks = [moveDependency(host), updateDependencies(host)]; + + if (!schema.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial(...tasks); +} + +export function updateDependencies(host: Tree) { + return addDependenciesToPackageJson( + host, + {}, + { + '@nrwl/detox': nxVersion, + detox: detoxVersion, + '@types/detox': typesDetoxVersion, + '@testing-library/jest-dom': testingLibraryJestDom, + 'jest-circus': jestVersion, + } + ); +} + +function moveDependency(host: Tree) { + return removeDependenciesFromPackageJson(host, ['@nrwl/detox'], []); +} + +export default detoxInitGenerator; +export const detoxInitSchematic = convertNxGenerator(detoxInitGenerator); diff --git a/packages/detox/src/generators/init/schema.d.ts b/packages/detox/src/generators/init/schema.d.ts new file mode 100644 index 0000000..e5fe924 --- /dev/null +++ b/packages/detox/src/generators/init/schema.d.ts @@ -0,0 +1,3 @@ +export interface Schema { + skipFormat?: boolean; +} diff --git a/packages/detox/src/generators/init/schema.json b/packages/detox/src/generators/init/schema.json new file mode 100644 index 0000000..fb4b7ce --- /dev/null +++ b/packages/detox/src/generators/init/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/schema", + "title": "Add Detox Schematics", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + } + }, + "required": [] +} diff --git a/packages/detox/src/utils/versions.ts b/packages/detox/src/utils/versions.ts new file mode 100644 index 0000000..d293973 --- /dev/null +++ b/packages/detox/src/utils/versions.ts @@ -0,0 +1,5 @@ +export const nxVersion = '12.3.6'; + +export const detoxVersion = '18.18.0'; +export const typesDetoxVersion = '17.14.0'; +export const testingLibraryJestDom = '5.14.1'; diff --git a/packages/detox/tsconfig.json b/packages/detox/tsconfig.json new file mode 100644 index 0000000..62ebbd9 --- /dev/null +++ b/packages/detox/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/detox/tsconfig.lib.json b/packages/detox/tsconfig.lib.json new file mode 100644 index 0000000..037d796 --- /dev/null +++ b/packages/detox/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/detox/tsconfig.spec.json b/packages/detox/tsconfig.spec.json new file mode 100644 index 0000000..559410b --- /dev/null +++ b/packages/detox/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 40c30d9..dba9adf 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -24,6 +24,13 @@ - [Run on devices](#run-on-devices) - [Release build](#release-build) - [Test/lint the app](#testlint-the-app) + - [E2e test the app](#e2e-test-the-app) + - [Setup](#setup) + - [Install applesimutils (Mac only)](#install-applesimutils-mac-only) + - [Install Jest Globally](#install-jest-globally) + - [Commands](#commands) + - [Manually Add E2E Folder](#manually-add-e2e-folder) + - [Change Testing Simulator/Emulator](#change-testing-simulatoremulator) - [Using components from React library](#using-components-from-react-library) - [CLI Commands and Options](#cli-commands-and-options) - [`start`](#start) @@ -36,7 +43,10 @@ - [`--port [number]`](#--port-number-2) - [`--sync`](#--sync-1) - [`sync-deps`](#sync-deps) + - [`--include [string]`](#--include-string) - [Learn more](#learn-more) +- [Contributing](#contributing) +- [Debugging](#debugging) @@ -111,6 +121,59 @@ npx nx test npx nx lint ``` +### E2e test the app + +#### Setup + +##### Install applesimutils (Mac only) + +[applesimutils](https://github.com/wix/AppleSimulatorUtils) is a collection of utils for Apple simulators. + +```sh +brew tap wix/brew +brew install applesimutils +``` + +##### Install Jest Globally + +```sh +npm install -g jest +``` + +#### Commands + +A built app must exist before run test commands. + +- `nx build-ios `: build the iOS app (Mac only) +- `nx test-ios `: run e2e tests on the built iOS app (Mac only) +- `nx build-ios --prod` and `nx test-ios --prod`: build and run release version of iOS app. Note: you might need open the xcode project under iOS and choose a team under "Sign & Capabilities". +- `nx build-android `: build the android app +- `nx test-android `: run e2e tests on the built android app +- `nx build-android --prod` and `nx test-android --prod`: build and run release version of android app. + +#### Manually Add E2E Folder + +A `` folder is automatically generate when you create a react native app. However, if you want to add e2e folder manually, you need to: + +- Install @nrwl/detox + + ```sh + # Using npm + npm install --save-dev @nrwl/detox + + # Using yarn + yarn add -D @nrwl/detox + ``` + +- Run `nx generate @nrwl/detox:app ` +- Follow instructions https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md to manully change android files. + +#### Change Testing Simulator/Emulator + +For iOS, in terminal, run `xcrun simctl list` to view a list of simulators on your Mac. To open your active simulator, `run open -a simulator`. In `/.detoxrc.json`, you could change the simulator under `devices.simulator.device`. + +For Android: in terminal, run `emulator -list-avds` to view a list of emulators installed. To open your emulator, run `emulator -avd `. In `/.detoxrc.json`, you could change the simulator under `devices.emulator.device`. + ## Using components from React library You can use a component from React library generated using Nx package for React. Once you run: @@ -194,3 +257,8 @@ To publish packages to a local registry, do the following: - Run `yarn release 999.9.9 latest --local` in Terminal 3 - Run `cd /tmp` in Terminal 3 - Run `npx create-nx-workspace` in Terminal 3 + +## Debugging + +- If you got a pod install error like "None of your spec sources contain a spec satisfying the dependency", go to ios folder and run `pod install --repo-update` in your terminal. +- If you got an error "error: Signing for "App" requires a development team. Select a development team in the Signing & Capabilities editor." when build for iOS, you need to open the xcode project under iOS and choose a team under "Sign & Capabilities". diff --git a/packages/react-native/jest.config.js b/packages/react-native/jest.config.js index e13f298..32aef31 100644 --- a/packages/react-native/jest.config.js +++ b/packages/react-native/jest.config.js @@ -8,4 +8,5 @@ module.exports = { 'ts-jest': { tsconfig: '/tsconfig.spec.json' }, }, displayName: 'react-native', + testEnvironment: 'node', }; diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 786a23c..34c7479 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -25,6 +25,7 @@ "dependencies": { "@angular-devkit/architect": "^0.1200.0", "@angular-devkit/schematics": "^12.0.0", + "@nrwl/detox": "*", "@nrwl/jest": "12.4.0", "@nrwl/linter": "12.4.0", "@nrwl/react": "12.4.0", diff --git a/packages/react-native/src/executors/run-android/schema.d.ts b/packages/react-native/src/executors/run-android/schema.d.ts index 6b1ff17..a12ec80 100644 --- a/packages/react-native/src/executors/run-android/schema.d.ts +++ b/packages/react-native/src/executors/run-android/schema.d.ts @@ -1,3 +1,4 @@ +// part of options from https://github.com/react-native-community/cli/blob/master/packages/platform-android/src/commands/runAndroid/index.ts#L314 export interface ReactNativeRunAndroidOptions { variant: string; appId: string; diff --git a/packages/react-native/src/executors/run-ios/schema.d.ts b/packages/react-native/src/executors/run-ios/schema.d.ts index c0b497b..61aff0f 100644 --- a/packages/react-native/src/executors/run-ios/schema.d.ts +++ b/packages/react-native/src/executors/run-ios/schema.d.ts @@ -1,3 +1,4 @@ +// part of options form https://github.com/react-native-community/cli/blob/master/packages/platform-ios/src/commands/runIOS/index.ts#L541 export interface ReactNativeRunIosOptions { xcodeConfiguration: string; port: number; diff --git a/packages/react-native/src/generators/application/application.spec.ts b/packages/react-native/src/generators/application/application.spec.ts index 2b50aef..90a192a 100644 --- a/packages/react-native/src/generators/application/application.spec.ts +++ b/packages/react-native/src/generators/application/application.spec.ts @@ -21,6 +21,7 @@ describe('app', () => { name: 'myApp', displayName: 'myApp', linter: Linter.EsLint, + e2eTestRunner: 'none', }); const workspaceJson = readWorkspaceConfiguration(appTree); const projects = getProjects(appTree); @@ -35,6 +36,7 @@ describe('app', () => { displayName: 'myApp', tags: 'one,two', linter: Linter.EsLint, + e2eTestRunner: 'none', }); const nxJson = readJson(appTree, '/nx.json'); @@ -53,6 +55,7 @@ describe('app', () => { name: 'myApp', displayName: 'myApp', linter: Linter.EsLint, + e2eTestRunner: 'none', }); expect(appTree.exists('apps/my-app/src/app/App.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/main.tsx')).toBeTruthy(); diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index dc44db3..0e05027 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -17,6 +17,7 @@ import initGenerator from '../init/init'; import { join } from 'path'; import { addProject } from './lib/add-project'; import { createApplicationFiles } from './lib/create-application-files'; +import { addDetox } from './lib/add-detox'; export async function reactNativeApplicationGenerator( host: Tree, @@ -42,6 +43,7 @@ export async function reactNativeApplicationGenerator( options.projectName, options.appProjectRoot ); + const detoxTask = await addDetox(host, options); const symlinkTask = runSymlink(options.appProjectRoot); const podInstallTask = runPodInstall(options.iosProjectRoot); const chmodTaskGradlew = runChmod( @@ -61,6 +63,7 @@ export async function reactNativeApplicationGenerator( initTask, lintTask, jestTask, + detoxTask, symlinkTask, podInstallTask, chmodTaskGradlew, diff --git a/packages/react-native/src/generators/application/files/app/android/app/build.gradle.template b/packages/react-native/src/generators/application/files/app/android/app/build.gradle.template index d9fc871..2c8ecdf 100644 --- a/packages/react-native/src/generators/application/files/app/android/app/build.gradle.template +++ b/packages/react-native/src/generators/application/files/app/android/app/build.gradle.template @@ -130,6 +130,10 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" + <% if (e2eTestRunner === 'detox') { %> + testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + <% } %> } splits { abi { @@ -216,6 +220,10 @@ dependencies { } else { implementation jscFlavor } + + <% if (e2eTestRunner === 'detox') { %> + androidTestImplementation('com.wix:detox:+') + <% } %> } // Run this once to be able to run the application with BUCK diff --git a/packages/react-native/src/generators/application/files/app/android/app/src/androidTest/java/com/__lowerCaseName__/DetoxTest.java.template b/packages/react-native/src/generators/application/files/app/android/app/src/androidTest/java/com/__lowerCaseName__/DetoxTest.java.template new file mode 100644 index 0000000..b681e32 --- /dev/null +++ b/packages/react-native/src/generators/application/files/app/android/app/src/androidTest/java/com/__lowerCaseName__/DetoxTest.java.template @@ -0,0 +1,36 @@ +// Replace "com.example" here and below with your app's package name from the top of MainActivity.java +package com.<%= lowerCaseName %>; + +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + // Replace 'MainActivity' with the value of android:name entry in + // in AndroidManifest.xml + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + // This is optional - in case you've decided to integrate TestButler + // See https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md#8-test-butler-support-optional + // TestButlerProbe.assertReadyIfInstalled(); + + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = (com.<%= lowerCaseName %>.BuildConfig.DEBUG ? 180 : 60); + + Detox.runTests(mActivityRule, detoxConfig); + } +} \ No newline at end of file diff --git a/packages/react-native/src/generators/application/files/app/android/app/src/main/AndroidManifest.xml.template b/packages/react-native/src/generators/application/files/app/android/app/src/main/AndroidManifest.xml.template index 9644873..6d66088 100644 --- a/packages/react-native/src/generators/application/files/app/android/app/src/main/AndroidManifest.xml.template +++ b/packages/react-native/src/generators/application/files/app/android/app/src/main/AndroidManifest.xml.template @@ -28,6 +28,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network_security_config"> > + + + 10.0.2.2 + localhost + + \ No newline at end of file diff --git a/packages/react-native/src/generators/application/files/app/android/build.gradle b/packages/react-native/src/generators/application/files/app/android/build.gradle.template similarity index 80% rename from packages/react-native/src/generators/application/files/app/android/build.gradle rename to packages/react-native/src/generators/application/files/app/android/build.gradle.template index e64d31e..4264860 100644 --- a/packages/react-native/src/generators/application/files/app/android/build.gradle +++ b/packages/react-native/src/generators/application/files/app/android/build.gradle.template @@ -33,6 +33,14 @@ allprojects { } google() - maven { url 'https://www.jitpack.io' } + + + <% if (e2eTestRunner === 'detox') { %> + maven { + // All of the Detox artifacts are provided via the npm module + url("$rootDir/../../../node_modules/detox/Detox-android") + } + <% } %> + } } diff --git a/packages/react-native/src/generators/application/lib/add-detox.ts b/packages/react-native/src/generators/application/lib/add-detox.ts new file mode 100644 index 0000000..811c56a --- /dev/null +++ b/packages/react-native/src/generators/application/lib/add-detox.ts @@ -0,0 +1,18 @@ +import { detoxApplicationGenerator } from '@nrwl/detox'; +import { Tree } from '@nrwl/devkit'; +import { NormalizedSchema } from './normalize-options'; +import { Linter } from '@nrwl/linter'; + +export async function addDetox(host: Tree, options: NormalizedSchema) { + if (options?.e2eTestRunner !== 'detox') { + return () => {}; + } + + return detoxApplicationGenerator(host, { + ...options, + linter: Linter.EsLint, + name: `${options.name}-e2e`, + directory: options.directory, + project: options.name, + }); +} diff --git a/packages/react-native/src/generators/application/lib/create-application-files.ts b/packages/react-native/src/generators/application/lib/create-application-files.ts index 03dd691..22a1fe0 100644 --- a/packages/react-native/src/generators/application/lib/create-application-files.ts +++ b/packages/react-native/src/generators/application/lib/create-application-files.ts @@ -1,4 +1,4 @@ -import { generateFiles, offsetFromRoot, Tree } from '@nrwl/devkit'; +import { generateFiles, offsetFromRoot, toJS, Tree } from '@nrwl/devkit'; import { join } from 'path'; import { NormalizedSchema } from './normalize-options'; @@ -10,4 +10,15 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { if (options.unitTestRunner === 'none') { host.delete(join(options.appProjectRoot, `/src/app/App.spec.tsx`)); } + if (options.e2eTestRunner === 'none') { + host.delete( + join( + options.androidProjectRoot, + `/app/src/androidTest/java/com/${options.lowerCaseName}/DetoxTest.java` + ) + ); + } + if (options.js) { + toJS(host); + } } diff --git a/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts b/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts index 2e876b2..cd39913 100644 --- a/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts +++ b/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts @@ -15,6 +15,7 @@ describe('Normalize Options', () => { const schema: Schema = { name: 'my-app', linter: Linter.EsLint, + e2eTestRunner: 'none', }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -29,12 +30,14 @@ describe('Normalize Options', () => { projectName: 'my-app', linter: Linter.EsLint, entryFile: '/virtual/apps/my-app/src/main.tsx', + e2eTestRunner: 'none', }); }); it('should normalize options with name in camel case', () => { const schema: Schema = { name: 'myApp', + e2eTestRunner: 'none', }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -48,6 +51,7 @@ describe('Normalize Options', () => { parsedTags: [], projectName: 'my-app', entryFile: '/virtual/apps/my-app/src/main.tsx', + e2eTestRunner: 'none', }); }); @@ -55,6 +59,7 @@ describe('Normalize Options', () => { const schema: Schema = { name: 'my-app', directory: 'directory', + e2eTestRunner: 'none', }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -69,6 +74,7 @@ describe('Normalize Options', () => { parsedTags: [], projectName: 'directory-my-app', entryFile: '/virtual/apps/directory/my-app/src/main.tsx', + e2eTestRunner: 'none', }); }); @@ -76,6 +82,7 @@ describe('Normalize Options', () => { const schema: Schema = { name: 'my-app', displayName: 'My App', + e2eTestRunner: 'none', }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -89,6 +96,7 @@ describe('Normalize Options', () => { parsedTags: [], projectName: 'my-app', entryFile: '/virtual/apps/my-app/src/main.tsx', + e2eTestRunner: 'none', }); }); }); diff --git a/packages/react-native/src/generators/application/schema.d.ts b/packages/react-native/src/generators/application/schema.d.ts index bfc5cdf..f46a4a3 100644 --- a/packages/react-native/src/generators/application/schema.d.ts +++ b/packages/react-native/src/generators/application/schema.d.ts @@ -13,4 +13,5 @@ export interface Schema { js?: boolean; linter?: Linter; setParserOptionsProject?: boolean; + e2eTestRunner: 'detox' | 'none'; } diff --git a/packages/react-native/src/generators/application/schema.json b/packages/react-native/src/generators/application/schema.json index 17c16bd..08aa7b5 100644 --- a/packages/react-native/src/generators/application/schema.json +++ b/packages/react-native/src/generators/application/schema.json @@ -62,6 +62,12 @@ "type": "boolean", "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", "default": false + }, + "e2eTestRunner": { + "description": "Adds the specified e2e test runner", + "type": "string", + "enum": ["detox", "none"], + "default": "detox" } }, "required": [] diff --git a/packages/react-native/src/generators/init/init.spec.ts b/packages/react-native/src/generators/init/init.spec.ts index dcb6d13..786aa7b 100644 --- a/packages/react-native/src/generators/init/init.spec.ts +++ b/packages/react-native/src/generators/init/init.spec.ts @@ -10,8 +10,8 @@ describe('init', () => { tree.write('.gitignore', ''); }); - it('should add react dependencies', async () => { - await reactNativeInitGenerator(tree, {}); + it('should add react native dependencies', async () => { + await reactNativeInitGenerator(tree, { e2eTestRunner: 'none' }); const packageJson = readJson(tree, 'package.json'); expect(packageJson.dependencies['react']).toBeDefined(); expect(packageJson.dependencies['react-native']).toBeDefined(); @@ -26,7 +26,7 @@ describe('init', () => { /node_modules ` ); - await reactNativeInitGenerator(tree, {}); + await reactNativeInitGenerator(tree, { e2eTestRunner: 'none' }); const content = tree.read('/.gitignore').toString(); @@ -36,7 +36,7 @@ describe('init', () => { describe('defaultCollection', () => { it('should be set if none was set before', async () => { - await reactNativeInitGenerator(tree, {}); + await reactNativeInitGenerator(tree, { e2eTestRunner: 'none' }); const workspaceJson = readJson(tree, 'workspace.json'); expect(workspaceJson.cli.defaultCollection).toEqual('@nrwl/react-native'); }); @@ -51,7 +51,7 @@ describe('init', () => { return json; }); - await reactNativeInitGenerator(tree, {}); + await reactNativeInitGenerator(tree, { e2eTestRunner: 'none' }); const workspaceJson = readJson(tree, 'workspace.json'); expect(workspaceJson.cli.defaultCollection).toEqual('@nrwl/react'); }); diff --git a/packages/react-native/src/generators/init/init.ts b/packages/react-native/src/generators/init/init.ts index d1c7e64..7861fa6 100644 --- a/packages/react-native/src/generators/init/init.ts +++ b/packages/react-native/src/generators/init/init.ts @@ -27,6 +27,7 @@ import { import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { addGitIgnoreEntry } from './lib/add-git-ignore-entry'; import { jestInitGenerator } from '@nrwl/jest'; +import { detoxInitGenerator } from '@nrwl/detox'; export async function reactNativeInitGenerator(host: Tree, schema: Schema) { setDefaultCollection(host, '@nrwl/react-native'); @@ -39,6 +40,11 @@ export async function reactNativeInitGenerator(host: Tree, schema: Schema) { tasks.push(jestTask); } + if (!schema.e2eTestRunner || schema.e2eTestRunner === 'detox') { + const detoxTask = await detoxInitGenerator(host, {}); + tasks.push(detoxTask); + } + if (!schema.skipFormat) { await formatFiles(host); } @@ -54,7 +60,6 @@ export function updateDependencies(host: Tree) { 'react-native': reactNativeVersion, }, { - '@nrwl/jest': nxVersion, '@nrwl/linter': nxVersion, '@types/react': typesReactVersion, '@types/react-native': typesReactNativeVersion, diff --git a/packages/react-native/src/generators/init/schema.d.ts b/packages/react-native/src/generators/init/schema.d.ts index dde6ab2..4d152dc 100644 --- a/packages/react-native/src/generators/init/schema.d.ts +++ b/packages/react-native/src/generators/init/schema.d.ts @@ -1,4 +1,5 @@ export interface Schema { unitTestRunner?: 'jest' | 'none'; skipFormat?: boolean; + e2eTestRunner: 'detox' | 'none'; } diff --git a/packages/react-native/src/generators/init/schema.json b/packages/react-native/src/generators/init/schema.json index 5765fa7..eeacf53 100644 --- a/packages/react-native/src/generators/init/schema.json +++ b/packages/react-native/src/generators/init/schema.json @@ -13,6 +13,12 @@ "description": "Skip formatting files", "type": "boolean", "default": false + }, + "e2eTestRunner": { + "description": "Adds the specified e2e test runner", + "type": "string", + "enum": ["detox", "none"], + "default": "detox" } }, "required": [] diff --git a/packages/react-native/src/generators/library/library.spec.ts b/packages/react-native/src/generators/library/library.spec.ts index 949d5f6..252ff72 100644 --- a/packages/react-native/src/generators/library/library.spec.ts +++ b/packages/react-native/src/generators/library/library.spec.ts @@ -3,7 +3,6 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import libraryGenerator from './library'; import { Linter } from '@nrwl/linter'; import { Schema } from './schema'; -import applicationGenerator from '../application/application'; describe('lib', () => { let appTree: Tree; diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index ef5036e..c92ffc4 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -37,6 +37,7 @@ export async function reactNativeLibraryGenerator( const initTask = await init(host, { ...options, skipFormat: true, + e2eTestRunner: 'none', }); const lintTask = await addLinting( diff --git a/packages/react-native/src/utils/pod-install-task.ts b/packages/react-native/src/utils/pod-install-task.ts index d01d482..291b443 100644 --- a/packages/react-native/src/utils/pod-install-task.ts +++ b/packages/react-native/src/utils/pod-install-task.ts @@ -1,5 +1,5 @@ import { spawn } from 'child_process'; -import * as os from 'os'; +import { platform } from 'os'; import * as chalk from 'chalk'; import { GeneratorCallback, logger } from '@nrwl/devkit'; @@ -21,7 +21,7 @@ ${chalk.bold('sudo xcode-select --switch /Applications/Xcode.app')} */ export function runPodInstall(cwd: string): GeneratorCallback { return () => { - if (os.platform() !== 'darwin') { + if (platform() !== 'darwin') { logger.info('Skipping `pod install` on non-darwin platform'); return; } diff --git a/packages/react-native/src/utils/testing-generators.ts b/packages/react-native/src/utils/testing-generators.ts index 4aa649f..89fe1f6 100644 --- a/packages/react-native/src/utils/testing-generators.ts +++ b/packages/react-native/src/utils/testing-generators.ts @@ -2,17 +2,18 @@ import { addProjectConfiguration, names, Tree } from '@nrwl/devkit'; import applicationGenerator from '../generators/application/application'; import { Linter } from '@nrwl/linter'; -export async function createApp(tree: Tree, appName: string): Promise { +export async function createApp(tree: Tree, appName: string): Promise { await applicationGenerator(tree, { linter: Linter.EsLint, skipFormat: true, style: 'css', unitTestRunner: 'none', name: appName, + e2eTestRunner: 'none', }); } -export async function createLib(tree: Tree, libName: string): Promise { +export async function createLib(tree: Tree, libName: string): Promise { const { fileName } = names(libName); tree.write(`/libs/${fileName}/src/index.ts`, `import React from 'react';\n`); diff --git a/packages/react-native/tsconfig.lib.json b/packages/react-native/tsconfig.lib.json index 5ec6ffd..037d796 100644 --- a/packages/react-native/tsconfig.lib.json +++ b/packages/react-native/tsconfig.lib.json @@ -4,8 +4,7 @@ "module": "commonjs", "outDir": "../../dist/out-tsc", "declaration": true, - "types": ["node"], - "rootDir": "." + "types": ["node"] }, "exclude": ["**/*.spec.ts"], "include": ["**/*.ts"] diff --git a/tsconfig.base.json b/tsconfig.base.json index 2764cf4..c90a7b4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -17,7 +17,8 @@ "baseUrl": ".", "rootDir": ".", "paths": { - "nrwl/react-native": ["packages/react-native/src/index.ts"] + "@nrwl/react-native": ["packages/react-native"], + "@nrwl/detox": ["packages/detox"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 3d2f491..f26dcd0 100644 --- a/workspace.json +++ b/workspace.json @@ -1,6 +1,89 @@ { "version": 1, "projects": { + "detox": { + "projectType": "library", + "root": "packages/detox", + "sourceRoot": "packages/detox/src", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "packages/detox/**/*.ts", + "packages/detox/**/*.spec.ts", + "packages/detox/**/*.spec.tsx", + "packages/detox/**/*.spec.js", + "packages/detox/**/*.spec.jsx", + "packages/detox/**/*.d.ts" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "options": { + "jestConfig": "packages/detox/jest.config.js", + "passWithNoTests": true + }, + "outputs": ["coverage/packages/detox"] + }, + "build": { + "builder": "@nrwl/node:package", + "options": { + "outputPath": "dist/packages/detox", + "tsConfig": "packages/detox/tsconfig.lib.json", + "packageJson": "packages/detox/package.json", + "main": "packages/detox/index.ts", + "assets": [ + "packages/detox/*.md", + { + "input": "packages/detox", + "glob": "**/files/**", + "output": "/" + }, + { + "input": "packages/detox", + "glob": "**/files/**/.babelrc.template", + "output": "/" + }, + { + "input": "packages/detox", + "glob": "**/files/**/.detoxrc.json.template", + "output": "/" + }, + { + "input": "./packages/detox", + "glob": "**/*.json", + "ignore": ["**/tsconfig*.json"], + "output": "/" + } + ] + }, + "outputs": ["{options.outputPath}"] + }, + "publish": { + "builder": "@nrwl/workspace:run-commands", + "options": { + "command": "node tools/scripts/publish.js detox" + } + } + } + }, + "detox-e2e": { + "projectType": "application", + "root": "e2e/detox-e2e", + "sourceRoot": "e2e/detox-e2e", + "architect": { + "e2e": { + "executor": "@nrwl/jest:jest", + "options": { + "passWithNoTests": true, + "runInBand": true, + "jestConfig": "e2e/detox-e2e/jest.config.js" + } + } + } + }, "react-native": { "root": "packages/react-native", "sourceRoot": "packages/react-native/src", @@ -8,14 +91,16 @@ "schematics": {}, "architect": { "lint": { - "builder": "@nrwl/linter:lint", + "builder": "@nrwl/linter:eslint", "options": { - "linter": "eslint", - "tsConfig": [ - "packages/react-native/tsconfig.lib.json", - "packages/react-native/tsconfig.spec.json" - ], - "exclude": ["**/node_modules/**", "!packages/react-native/**/*"] + "lintFilePatterns": [ + "packages/react-native/**/*.ts", + "packages/react-native/**/*.spec.ts", + "packages/react-native/**/*.spec.tsx", + "packages/react-native/**/*.spec.js", + "packages/react-native/**/*.spec.jsx", + "packages/react-native/**/*.d.ts" + ] } }, "test": { @@ -57,18 +142,9 @@ }, { "input": "./packages/react-native", - "glob": "collection.json", - "output": "." - }, - { - "input": "./packages/react-native", - "glob": "builders.json", - "output": "." - }, - { - "input": "./packages/react-native", - "glob": "migrations.json", - "output": "." + "glob": "**/*.json", + "ignore": ["**/tsconfig*.json"], + "output": "/" } ] }, @@ -85,14 +161,13 @@ "react-native-e2e": { "projectType": "application", "root": "e2e/react-native-e2e", - "sourceRoot": "e2e/react-native-e2e/src", + "sourceRoot": "e2e/react-native-e2e", "architect": { "e2e": { - "builder": "@nrwl/nx-plugin:e2e", + "executor": "@nrwl/jest:jest", "options": { - "target": "react-native:build", - "npmPackageName": "@nrwl/react-native", - "pluginOutputPath": "dist/packages/react-native", + "passWithNoTests": true, + "runInBand": true, "jestConfig": "e2e/react-native-e2e/jest.config.js" } }