From 2e425940997f04a2acfda1346f3aafb45dd0cb12 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Wed, 5 Jan 2022 10:28:31 +0100 Subject: [PATCH] test(android): add tests for Gradle utility functions (#687) --- .github/actions/setup-toolchain/action.yml | 1 - .gitignore | 1 + test/android-test-app/gradle.js | 114 ++++++++++ test/android-test-app/test-app-util.test.js | 236 ++++++++++++++++++++ 4 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 test/android-test-app/gradle.js create mode 100644 test/android-test-app/test-app-util.test.js diff --git a/.github/actions/setup-toolchain/action.yml b/.github/actions/setup-toolchain/action.yml index adc8c5ce..5231579f 100644 --- a/.github/actions/setup-toolchain/action.yml +++ b/.github/actions/setup-toolchain/action.yml @@ -11,7 +11,6 @@ runs: with: node-version: 14 - name: Set up JDK - if: ${{ inputs.platform == 'android' }} uses: actions/setup-java@v2.5.0 with: distribution: temurin diff --git a/.gitignore b/.gitignore index 1b617629..e78e7798 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.tgz *.xcworkspace/ .DS_Store +.android-test-* .gradle/ .idea/ .vs/ diff --git a/test/android-test-app/gradle.js b/test/android-test-app/gradle.js new file mode 100644 index 00000000..0683316e --- /dev/null +++ b/test/android-test-app/gradle.js @@ -0,0 +1,114 @@ +// @ts-check +// istanbul ignore file + +const { spawnSync } = require("child_process"); +const fs = require("fs/promises"); +const os = require("os"); +const path = require("path"); +const { version: targetVersion } = require("react-native/package.json"); +const { gatherConfig, writeAllFiles } = require("../../scripts/configure"); + +/** + * Joins the strings if an array is passed, otherwise returns the string. + * @param {string | string[]} strings + * @returns + */ +function joinStrings(strings, separator = "") { + return Array.isArray(strings) ? strings.join(separator) : strings; +} + +/** + * Returns project path given name. + * @param {string} name + */ +function projectPath(name) { + return `.android-test-${name}`; +} + +/** + * Initializes a React Native project. + * @param {string} name + * @param {import("../../scripts/configure").ConfigureParams["platforms"]} platforms + * @param {Record=} setupFiles + */ +async function makeProject(name, platforms, setupFiles = {}) { + const packagePath = projectPath(name); + const { files } = gatherConfig({ + name, + packagePath, + testAppPath: path.resolve(__dirname, "..", ".."), + targetVersion, + platforms, + flatten: true, + force: true, + init: true, + }); + + await writeAllFiles(files, packagePath); + + try { + await fs.symlink( + path.resolve(__dirname, "..", "..", "example", "node_modules"), + path.join(packagePath, "node_modules"), + "dir" + ); + } catch (e) { + // @ts-ignore + if (e.code !== "EEXIST") { + throw e; + } + } + + await Promise.all( + Object.entries(setupFiles).map(([filename, content]) => { + const p = path.join(packagePath, filename); + return fs + .mkdir(path.dirname(p), { recursive: true }) + .then(() => fs.writeFile(p, joinStrings(content, "\n"))); + }) + ); + + return packagePath; +} + +/** + * Removes specified project. + * @param {string} name + */ +function removeProject(name) { + fs.rm(projectPath(name), { + maxRetries: 3, + recursive: true, + }); +} + +/** + * Runs Gradle in specified directory. + * @param {string} cwd + * @param {...string} args arguments + * @returns + */ +function runGradle(cwd, ...args) { + const gradlew = os.platform() === "win32" ? "gradlew.bat" : "./gradlew"; + return spawnSync(gradlew, args, { cwd, encoding: "utf-8" }); +} + +/** + * Initializes a new React Native project and runs Gradle. + * @param {string} name + * @param {import("../../scripts/configure").ConfigureParams["platforms"]} platforms + * @param {Record=} setupFiles + */ +async function runGradleWithProject(name, platforms, setupFiles = {}) { + const projectPath = await makeProject(name, platforms, setupFiles); + const result = runGradle(projectPath); + const stdout = joinStrings(result.stdout); + if (result.stderr) { + console.log(stdout); + console.error(joinStrings(result.stderr)); + } + return { ...result, stdout }; +} + +exports.removeProject = removeProject; +exports.runGradleWithProject = runGradleWithProject; diff --git a/test/android-test-app/test-app-util.test.js b/test/android-test-app/test-app-util.test.js new file mode 100644 index 00000000..b0fbc929 --- /dev/null +++ b/test/android-test-app/test-app-util.test.js @@ -0,0 +1,236 @@ +// @ts-check +"use strict"; + +describe("test-app-util", () => { + const { removeProject, runGradleWithProject } = require("./gradle"); + + const buildGradle = [ + "buildscript {", + ' def androidTestAppDir = "node_modules/react-native-test-app/android"', + ' apply(from: "${androidTestAppDir}/dependencies.gradle")', + ' apply(from: "${androidTestAppDir}/test-app-util.gradle")', + "", + " repositories {", + " mavenCentral()", + " google()", + " }", + "", + " dependencies {", + ' classpath "com.android.tools.build:gradle:$androidPluginVersion"', + ' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"', + " }", + "}", + "", + ]; + + const defaultTestProject = "TestAppUtilTest"; + + /** + * Runs Gradle in test project. + * @param {Record=} setupFiles + */ + function runGradle(setupFiles) { + return runGradleWithProject(defaultTestProject, ["android"], setupFiles); + } + + /** + * Returns version number. + * @param {string} version + * @returns + */ + function toVersionNumber(version) { + const [major, minor, patch] = version.split("."); + return Number(major) * 10000 + Number(minor) * 100 + Number(patch); + } + + afterAll(() => { + removeProject(defaultTestProject); + }); + + test("getAppName() returns `name` if `displayName` is omitted", async () => { + const { status, stdout } = await runGradle({ + "app.json": JSON.stringify({ + name: "AppName", + resources: ["dist/res", "dist/main.android.jsbundle"], + }), + "build.gradle": [ + ...buildGradle, + 'println("getAppName() = " + ext.getAppName())', + ], + }); + + expect(status).toBe(0); + expect(stdout).toContain("getAppName() = AppName"); + }); + + test("getAppName() returns `displayName` if set", async () => { + const { status, stdout } = await runGradle({ + "app.json": JSON.stringify({ + name: "AppName", + displayName: "AppDisplayName", + resources: ["dist/res", "dist/main.android.jsbundle"], + }), + "build.gradle": [ + ...buildGradle, + 'println("getAppName() = " + ext.getAppName())', + ], + }); + + expect(status).toBe(0); + expect(stdout).toContain("getAppName() = AppDisplayName"); + }); + + test("getApplicationId() returns default id", async () => { + const { status, stdout } = await runGradle({ + "app.json": JSON.stringify({ + name: "AppName", + displayName: "AppDisplayName", + resources: ["dist/res", "dist/main.android.jsbundle"], + }), + "build.gradle": [ + ...buildGradle, + 'println("getApplicationId() = " + ext.getApplicationId())', + ], + }); + + expect(status).toBe(0); + expect(stdout).toContain("getApplicationId() = com.microsoft.reacttestapp"); + }); + + test("getApplicationId() returns package name", async () => { + const { status, stdout } = await runGradle({ + "app.json": JSON.stringify({ + name: "AppName", + displayName: "AppDisplayName", + android: { + package: "com.contoso.application.id", + }, + resources: ["dist/res", "dist/main.android.jsbundle"], + }), + "build.gradle": [ + ...buildGradle, + 'println("getApplicationId() = " + ext.getApplicationId())', + ], + }); + + expect(status).toBe(0); + expect(stdout).toContain("getApplicationId() = com.contoso.application.id"); + }); + + test("getFlipperRecommendedVersion() returns `null` if unsupported", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getFlipperRecommendedVersion() = " + ext.getFlipperRecommendedVersion(file("${rootDir}/no-flipper")))', + ], + "no-flipper/node_modules/react-native/template/android/gradle.properties": + "", + }); + + expect(status).toBe(0); + expect(stdout).toContain("getFlipperRecommendedVersion() = null"); + }); + + test("getFlipperRecommendedVersion() returns version number if supported", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getFlipperRecommendedVersion() = " + ext.getFlipperRecommendedVersion(rootDir))', + ], + }); + + expect(status).toBe(0); + expect(stdout).toMatch(/getFlipperRecommendedVersion\(\) = \d+\.\d+\.\d+/); + }); + + test("getFlipperVersion() returns `null` if unsupported", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getFlipperVersion() = " + ext.getFlipperVersion(file("${rootDir}/no-flipper")))', + ], + "no-flipper/node_modules/react-native/template/android/gradle.properties": + "", + }); + + expect(status).toBe(0); + expect(stdout).toContain("getFlipperVersion() = null"); + }); + + test("getFlipperVersion() returns recommended version if unset", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getFlipperVersion() = " + ext.getFlipperVersion(rootDir))', + ], + }); + + expect(status).toBe(0); + expect(stdout).toMatch(/getFlipperVersion\(\) = \d+\.\d+\.\d+/); + }); + + test("getFlipperVersion() returns user set version", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getFlipperVersion() = " + ext.getFlipperVersion(rootDir))', + ], + "gradle.properties": [ + "android.useAndroidX=true", + "android.enableJetifier=true", + "FLIPPER_VERSION=0.0.0-test", + ], + }); + + expect(status).toBe(0); + expect(stdout).toContain("getFlipperVersion() = 0.0.0-test"); + }); + + test("getFlipperVersion() returns null if disabled", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getFlipperVersion() = " + ext.getFlipperVersion(rootDir))', + ], + "gradle.properties": [ + "android.useAndroidX=true", + "android.enableJetifier=true", + "FLIPPER_VERSION=false", + ], + }); + + expect(status).toBe(0); + expect(stdout).toContain("getFlipperVersion() = null"); + }); + + test("getReactNativeVersionNumber() returns react-native version as a number", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getReactNativeVersionNumber() = " + ext.getReactNativeVersionNumber(rootDir))', + ], + }); + + expect(status).toBe(0); + + const { version } = require("react-native/package.json"); + + expect(stdout).toContain( + `getReactNativeVersionNumber() = ${toVersionNumber(version)}` + ); + }); + + test("getReactNativeVersionNumber() handles pre-release identifiers", async () => { + const { status, stdout } = await runGradle({ + "build.gradle": [ + ...buildGradle, + 'println("getReactNativeVersionNumber() = " + ext.getReactNativeVersionNumber(file("${rootDir}/pre-release-version")))', + ], + "pre-release-version/node_modules/react-native/package.json": + JSON.stringify({ name: "react-native", version: "1.2.3-053c2b4be" }), + }); + + expect(status).toBe(0); + expect(stdout).toContain(`getReactNativeVersionNumber() = 10203`); + }); +});