Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
78 changes: 70 additions & 8 deletions e2e/harness-workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ type WorkflowExpectation = {
readonly rootDir: string
}

type PackageJson = {
readonly devDependencies?: Record<string, string>
}

const execFileAsync = promisify(execFile)
const generatedRoots: string[] = []

const createProject = async (
packageName: string,
monorepo: boolean
monorepo: boolean,
packageType: 'module' | 'view'
): Promise<GeneratedProject> => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'nitro-cli-e2e-'))
generatedRoots.push(rootDir)
Expand Down Expand Up @@ -51,6 +56,8 @@ const createProject = async (
'--include-harness',
'--skip-install',
'--ci',
'--package-type',
packageType,
]

if (monorepo) {
Expand Down Expand Up @@ -97,17 +104,28 @@ const assertHarnessScripts = async (rootDir: string): Promise<void> => {
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'
)
}

const assertTemplateDependencyVersions = async (
rootDir: string,
packagePath: string
): Promise<void> => {
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<void> => {
Expand Down Expand Up @@ -155,6 +173,36 @@ const assertHarnessWorkflowContent = async (
expect(iosBuildWorkflow).not.toContain('$$exampleApp$$')
}

const assertHarnessConfigContent = async (rootDir: string): Promise<void> => {
const harnessConfig = await readText(
path.join(rootDir, 'example', 'rn-harness.config.mjs')
)

expect(harnessConfig).toContain('platformReadyTimeout: 600000')
expect(harnessConfig).toContain('bridgeTimeout: 300000')
}

const assertHarnessViewTestContent = async (
rootDir: string,
harnessFileName: string,
testID: string
): Promise<void> => {
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 =>
Expand All @@ -165,10 +213,12 @@ 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)
await assertTemplateDependencyVersions(project.rootDir, '.')
await assertHarnessConfigContent(project.rootDir)
await assertHarnessWorkflowContent({
androidBuildWorkflowPath: 'android/**',
harnessWorkflowPath: 'src/**',
Expand All @@ -178,16 +228,28 @@ 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)
await assertHarnessScripts(project.rootDir)
await assertTemplateDependencyVersions(project.rootDir, packagePath)
await assertHarnessConfigContent(project.rootDir)
await assertHarnessWorkflowContent({
androidBuildWorkflowPath: `${packagePath}/android/**`,
harnessWorkflowPath: `${packagePath}/src/**`,
iosBuildWorkflowPath: `${packagePath}/ios/**`,
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)
})
14 changes: 8 additions & 6 deletions src/code-snippets/code.js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ const config = {
${runners}
],
defaultRunner: '${defaultRunner}',
platformReadyTimeout: 600000,
bridgeTimeout: 300000,
}

Expand Down Expand Up @@ -291,19 +292,20 @@ 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}'

describe('${toPascalCase(moduleName)}', () => {
it('renders the native view', async () => {
await render(
<${toPascalCase(moduleName)}
isRed={true}
style={styles.view}
testID="${moduleName}"
/>
<View collapsable={false} testID="${moduleName}">
<${toPascalCase(moduleName)}
isRed={true}
style={styles.view}
/>
</View>
)

const view = await screen.findByTestId('${moduleName}')
Expand Down
6 changes: 2 additions & 4 deletions src/code-snippets/code.kotlin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@ class Hybrid${toPascalCase(moduleName)}(
override val view: View = View(context)

// Props
private var _isRed = false
override var isRed: Boolean
get() = _isRed
override var isRed: Boolean = false
set(value) {
_isRed = value
field = value
view.setBackgroundColor(if (value) Color.RED else Color.BLACK)
}
}
Expand Down
41 changes: 2 additions & 39 deletions src/generate-nitro-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,6 @@ export class NitroModuleFactory {
encoding: 'utf8',
})
const exampleAppPackageJson = JSON.parse(examplePackageJsonStr)
const exampleAppName = `${toPascalCase(this.config.packageName)}Example`

exampleAppPackageJson.name = `${this.config.finalPackageName}-example`

Expand Down Expand Up @@ -737,50 +736,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'),
}
: {}),
}
Expand Down
Loading