diff --git a/docs/generated/packages/detox.json b/docs/generated/packages/detox.json index 888a5c0196..803a4c9a70 100644 --- a/docs/generated/packages/detox.json +++ b/docs/generated/packages/detox.json @@ -59,6 +59,12 @@ "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What name would you like to use for the E2E project?" }, + "framework": { + "type": "string", + "description": "App framework to test", + "enum": ["react-native", "expo"], + "x-prompt": "What app framework should detox test?" + }, "directory": { "type": "string", "description": "A directory where the project is placed." @@ -85,7 +91,7 @@ "default": false } }, - "required": ["name", "project"], + "required": ["name", "project", "framework"], "presets": [] }, "aliases": ["app"], diff --git a/packages/detox/src/executors/build/build.impl.ts b/packages/detox/src/executors/build/build.impl.ts index 3c4f822ec1..b3c4e985f3 100644 --- a/packages/detox/src/executors/build/build.impl.ts +++ b/packages/detox/src/executors/build/build.impl.ts @@ -37,7 +37,7 @@ export function runCliBuild( join(workspaceRoot, './node_modules/detox/local-cli/cli.js'), ['build', ...createDetoxBuildOptions(options)], { - cwd: projectRoot, + cwd: join(workspaceRoot, projectRoot), } ); diff --git a/packages/detox/src/executors/test/test.impl.ts b/packages/detox/src/executors/test/test.impl.ts index 76b16a56db..cfb404709e 100644 --- a/packages/detox/src/executors/test/test.impl.ts +++ b/packages/detox/src/executors/test/test.impl.ts @@ -58,7 +58,7 @@ function runCliTest( join(workspaceRoot, './node_modules/detox/local-cli/cli.js'), ['test', ...createDetoxTestOptions(options)], { - cwd: projectRoot, + cwd: join(workspaceRoot, projectRoot), } ); diff --git a/packages/detox/src/generators/application/application.spec.ts b/packages/detox/src/generators/application/application.spec.ts index f8b07b9525..96ba8615b7 100644 --- a/packages/detox/src/generators/application/application.spec.ts +++ b/packages/detox/src/generators/application/application.spec.ts @@ -27,6 +27,7 @@ describe('detox application generator', () => { name: 'my-app-e2e', project: 'my-app', linter: Linter.None, + framework: 'react-native', }); }); @@ -60,6 +61,7 @@ describe('detox application generator', () => { directory: 'my-dir', project: 'my-dir-my-app', linter: Linter.None, + framework: 'react-native', }); }); @@ -94,6 +96,7 @@ describe('detox application generator', () => { name: 'my-dir/my-app-e2e', project: 'my-dir-my-app', linter: Linter.None, + framework: 'react-native', }); }); @@ -128,6 +131,7 @@ describe('detox application generator', () => { name: 'my-app-e2e', project: 'my-app', linter: Linter.None, + framework: 'react-native', }); const tsConfig = readJson(tree, 'apps/my-app-e2e/tsconfig.json'); @@ -141,6 +145,7 @@ describe('detox application generator', () => { name: 'my-app-e2e', project: 'my-app', linter: Linter.None, + framework: 'react-native', }); const tsConfig = readJson(tree, 'apps/my-app-e2e/tsconfig.json'); diff --git a/packages/detox/src/generators/application/files/app/.detoxrc.json.template b/packages/detox/src/generators/application/files/app/.detoxrc.json.template index ccd9b8451b..2a6204fd7d 100644 --- a/packages/detox/src/generators/application/files/app/.detoxrc.json.template +++ b/packages/detox/src/generators/application/files/app/.detoxrc.json.template @@ -12,6 +12,18 @@ "build": "cd ../<%= appFileName %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 13' -derivedDataPath ./build -quiet", "binaryPath": "../<%= appFileName %>/ios/build/Build/Products/Release-iphonesimulator/<%= appClassName %>.app" }, +<% if (framework === 'expo') { %> + "ios.eas": { + "type": "ios.app", + "build": "<%= exec %> nx run <%= appFileName %>:download --platform ios --distribution simulator --output=<%= projectDirectory %>/<%= appFileName %>/dist/", + "binaryPath": "../<%= appFileName %>/dist/<%= appDisplayName %>.app" + }, + "ios.local": { + "type": "ios.app", + "build": "<%= exec %> nx run <%= appFileName %>:build --platform ios --profile preview --wait --local --no-interactive --output=<%= projectDirectory %>/<%= appFileName %>/dist/", + "binaryPath": "../<%= appFileName %>/dist/<%= appDisplayName %>.app" + }, +<% } %> "android.debug": { "type": "android.apk", "build": "cd ../<%= appFileName %>/android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug", @@ -21,7 +33,19 @@ "type": "android.apk", "build": "cd ../<%= appFileName %>/android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release", "binaryPath": "../<%= appFileName %>/android/app/build/outputs/apk/release/app-release.apk" - } + }, +<% if (framework === 'expo') { %> + "android.eas": { + "type": "ios.app", + "build": "<%= exec %> nx run <%= appFileName %>:download --platform android --output=<%= projectDirectory %>/<%= appFileName %>/dist/", + "binaryPath": "../<%= appFileName %>/dist/<%= appDisplayName %>.apk" + }, + "android.local": { + "type": "ios.app", + "build": "<%= exec %> nx run <%= appFileName %>:build --platform android --profile preview --wait --local --no-interactive --output=<%= projectDirectory %>/<%= appFileName %>/dist/", + "binaryPath": "../<%= appFileName %>/dist/<%= appDisplayName %>.apk" + }, +<% } %> }, "devices": { "simulator": { @@ -46,6 +70,16 @@ "device": "simulator", "app": "ios.debug" }, +<% if (framework === 'expo') { %> + "ios.sim.eas": { + "device": "simulator", + "app": "ios.eas" + }, + "ios.sim.local": { + "device": "simulator", + "app": "ios.local" + }, +<% } %> "android.emu.release": { "device": "emulator", "app": "android.release" @@ -53,6 +87,16 @@ "android.emu.debug": { "device": "emulator", "app": "android.debug" - } + }, +<% if (framework === 'expo') { %> + "android.sim.eas": { + "device": "simulator", + "app": "android.eas" + }, + "android.sim.local": { + "device": "simulator", + "app": "android.local" + }, +<% } %> } } diff --git a/packages/detox/src/generators/application/lib/add-linting.spec.ts b/packages/detox/src/generators/application/lib/add-linting.spec.ts index dede28035e..c719c8348a 100644 --- a/packages/detox/src/generators/application/lib/add-linting.spec.ts +++ b/packages/detox/src/generators/application/lib/add-linting.spec.ts @@ -12,11 +12,14 @@ describe('Add Linting', () => { addProject(tree, { name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.EsLint, + framework: 'react-native', }); }); @@ -24,11 +27,14 @@ describe('Add Linting', () => { addLinting(tree, { name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.EsLint, + framework: 'react-native', }); const project = readProjectConfiguration(tree, 'my-app-e2e'); @@ -40,11 +46,14 @@ describe('Add Linting', () => { addLinting(tree, { name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.TsLint, + framework: 'react-native', }); const project = readProjectConfiguration(tree, 'my-app-e2e'); @@ -58,11 +67,14 @@ describe('Add Linting', () => { addLinting(tree, { name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.None, + framework: 'react-native', }); const project = readProjectConfiguration(tree, 'my-app-e2e'); diff --git a/packages/detox/src/generators/application/lib/add-linting.ts b/packages/detox/src/generators/application/lib/add-linting.ts index b02cad2609..fe45376b9b 100644 --- a/packages/detox/src/generators/application/lib/add-linting.ts +++ b/packages/detox/src/generators/application/lib/add-linting.ts @@ -39,7 +39,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { () => reactEslintJson ); - const installTask = await addDependenciesToPackageJson( + const installTask = addDependenciesToPackageJson( host, extraEslintDependencies.dependencies, extraEslintDependencies.devDependencies diff --git a/packages/detox/src/generators/application/lib/add-project.spec.ts b/packages/detox/src/generators/application/lib/add-project.spec.ts index b10fdf64de..c58ce25fde 100644 --- a/packages/detox/src/generators/application/lib/add-project.spec.ts +++ b/packages/detox/src/generators/application/lib/add-project.spec.ts @@ -31,11 +31,14 @@ describe('Add Project', () => { addProject(tree, { name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.EsLint, + framework: 'react-native', }); }); @@ -76,11 +79,14 @@ describe('Add Project', () => { addProject(tree, { name: 'my-dir-my-app-e2e', projectName: 'my-dir-my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-dir/my-app-e2e', project: 'my-dir-my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.EsLint, + framework: 'react-native', }); }); diff --git a/packages/detox/src/generators/application/lib/add-project.ts b/packages/detox/src/generators/application/lib/add-project.ts index 992cfd944d..3289bd01e0 100644 --- a/packages/detox/src/generators/application/lib/add-project.ts +++ b/packages/detox/src/generators/application/lib/add-project.ts @@ -3,6 +3,12 @@ import { TargetConfiguration, Tree, } from '@nrwl/devkit'; +import { + expoBuildTarget, + expoTestTarget, + reactNativeBuildTarget, + reactNativeTestTarget, +} from './get-targets'; import { NormalizedSchema } from './normalize-options'; export function addProject(host: Tree, options: NormalizedSchema) { @@ -17,59 +23,35 @@ export function addProject(host: Tree, options: NormalizedSchema) { } function getTargets(options: NormalizedSchema) { - const architect: { [key: string]: TargetConfiguration } = {}; + const targets: { [key: string]: TargetConfiguration } = {}; - architect['build-ios'] = { + targets['build-ios'] = { executor: '@nrwl/detox:build', - options: { - detoxConfiguration: 'ios.sim.debug', - }, - configurations: { - production: { - detoxConfiguration: 'ios.sim.release', - }, - }, + ...(options.framework === 'react-native' + ? reactNativeBuildTarget('ios') + : expoBuildTarget('ios')), }; - architect['test-ios'] = { + targets['test-ios'] = { executor: '@nrwl/detox:test', - options: { - detoxConfiguration: 'ios.sim.debug', - buildTarget: `${options.name}:build-ios`, - }, - configurations: { - production: { - detoxConfiguration: 'ios.sim.release', - buildTarget: `${options.name}:build-ios:prod`, - }, - }, + ...(options.framework === 'react-native' + ? reactNativeTestTarget('ios', options.name) + : expoTestTarget('ios', options.name)), }; - architect['build-android'] = { + targets['build-android'] = { executor: '@nrwl/detox:build', - options: { - detoxConfiguration: 'android.emu.debug', - }, - configurations: { - production: { - detoxConfiguration: 'android.emu.release', - }, - }, + ...(options.framework === 'react-native' + ? reactNativeBuildTarget('android') + : expoBuildTarget('android')), }; - architect['test-android'] = { + targets['test-android'] = { executor: '@nrwl/detox:test', - options: { - detoxConfiguration: 'android.emu.debug', - buildTarget: `${options.name}:build-android`, - }, - configurations: { - production: { - detoxConfiguration: 'android.emu.release', - buildTarget: `${options.name}:build-android:prod`, - }, - }, + ...(options.framework === 'react-native' + ? reactNativeTestTarget('android', options.name) + : expoTestTarget('android', options.name)), }; - return architect; + return targets; } diff --git a/packages/detox/src/generators/application/lib/create-files.spec.ts b/packages/detox/src/generators/application/lib/create-files.spec.ts index e945319f38..825be40a9d 100644 --- a/packages/detox/src/generators/application/lib/create-files.spec.ts +++ b/packages/detox/src/generators/application/lib/create-files.spec.ts @@ -14,11 +14,14 @@ describe('Create Files', () => { createFiles(tree, { name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.EsLint, + framework: 'react-native', }); expect(tree.exists('apps/my-app-e2e/.detoxrc.json')).toBeTruthy(); diff --git a/packages/detox/src/generators/application/lib/create-files.ts b/packages/detox/src/generators/application/lib/create-files.ts index f0583f3beb..32dd57e456 100644 --- a/packages/detox/src/generators/application/lib/create-files.ts +++ b/packages/detox/src/generators/application/lib/create-files.ts @@ -1,4 +1,11 @@ -import { generateFiles, offsetFromRoot, toJS, Tree } from '@nrwl/devkit'; +import { + detectPackageManager, + generateFiles, + getPackageManagerCommand, + offsetFromRoot, + toJS, + Tree, +} from '@nrwl/devkit'; import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript'; import { join } from 'path'; import { NormalizedSchema } from './normalize-options'; @@ -6,6 +13,7 @@ import { NormalizedSchema } from './normalize-options'; export function createFiles(host: Tree, options: NormalizedSchema) { generateFiles(host, join(__dirname, '../files/app'), options.projectRoot, { ...options, + exec: getPackageManagerCommand(detectPackageManager(host.root)).exec, offsetFromRoot: offsetFromRoot(options.projectRoot), rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot), }); diff --git a/packages/detox/src/generators/application/lib/get-targets.ts b/packages/detox/src/generators/application/lib/get-targets.ts new file mode 100644 index 0000000000..156df144dc --- /dev/null +++ b/packages/detox/src/generators/application/lib/get-targets.ts @@ -0,0 +1,69 @@ +export function reactNativeBuildTarget(platform: 'ios' | 'android') { + return { + options: { + detoxConfiguration: `${platform}.sim.debug`, + }, + configurations: { + production: { + detoxConfiguration: `${platform}.sim.release`, + }, + }, + }; +} + +export function expoBuildTarget(platform: string) { + return { + options: { + detoxConfiguration: `${platform}.sim.debug`, + }, + configurations: { + local: { + detoxConfiguration: `${platform}.sim.local`, + }, + bare: { + detoxConfiguration: `${platform}.sim.debug`, + }, + production: { + detoxConfiguration: `${platform}.sim.release`, + }, + }, + }; +} + +export function reactNativeTestTarget(platform: string, name: string) { + return { + options: { + detoxConfiguration: `${platform}.sim.debug`, + buildTarget: `${name}:build-ios`, + }, + configurations: { + production: { + detoxConfiguration: `${platform}.sim.release`, + buildTarget: `${name}:build-ios:production`, + }, + }, + }; +} + +export function expoTestTarget(platform: string, name: string) { + return { + options: { + detoxConfiguration: `${platform}.sim.eas`, + buildTarget: `${name}:build-ios`, + }, + configurations: { + local: { + detoxConfiguration: `${platform}.sim.local`, + buildTarget: `${name}:build-ios:local`, + }, + bare: { + detoxConfiguration: `${platform}.sim.debug`, + buildTarget: `${name}:build-ios:bare`, + }, + production: { + detoxConfiguration: `${platform}.sim.release`, + buildTarget: `${name}:build-ios:production`, + }, + }, + }; +} diff --git a/packages/detox/src/generators/application/lib/normalize-options.spec.ts b/packages/detox/src/generators/application/lib/normalize-options.spec.ts index 636e40ab1a..ca7381a751 100644 --- a/packages/detox/src/generators/application/lib/normalize-options.spec.ts +++ b/packages/detox/src/generators/application/lib/normalize-options.spec.ts @@ -17,18 +17,22 @@ describe('Normalize Options', () => { targets: {}, }); const schema: Schema = { + framework: 'react-native', name: 'my-app-e2e', project: 'my-app', linter: Linter.EsLint, }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ + framework: 'react-native', name: 'my-app-e2e', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', project: 'my-app', appFileName: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', linter: Linter.EsLint, }); }); @@ -39,17 +43,47 @@ describe('Normalize Options', () => { targets: {}, }); const schema: Schema = { + framework: 'react-native', name: 'myAppE2e', project: 'myApp', }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ appClassName: 'MyApp', + appDisplayName: 'MyApp', appFileName: 'my-app', name: 'my-app-e2e', project: 'myApp', projectName: 'my-app-e2e', + projectDirectory: 'apps', projectRoot: 'apps/my-app-e2e', + framework: 'react-native', + }); + }); + + it('should normalize options with display name', () => { + addProjectConfiguration(appTree, 'my-app', { + root: 'apps/my-app', + targets: {}, + }); + const schema: Schema = { + framework: 'react-native', + name: 'myAppE2e', + project: 'myApp', + displayName: 'app display name', + }; + const options = normalizeOptions(appTree, schema); + expect(options).toEqual({ + displayName: 'app display name', + appClassName: 'MyApp', + appDisplayName: 'AppDisplayName', + appFileName: 'my-app', + name: 'my-app-e2e', + project: 'myApp', + projectName: 'my-app-e2e', + projectDirectory: 'apps', + projectRoot: 'apps/my-app-e2e', + framework: 'react-native', }); }); @@ -59,6 +93,7 @@ describe('Normalize Options', () => { targets: {}, }); const schema: Schema = { + framework: 'react-native', name: 'my-app-e2e', project: 'my-app', directory: 'directory', @@ -67,11 +102,14 @@ describe('Normalize Options', () => { expect(options).toEqual({ project: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', appFileName: 'my-app', + projectDirectory: 'apps/directory', projectRoot: 'apps/directory/my-app-e2e', name: 'my-app-e2e', directory: 'directory', projectName: 'directory-my-app-e2e', + framework: 'react-native', }); }); @@ -81,6 +119,7 @@ describe('Normalize Options', () => { targets: {}, }); const schema: Schema = { + framework: 'react-native', name: 'directory/my-app-e2e', project: 'my-app', }; @@ -88,10 +127,13 @@ describe('Normalize Options', () => { expect(options).toEqual({ project: 'my-app', appClassName: 'MyApp', + appDisplayName: 'MyApp', appFileName: 'my-app', projectRoot: 'apps/directory/my-app-e2e', + projectDirectory: 'apps', name: 'directory/my-app-e2e', projectName: 'directory-my-app-e2e', + framework: 'react-native', }); }); }); diff --git a/packages/detox/src/generators/application/lib/normalize-options.ts b/packages/detox/src/generators/application/lib/normalize-options.ts index a197552c28..c07f9c5569 100644 --- a/packages/detox/src/generators/application/lib/normalize-options.ts +++ b/packages/detox/src/generators/application/lib/normalize-options.ts @@ -9,7 +9,9 @@ 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 + appDisplayName: string; // the display name of the app to be tested projectName: string; // the name of e2e project + projectDirectory: string; // the directory of e2e project projectRoot: string; // the root path of e2e project } @@ -31,9 +33,10 @@ export function normalizeOptions( const projectName = ( directoryFileName ? `${directoryFileName}-${fileName}` : fileName ).replace(new RegExp('/', 'g'), '-'); - const projectRoot = directoryFileName - ? joinPathFragments(appsDir, directoryFileName, fileName) - : joinPathFragments(appsDir, fileName); + const projectDirectory = directoryFileName + ? joinPathFragments(appsDir, directoryFileName) + : appsDir; + const projectRoot = joinPathFragments(projectDirectory, fileName); const { fileName: appFileName, className: appClassName } = names( options.project @@ -43,8 +46,12 @@ export function normalizeOptions( ...options, appFileName, appClassName, + appDisplayName: options.displayName + ? names(options.displayName).className + : appClassName, name: fileName, projectName, + projectDirectory, projectRoot, }; } diff --git a/packages/detox/src/generators/application/schema.d.ts b/packages/detox/src/generators/application/schema.d.ts index c8f9019d26..d333ae980c 100644 --- a/packages/detox/src/generators/application/schema.d.ts +++ b/packages/detox/src/generators/application/schema.d.ts @@ -1,11 +1,13 @@ import { Linter } from '@nrwl/linter'; export interface Schema { - project: string; - name: string; + project: string; // name of the project app to be tested + displayName?: string; // display name of the mobile app + name: string; // name of the e2e app directory?: string; linter?: Linter; js?: boolean; skipFormat?: boolean; setParserOptionsProject?: boolean; + framework: 'react-native' | 'expo'; } diff --git a/packages/detox/src/generators/application/schema.json b/packages/detox/src/generators/application/schema.json index 4d39100500..e9f5129ed7 100644 --- a/packages/detox/src/generators/application/schema.json +++ b/packages/detox/src/generators/application/schema.json @@ -21,6 +21,12 @@ }, "x-prompt": "What name would you like to use for the E2E project?" }, + "framework": { + "type": "string", + "description": "App framework to test", + "enum": ["react-native", "expo"], + "x-prompt": "What app framework should detox test?" + }, "directory": { "type": "string", "description": "A directory where the project is placed." @@ -47,5 +53,5 @@ "default": false } }, - "required": ["name", "project"] + "required": ["name", "project", "framework"] } diff --git a/packages/react-native/src/generators/application/lib/add-detox.ts b/packages/react-native/src/generators/application/lib/add-detox.ts index eef3d47877..7f4030c4e2 100644 --- a/packages/react-native/src/generators/application/lib/add-detox.ts +++ b/packages/react-native/src/generators/application/lib/add-detox.ts @@ -14,5 +14,6 @@ export async function addDetox(host: Tree, options: NormalizedSchema) { name: `${options.name}-e2e`, directory: options.directory, project: options.projectName, + framework: 'react-native', }); }