From 42e4e1919f7c8c42711c5957599467b72b13f484 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:37:31 +0000 Subject: [PATCH 01/11] chore(deps-dev): bump react-native Bumps the react-native group with 1 update in the /assets/template directory: [react-native](https://github.com/react/react-native/tree/HEAD/packages/react-native). Updates `react-native` from 0.86.0 to 0.87.1 - [Release notes](https://github.com/react/react-native/releases) - [Changelog](https://github.com/react/react-native/blob/main/CHANGELOG.md) - [Commits](https://github.com/react/react-native/commits/v0.87.1/packages/react-native) --- updated-dependencies: - dependency-name: react-native dependency-version: 0.87.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: react-native ... Signed-off-by: dependabot[bot] --- assets/template/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/template/package.json b/assets/template/package.json index cc80725..3127d2f 100644 --- a/assets/template/package.json +++ b/assets/template/package.json @@ -59,7 +59,7 @@ "@types/react": "19.2.0", "nitrogen": "^0.37.1", "react": "19.2.3", - "react-native": "0.86.0", + "react-native": "0.87.1", "react-native-builder-bob": "^0.43.0", "react-native-nitro-modules": "^0.37.1", "conventional-changelog-conventionalcommits": "^10.2.1", From a47adcd249ca671923ed872a5a690912df51209d Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Fri, 18 Sep 2026 10:47:31 +0200 Subject: [PATCH 02/11] fix: update nitro template deps for react native 0.87 --- e2e/harness-workflows.test.ts | 31 +++++++++++++++++++++++++++++++ src/code-snippets/code.js.ts | 1 + 2 files changed, 32 insertions(+) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index df90b69..647abbc 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -17,6 +17,10 @@ type WorkflowExpectation = { readonly rootDir: string } +type PackageJson = { + readonly devDependencies?: Record +} + const execFileAsync = promisify(execFile) const generatedRoots: string[] = [] @@ -108,6 +112,20 @@ const assertHarnessScripts = async (rootDir: string): Promise => { ) } +const assertTemplateDependencyVersions = async ( + rootDir: string, + packagePath: string +): Promise => { + const packageJson = JSON.parse( + await readText(path.join(rootDir, packagePath, 'package.json')) + ) as PackageJson + const devDependencies = packageJson.devDependencies + + expect(devDependencies?.nitrogen).toBe('^0.37.1') + expect(devDependencies?.['react-native']).toBe('0.87.1') + expect(devDependencies?.['react-native-nitro-modules']).toBe('^0.37.1') +} + const assertHarnessWorkflowContent = async ( expectation: WorkflowExpectation ): Promise => { @@ -155,6 +173,15 @@ const assertHarnessWorkflowContent = async ( expect(iosBuildWorkflow).not.toContain('$$exampleApp$$') } +const assertHarnessConfigContent = async (rootDir: string): Promise => { + const harnessConfig = await readText( + path.join(rootDir, 'example', 'rn-harness.config.mjs') + ) + + expect(harnessConfig).toContain('platformReadyTimeout: 600000') + expect(harnessConfig).toContain('bridgeTimeout: 300000') +} + afterAll(async () => { await Promise.all( generatedRoots.map(rootDir => @@ -169,6 +196,8 @@ describe('React Native Harness workflow generation', () => { await assertWorkflowFiles(project.rootDir) await assertHarnessScripts(project.rootDir) + await assertTemplateDependencyVersions(project.rootDir, '.') + await assertHarnessConfigContent(project.rootDir) await assertHarnessWorkflowContent({ androidBuildWorkflowPath: 'android/**', harnessWorkflowPath: 'src/**', @@ -183,6 +212,8 @@ describe('React Native Harness workflow generation', () => { await assertWorkflowFiles(project.rootDir) await assertHarnessScripts(project.rootDir) + await assertTemplateDependencyVersions(project.rootDir, packagePath) + await assertHarnessConfigContent(project.rootDir) await assertHarnessWorkflowContent({ androidBuildWorkflowPath: `${packagePath}/android/**`, harnessWorkflowPath: `${packagePath}/src/**`, diff --git a/src/code-snippets/code.js.ts b/src/code-snippets/code.js.ts index ec86acc..0a5cda1 100644 --- a/src/code-snippets/code.js.ts +++ b/src/code-snippets/code.js.ts @@ -252,6 +252,7 @@ const config = { ${runners} ], defaultRunner: '${defaultRunner}', + platformReadyTimeout: 600000, bridgeTimeout: 300000, } From d8f2cb209c49cc8baa979a6a405f96734ade63e2 Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Fri, 18 Sep 2026 23:43:00 +0200 Subject: [PATCH 03/11] fix: disable metro package exports in generated examples --- e2e/harness-workflows.test.ts | 10 ++++++++++ src/code-snippets/code.js.ts | 3 +++ 2 files changed, 13 insertions(+) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index 647abbc..a5d6a98 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -182,6 +182,14 @@ const assertHarnessConfigContent = async (rootDir: string): Promise => { expect(harnessConfig).toContain('bridgeTimeout: 300000') } +const assertMetroConfigContent = async (rootDir: string): Promise => { + const metroConfig = await readText( + path.join(rootDir, 'example', 'metro.config.js') + ) + + expect(metroConfig).toContain('unstable_enablePackageExports: false') +} + afterAll(async () => { await Promise.all( generatedRoots.map(rootDir => @@ -198,6 +206,7 @@ describe('React Native Harness workflow generation', () => { await assertHarnessScripts(project.rootDir) await assertTemplateDependencyVersions(project.rootDir, '.') await assertHarnessConfigContent(project.rootDir) + await assertMetroConfigContent(project.rootDir) await assertHarnessWorkflowContent({ androidBuildWorkflowPath: 'android/**', harnessWorkflowPath: 'src/**', @@ -214,6 +223,7 @@ describe('React Native Harness workflow generation', () => { await assertHarnessScripts(project.rootDir) await assertTemplateDependencyVersions(project.rootDir, packagePath) await assertHarnessConfigContent(project.rootDir) + await assertMetroConfigContent(project.rootDir) await assertHarnessWorkflowContent({ androidBuildWorkflowPath: `${packagePath}/android/**`, harnessWorkflowPath: `${packagePath}/src/**`, diff --git a/src/code-snippets/code.js.ts b/src/code-snippets/code.js.ts index 0a5cda1..ac4b1b4 100644 --- a/src/code-snippets/code.js.ts +++ b/src/code-snippets/code.js.ts @@ -119,6 +119,9 @@ const root = path.resolve(__dirname, '${packageRelativePath}'); */ const config = { watchFolders: [root], + resolver: { + unstable_enablePackageExports: false, + }, }; module.exports = mergeConfig(getDefaultConfig(__dirname), config);` From b59310e9cee940235da435d242480143da585ed4 Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Fri, 18 Sep 2026 23:44:37 +0200 Subject: [PATCH 04/11] chore: clean up kotlin snippet --- src/code-snippets/code.kotlin.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/code-snippets/code.kotlin.ts b/src/code-snippets/code.kotlin.ts index 9213887..c7939d8 100644 --- a/src/code-snippets/code.kotlin.ts +++ b/src/code-snippets/code.kotlin.ts @@ -49,18 +49,13 @@ import com.margelo.nitro.${replaceHyphen(moduleName)}.Hybrid${toPascalCase( @DoNotStrip class Hybrid${toPascalCase(moduleName)}(val context: ThemedReactContext): Hybrid${toPascalCase(moduleName)}Spec() { // View - override val view: View = View(context) + override val view = View(context) // Props - private var _isRed = false - override var isRed: Boolean - get() = _isRed + override var isRed: Boolean = false set(value) { - _isRed = value - view.setBackgroundColor( - if (value) Color.RED - else Color.BLACK - ) + field = value + view.setBackgroundColor(if (value) Color.RED else Color.BLACK) } } ` From 34deea718b8d815faf4b98990d7cab2feb7ea508 Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Fri, 18 Sep 2026 23:49:59 +0200 Subject: [PATCH 05/11] fix: stabilize generated view harness queries --- e2e/harness-workflows.test.ts | 40 ++++++++++++++++++++++++++++++++--- src/code-snippets/code.js.ts | 13 ++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index a5d6a98..1524c7a 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -26,7 +26,8 @@ const generatedRoots: string[] = [] const createProject = async ( packageName: string, - monorepo: boolean + monorepo: boolean, + packageType: 'module' | 'view' ): Promise => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'nitro-cli-e2e-')) generatedRoots.push(rootDir) @@ -55,6 +56,8 @@ const createProject = async ( '--include-harness', '--skip-install', '--ci', + '--package-type', + packageType, ] if (monorepo) { @@ -190,6 +193,27 @@ const assertMetroConfigContent = async (rootDir: string): Promise => { expect(metroConfig).toContain('unstable_enablePackageExports: false') } +const assertHarnessViewTestContent = async ( + rootDir: string, + harnessFileName: string, + testID: string +): Promise => { + const harnessTest = await readText( + path.join( + rootDir, + 'example', + '__tests__', + `${harnessFileName}.harness.tsx` + ) + ) + + expect(harnessTest).toContain( + "import { StyleSheet, View } from 'react-native'" + ) + expect(harnessTest).toContain('collapsable={false}') + expect(harnessTest).toContain(`testID="${testID}"`) +} + afterAll(async () => { await Promise.all( generatedRoots.map(rootDir => @@ -200,7 +224,7 @@ afterAll(async () => { describe('React Native Harness workflow generation', () => { test('generates build and harness workflows for the default project layout', async () => { - const project = await createProject('rootharness', false) + const project = await createProject('rootharness', false, 'module') await assertWorkflowFiles(project.rootDir) await assertHarnessScripts(project.rootDir) @@ -216,7 +240,7 @@ describe('React Native Harness workflow generation', () => { }, 120_000) test('generates build and harness workflows for the monorepo project layout', async () => { - const project = await createProject('monoharness', true) + const project = await createProject('monoharness', true, 'module') const packagePath = `packages/react-native-${project.packageName}` await assertWorkflowFiles(project.rootDir) @@ -231,4 +255,14 @@ describe('React Native Harness workflow generation', () => { rootDir: project.rootDir, }) }, 120_000) + + test('generates stable harness test queries for native views', async () => { + const project = await createProject('viewharness', false, 'view') + + await assertHarnessViewTestContent( + project.rootDir, + 'viewharness', + 'viewharness' + ) + }, 120_000) }) diff --git a/src/code-snippets/code.js.ts b/src/code-snippets/code.js.ts index ac4b1b4..13a0402 100644 --- a/src/code-snippets/code.js.ts +++ b/src/code-snippets/code.js.ts @@ -295,7 +295,7 @@ describe('${toPascalCase(moduleName)}', () => { } return `import React from 'react' -import { StyleSheet } from 'react-native' +import { StyleSheet, View } from 'react-native' import { describe, it, expect, render } from 'react-native-harness' import { screen } from '@react-native-harness/ui' import { ${toPascalCase(moduleName)} } from '${finalModuleName}' @@ -303,11 +303,12 @@ import { ${toPascalCase(moduleName)} } from '${finalModuleName}' describe('${toPascalCase(moduleName)}', () => { it('renders the native view', async () => { await render( - <${toPascalCase(moduleName)} - isRed={true} - style={styles.view} - testID="${moduleName}" - /> + + <${toPascalCase(moduleName)} + isRed={true} + style={styles.view} + /> + ) const view = await screen.findByTestId('${moduleName}') From ea3ee9328d197e922ac0964e49b4263348a01a9f Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Sat, 19 Sep 2026 00:04:40 +0200 Subject: [PATCH 06/11] Revert "fix: stabilize generated view harness queries" This reverts commit 34deea718b8d815faf4b98990d7cab2feb7ea508. --- e2e/harness-workflows.test.ts | 40 +++-------------------------------- src/code-snippets/code.js.ts | 13 ++++++------ 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index 1524c7a..a5d6a98 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -26,8 +26,7 @@ const generatedRoots: string[] = [] const createProject = async ( packageName: string, - monorepo: boolean, - packageType: 'module' | 'view' + monorepo: boolean ): Promise => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'nitro-cli-e2e-')) generatedRoots.push(rootDir) @@ -56,8 +55,6 @@ const createProject = async ( '--include-harness', '--skip-install', '--ci', - '--package-type', - packageType, ] if (monorepo) { @@ -193,27 +190,6 @@ const assertMetroConfigContent = async (rootDir: string): Promise => { expect(metroConfig).toContain('unstable_enablePackageExports: false') } -const assertHarnessViewTestContent = async ( - rootDir: string, - harnessFileName: string, - testID: string -): Promise => { - const harnessTest = await readText( - path.join( - rootDir, - 'example', - '__tests__', - `${harnessFileName}.harness.tsx` - ) - ) - - expect(harnessTest).toContain( - "import { StyleSheet, View } from 'react-native'" - ) - expect(harnessTest).toContain('collapsable={false}') - expect(harnessTest).toContain(`testID="${testID}"`) -} - afterAll(async () => { await Promise.all( generatedRoots.map(rootDir => @@ -224,7 +200,7 @@ afterAll(async () => { describe('React Native Harness workflow generation', () => { test('generates build and harness workflows for the default project layout', async () => { - const project = await createProject('rootharness', false, 'module') + const project = await createProject('rootharness', false) await assertWorkflowFiles(project.rootDir) await assertHarnessScripts(project.rootDir) @@ -240,7 +216,7 @@ describe('React Native Harness workflow generation', () => { }, 120_000) test('generates build and harness workflows for the monorepo project layout', async () => { - const project = await createProject('monoharness', true, 'module') + const project = await createProject('monoharness', true) const packagePath = `packages/react-native-${project.packageName}` await assertWorkflowFiles(project.rootDir) @@ -255,14 +231,4 @@ describe('React Native Harness workflow generation', () => { rootDir: project.rootDir, }) }, 120_000) - - test('generates stable harness test queries for native views', async () => { - const project = await createProject('viewharness', false, 'view') - - await assertHarnessViewTestContent( - project.rootDir, - 'viewharness', - 'viewharness' - ) - }, 120_000) }) diff --git a/src/code-snippets/code.js.ts b/src/code-snippets/code.js.ts index 13a0402..ac4b1b4 100644 --- a/src/code-snippets/code.js.ts +++ b/src/code-snippets/code.js.ts @@ -295,7 +295,7 @@ describe('${toPascalCase(moduleName)}', () => { } return `import React from 'react' -import { StyleSheet, View } from 'react-native' +import { StyleSheet } from 'react-native' import { describe, it, expect, render } from 'react-native-harness' import { screen } from '@react-native-harness/ui' import { ${toPascalCase(moduleName)} } from '${finalModuleName}' @@ -303,12 +303,11 @@ import { ${toPascalCase(moduleName)} } from '${finalModuleName}' describe('${toPascalCase(moduleName)}', () => { it('renders the native view', async () => { await render( - - <${toPascalCase(moduleName)} - isRed={true} - style={styles.view} - /> - + <${toPascalCase(moduleName)} + isRed={true} + style={styles.view} + testID="${moduleName}" + /> ) const view = await screen.findByTestId('${moduleName}') From ee67738d8cd19b965b9e8a1ead0608dc386ca7cc Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Sat, 19 Sep 2026 00:04:40 +0200 Subject: [PATCH 07/11] Revert "chore: clean up kotlin snippet" This reverts commit b59310e9cee940235da435d242480143da585ed4. --- src/code-snippets/code.kotlin.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/code-snippets/code.kotlin.ts b/src/code-snippets/code.kotlin.ts index c7939d8..9213887 100644 --- a/src/code-snippets/code.kotlin.ts +++ b/src/code-snippets/code.kotlin.ts @@ -49,13 +49,18 @@ import com.margelo.nitro.${replaceHyphen(moduleName)}.Hybrid${toPascalCase( @DoNotStrip class Hybrid${toPascalCase(moduleName)}(val context: ThemedReactContext): Hybrid${toPascalCase(moduleName)}Spec() { // View - override val view = View(context) + override val view: View = View(context) // Props - override var isRed: Boolean = false + private var _isRed = false + override var isRed: Boolean + get() = _isRed set(value) { - field = value - view.setBackgroundColor(if (value) Color.RED else Color.BLACK) + _isRed = value + view.setBackgroundColor( + if (value) Color.RED + else Color.BLACK + ) } } ` From 5fdf105c68b34b7135565b35c45f74de768d1744 Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Sat, 19 Sep 2026 00:05:40 +0200 Subject: [PATCH 08/11] Reapply "chore: clean up kotlin snippet" This reverts commit ee67738d8cd19b965b9e8a1ead0608dc386ca7cc. --- src/code-snippets/code.kotlin.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/code-snippets/code.kotlin.ts b/src/code-snippets/code.kotlin.ts index 9213887..c7939d8 100644 --- a/src/code-snippets/code.kotlin.ts +++ b/src/code-snippets/code.kotlin.ts @@ -49,18 +49,13 @@ import com.margelo.nitro.${replaceHyphen(moduleName)}.Hybrid${toPascalCase( @DoNotStrip class Hybrid${toPascalCase(moduleName)}(val context: ThemedReactContext): Hybrid${toPascalCase(moduleName)}Spec() { // View - override val view: View = View(context) + override val view = View(context) // Props - private var _isRed = false - override var isRed: Boolean - get() = _isRed + override var isRed: Boolean = false set(value) { - _isRed = value - view.setBackgroundColor( - if (value) Color.RED - else Color.BLACK - ) + field = value + view.setBackgroundColor(if (value) Color.RED else Color.BLACK) } } ` From d7ba7f8a4419c06bb656bea39a51ca522e320bd5 Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Sat, 19 Sep 2026 00:05:40 +0200 Subject: [PATCH 09/11] Revert "fix: disable metro package exports in generated examples" This reverts commit d8f2cb209c49cc8baa979a6a405f96734ade63e2. --- e2e/harness-workflows.test.ts | 10 ---------- src/code-snippets/code.js.ts | 3 --- 2 files changed, 13 deletions(-) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index a5d6a98..647abbc 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -182,14 +182,6 @@ const assertHarnessConfigContent = async (rootDir: string): Promise => { expect(harnessConfig).toContain('bridgeTimeout: 300000') } -const assertMetroConfigContent = async (rootDir: string): Promise => { - const metroConfig = await readText( - path.join(rootDir, 'example', 'metro.config.js') - ) - - expect(metroConfig).toContain('unstable_enablePackageExports: false') -} - afterAll(async () => { await Promise.all( generatedRoots.map(rootDir => @@ -206,7 +198,6 @@ describe('React Native Harness workflow generation', () => { await assertHarnessScripts(project.rootDir) await assertTemplateDependencyVersions(project.rootDir, '.') await assertHarnessConfigContent(project.rootDir) - await assertMetroConfigContent(project.rootDir) await assertHarnessWorkflowContent({ androidBuildWorkflowPath: 'android/**', harnessWorkflowPath: 'src/**', @@ -223,7 +214,6 @@ describe('React Native Harness workflow generation', () => { await assertHarnessScripts(project.rootDir) await assertTemplateDependencyVersions(project.rootDir, packagePath) await assertHarnessConfigContent(project.rootDir) - await assertMetroConfigContent(project.rootDir) await assertHarnessWorkflowContent({ androidBuildWorkflowPath: `${packagePath}/android/**`, harnessWorkflowPath: `${packagePath}/src/**`, diff --git a/src/code-snippets/code.js.ts b/src/code-snippets/code.js.ts index ac4b1b4..0a5cda1 100644 --- a/src/code-snippets/code.js.ts +++ b/src/code-snippets/code.js.ts @@ -119,9 +119,6 @@ const root = path.resolve(__dirname, '${packageRelativePath}'); */ const config = { watchFolders: [root], - resolver: { - unstable_enablePackageExports: false, - }, }; module.exports = mergeConfig(getDefaultConfig(__dirname), config);` From fb73a1c012ff9b7182c7fb9d6013c09fadf22c3d Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Sat, 19 Sep 2026 00:22:54 +0200 Subject: [PATCH 10/11] fix: simplify generated harness scripts --- e2e/harness-workflows.test.ts | 7 ++---- src/generate-nitro-package.ts | 41 ++--------------------------------- 2 files changed, 4 insertions(+), 44 deletions(-) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index 647abbc..a3ddc2b 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -101,13 +101,10 @@ const assertHarnessScripts = async (rootDir: string): Promise => { const scripts = examplePackageJson.scripts expect(scripts?.['test:harness']).toBe('react-native-harness') - expect(scripts?.['test:harness:android']).toContain( - 'chmod +x android/gradlew' - ) - expect(scripts?.['test:harness:android']).toContain( + expect(scripts?.['test:harness:android']).toBe( 'react-native-harness --harnessRunner android' ) - expect(scripts?.['test:harness:ios']).toContain( + expect(scripts?.['test:harness:ios']).toBe( 'react-native-harness --harnessRunner ios' ) } diff --git a/src/generate-nitro-package.ts b/src/generate-nitro-package.ts index b01e463..ed95fb4 100644 --- a/src/generate-nitro-package.ts +++ b/src/generate-nitro-package.ts @@ -597,7 +597,6 @@ export class NitroModuleFactory { encoding: 'utf8', }) const exampleAppPackageJson = JSON.parse(examplePackageJsonStr) - const exampleAppName = `${toPascalCase(this.config.packageName)}Example` exampleAppPackageJson.name = `${this.config.finalPackageName}-example` @@ -642,50 +641,14 @@ export class NitroModuleFactory { 'test:harness': 'react-native-harness', ...(this.config.platforms.includes(SupportedPlatform.ANDROID) ? { - 'test:harness:android': [ - 'if [ -z "${HARNESS_APP_PATH:-}" ]; then', - ' set -euo pipefail', - ' chmod +x android/gradlew', - ' (cd android && ./gradlew assembleDebug --no-daemon --build-cache)', - ' HARNESS_APP_PATH="$(find android/app/build/outputs/apk/debug -maxdepth 1 -type f -name "*.apk" | head -1)"', - ' if [ -z "${HARNESS_APP_PATH}" ]; then', - ' echo "Unable to locate the built Android app bundle."', - ' exit 1', - ' fi', - ' export HARNESS_APP_PATH', - 'fi', + 'test:harness:android': 'react-native-harness --harnessRunner android', - ].join('\n'), } : {}), ...(this.config.platforms.includes(SupportedPlatform.IOS) ? { - 'test:harness:ios': [ - 'set -euo pipefail', - 'if [ -z "${DEVICE_MODEL:-}" ] || [ -z "${IOS_VERSION:-}" ]; then', - ' IOS_VERSION="${IOS_VERSION:-26.5}"', - ' export IOS_VERSION', - ' IOS_SIMULATOR="$(xcrun simctl list devices available --json | node -e \'const fs = require("node:fs"); const input = fs.readFileSync(0, "utf8"); const data = JSON.parse(input); const desiredVersion = process.env.IOS_VERSION; const candidates = Object.entries(data.devices).flatMap(([runtime, devices]) => { const version = runtime.match(/iOS-(\\d+(?:-\\d+)*)$/)?.[1]?.replaceAll("-", "."); if (version == null || (desiredVersion != null && version !== desiredVersion)) return []; return devices.filter(device => device.isAvailable === true && device.name.startsWith("iPhone")).map(device => ({ name: device.name, version })); }); candidates.sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }) || b.name.localeCompare(a.name, undefined, { numeric: true })); const selected = candidates[0]; if (selected == null) process.exit(1); console.log(`${selected.name}|${selected.version}`);\' || true)"', - ' if [ -z "${IOS_SIMULATOR}" ]; then', - ' echo "Unable to resolve an available iOS simulator."', - ' xcrun simctl list devices available', - ' exit 1', - ' fi', - ' DEVICE_MODEL="${IOS_SIMULATOR%%|*}"', - ' IOS_VERSION="${IOS_SIMULATOR#*|}"', - ' export DEVICE_MODEL IOS_VERSION', - 'fi', - 'if [ -z "${HARNESS_APP_PATH:-}" ]; then', - ` xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ -derivedDataPath build -UseModernBuildSystem=YES -workspace ${exampleAppName}.xcworkspace -scheme ${exampleAppName} -sdk iphonesimulator -configuration Debug build CODE_SIGNING_ALLOWED=NO`, - ' HARNESS_APP_PATH="$(find ios/build/Build/Products/Debug-iphonesimulator -maxdepth 1 -type d -name "*.app" | head -1)"', - ' if [ -z "${HARNESS_APP_PATH}" ]; then', - ' echo "Unable to locate the built iOS app bundle."', - ' exit 1', - ' fi', - ' export HARNESS_APP_PATH', - 'fi', + 'test:harness:ios': 'react-native-harness --harnessRunner ios', - ].join('\n'), } : {}), } From ad55fb3adff1463da7e0ef240d2bbb3b5882925c Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Sat, 19 Sep 2026 01:04:07 +0200 Subject: [PATCH 11/11] fix: stabilize generated view harness query --- e2e/harness-workflows.test.ts | 40 ++++++++++++++++++++++++++++++++--- src/code-snippets/code.js.ts | 13 ++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/e2e/harness-workflows.test.ts b/e2e/harness-workflows.test.ts index a3ddc2b..39f6eb4 100644 --- a/e2e/harness-workflows.test.ts +++ b/e2e/harness-workflows.test.ts @@ -26,7 +26,8 @@ const generatedRoots: string[] = [] const createProject = async ( packageName: string, - monorepo: boolean + monorepo: boolean, + packageType: 'module' | 'view' ): Promise => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'nitro-cli-e2e-')) generatedRoots.push(rootDir) @@ -55,6 +56,8 @@ const createProject = async ( '--include-harness', '--skip-install', '--ci', + '--package-type', + packageType, ] if (monorepo) { @@ -179,6 +182,27 @@ const assertHarnessConfigContent = async (rootDir: string): Promise => { expect(harnessConfig).toContain('bridgeTimeout: 300000') } +const assertHarnessViewTestContent = async ( + rootDir: string, + harnessFileName: string, + testID: string +): Promise => { + const harnessTest = await readText( + path.join( + rootDir, + 'example', + '__tests__', + `${harnessFileName}.harness.tsx` + ) + ) + + expect(harnessTest).toContain( + "import { StyleSheet, View } from 'react-native'" + ) + expect(harnessTest).toContain('collapsable={false}') + expect(harnessTest).toContain(`testID="${testID}"`) +} + afterAll(async () => { await Promise.all( generatedRoots.map(rootDir => @@ -189,7 +213,7 @@ afterAll(async () => { describe('React Native Harness workflow generation', () => { test('generates build and harness workflows for the default project layout', async () => { - const project = await createProject('rootharness', false) + const project = await createProject('rootharness', false, 'module') await assertWorkflowFiles(project.rootDir) await assertHarnessScripts(project.rootDir) @@ -204,7 +228,7 @@ describe('React Native Harness workflow generation', () => { }, 120_000) test('generates build and harness workflows for the monorepo project layout', async () => { - const project = await createProject('monoharness', true) + const project = await createProject('monoharness', true, 'module') const packagePath = `packages/react-native-${project.packageName}` await assertWorkflowFiles(project.rootDir) @@ -218,4 +242,14 @@ describe('React Native Harness workflow generation', () => { rootDir: project.rootDir, }) }, 120_000) + + test('generates stable harness test queries for native views', async () => { + const project = await createProject('viewharness', false, 'view') + + await assertHarnessViewTestContent( + project.rootDir, + 'viewharness', + 'viewharness' + ) + }, 120_000) }) diff --git a/src/code-snippets/code.js.ts b/src/code-snippets/code.js.ts index 0a5cda1..5a40049 100644 --- a/src/code-snippets/code.js.ts +++ b/src/code-snippets/code.js.ts @@ -292,7 +292,7 @@ describe('${toPascalCase(moduleName)}', () => { } return `import React from 'react' -import { StyleSheet } from 'react-native' +import { StyleSheet, View } from 'react-native' import { describe, it, expect, render } from 'react-native-harness' import { screen } from '@react-native-harness/ui' import { ${toPascalCase(moduleName)} } from '${finalModuleName}' @@ -300,11 +300,12 @@ import { ${toPascalCase(moduleName)} } from '${finalModuleName}' describe('${toPascalCase(moduleName)}', () => { it('renders the native view', async () => { await render( - <${toPascalCase(moduleName)} - isRed={true} - style={styles.view} - testID="${moduleName}" - /> + + <${toPascalCase(moduleName)} + isRed={true} + style={styles.view} + /> + ) const view = await screen.findByTestId('${moduleName}')