From f68e50b1b0a72be01883ed65f0ed904e47b0c02c Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Thu, 13 Aug 2026 16:05:39 +0700 Subject: [PATCH] fix(jest): repair the broken `yarn test` setup `yarn test`, the command CONTRIBUTING.md tells contributors to run, has been failing on a fresh clone since 2023: SyntaxError: node_modules/@react-native/js-polyfills/error-guard.js: Missing semicolon. (14:4) Test Suites: 2 failed, 2 total Two separate causes: 1. `5aa0d16` ("chore: Clean up") deleted the root `babel.config.js` and moved the preset into a `babel` key in `package.json`. That key is a *file-relative* Babel config, so it does not apply to files in other packages -- i.e. not to `node_modules/**`. React Native's jest preset needs Babel to strip Flow types out of RN internals, so without a *root* config the transform fails. The preset it named, `metro-react-native-babel-preset`, is also not installed at all: it was deprecated in RN 0.73 in favour of `@react-native/babel-preset`. `tsconfig.json` still lists `babel.config.js` in `include`, which suggests the file was dropped by accident. 2. `modulePathIgnorePatterns` only excluded `example/node_modules`, so the root runner also collected `example/__tests__/tflite.harness.ts` -- a react-native-harness e2e test that belongs to `example/jest.harness.config.mjs`, not to the root runner. Fixes: - restore `babel.config.js` using `@react-native/babel-preset`, the preset that matches the root's `react-native@0.73.3`, and add it as an explicit devDependency (pinned to `0.73.20`, which is already in `yarn.lock` as a transitive dependency, so `--frozen-lockfile` still passes) - drop the now-redundant, broken `babel` key from `package.json` - add `testPathIgnorePatterns` so the root runner leaves `example/` alone Replace the `it.todo('write a test')` placeholder with real coverage of `withAndroidGpuLibraries`, the Expo config plugin's Android manifest logic -- pure TypeScript, no native module required. Finally, add a jest job to `validate-js.yml`. CI only ran tsc and eslint, which is why this went unnoticed for three years. --- .github/workflows/validate-js.yml | 29 ++++ babel.config.js | 3 + package.json | 10 +- src/__tests__/index.test.tsx | 1 - src/__tests__/withAndroidGpuLibraries.test.ts | 135 ++++++++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 babel.config.js delete mode 100644 src/__tests__/index.test.tsx create mode 100644 src/__tests__/withAndroidGpuLibraries.test.ts diff --git a/.github/workflows/validate-js.yml b/.github/workflows/validate-js.yml index 14d08be42..5beffb79d 100644 --- a/.github/workflows/validate-js.yml +++ b/.github/workflows/validate-js.yml @@ -29,6 +29,35 @@ on: - 'example/*.tsx' jobs: + test: + name: Test JS (jest) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Restore node_modules from cache + uses: actions/cache@v4 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install node_modules + run: yarn install --frozen-lockfile + + - name: Run Jest + run: yarn test + compile: name: Compile JS (tsc) runs-on: ubuntu-latest diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..3e0218e68 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +} diff --git a/package.json b/package.json index 8de47e24b..962116ec7 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@expo/config-plugins": "^7.8.4", "@jamesacarr/eslint-formatter-github-actions": "^0.2.0", "@react-native-community/eslint-config": "^3.0.2", + "@react-native/babel-preset": "0.73.20", "@release-it/conventional-changelog": "^8.0.1", "@types/jest": "^29.5.11", "@types/react": "~18.2.48", @@ -99,6 +100,10 @@ "modulePathIgnorePatterns": [ "/example/node_modules", "/lib/" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/example/" ] }, "release-it": { @@ -126,11 +131,6 @@ "trailingComma": "es5", "useTabs": false }, - "babel": { - "presets": [ - "module:metro-react-native-babel-preset" - ] - }, "react-native-builder-bob": { "source": "src", "output": "lib", diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx deleted file mode 100644 index d41123fcb..000000000 --- a/src/__tests__/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -it.todo('write a test') diff --git a/src/__tests__/withAndroidGpuLibraries.test.ts b/src/__tests__/withAndroidGpuLibraries.test.ts new file mode 100644 index 000000000..8d22bbcf8 --- /dev/null +++ b/src/__tests__/withAndroidGpuLibraries.test.ts @@ -0,0 +1,135 @@ +import type { + AndroidConfig, + ExportedConfig, + ModProps, +} from '@expo/config-plugins' +import { withAndroidGpuLibraries } from '../expo-plugin/withAndroidGpuLibraries' + +type AndroidManifest = AndroidConfig.Manifest.AndroidManifest +type ManifestUsesLibrary = AndroidConfig.Manifest.ManifestUsesLibrary +type ManifestApplicationWithNativeLibraries = + AndroidConfig.Manifest.ManifestApplication & { + 'uses-native-library'?: ManifestUsesLibrary[] + } + +function createExpoConfig(): ExportedConfig { + return { name: 'TfliteExample', slug: 'tflite-example' } +} + +function createManifest( + usesNativeLibrary?: ManifestUsesLibrary[] +): AndroidManifest { + const application: ManifestApplicationWithNativeLibraries = { + $: { 'android:name': '.MainApplication' }, + } + if (usesNativeLibrary != null) + application['uses-native-library'] = usesNativeLibrary + + return { + manifest: { + $: { 'xmlns:android': 'http://schemas.android.com/apk/res/android' }, + queries: [], + application: [application], + }, + } +} + +/** + * Runs the `withAndroidGpuLibraries` plugin's `android.manifest` mod against the + * given manifest, the same way `expo prebuild` would, and returns the result. + */ +async function applyPlugin( + manifest: AndroidManifest, + enabledLibraries: boolean | string[] +): Promise { + const expoConfig = createExpoConfig() + const config = withAndroidGpuLibraries( + expoConfig, + enabledLibraries + ) as ExportedConfig + const mod = config.mods?.android?.manifest + if (mod == null) + throw new Error('withAndroidGpuLibraries did not register a manifest mod!') + + const modRequest: ModProps = { + projectRoot: '/app', + platformProjectRoot: '/app/android', + modName: 'manifest', + platform: 'android', + introspect: false, + } + const result = await mod({ + ...config, + modResults: manifest, + modRequest: modRequest, + modRawConfig: expoConfig, + }) + + return result.modResults +} + +function getUsesNativeLibraries( + manifest: AndroidManifest +): ManifestUsesLibrary[] { + const application: ManifestApplicationWithNativeLibraries | undefined = + manifest.manifest.application?.[0] + if (application == null) throw new Error('No in the manifest!') + return application['uses-native-library'] ?? [] +} + +function getUsesNativeLibraryNames(manifest: AndroidManifest): string[] { + return getUsesNativeLibraries(manifest).map((lib) => lib.$['android:name']) +} + +describe('withAndroidGpuLibraries', () => { + it('adds libOpenCL.so when enabled with `true`', async () => { + const manifest = await applyPlugin(createManifest(), true) + + expect(getUsesNativeLibraryNames(manifest)).toEqual(['libOpenCL.so']) + }) + + it('marks the added libraries as not required', async () => { + const manifest = await applyPlugin(createManifest(), true) + + expect(getUsesNativeLibraries(manifest)[0]?.$).toEqual({ + 'android:name': 'libOpenCL.so', + 'android:required': false, + }) + }) + + it('adds libOpenCL.so alongside the explicitly listed libraries', async () => { + const manifest = await applyPlugin(createManifest(), [ + 'libOpenCL-pixel.so', + 'libGLES_mali.so', + ]) + + expect(getUsesNativeLibraryNames(manifest)).toEqual([ + 'libOpenCL.so', + 'libOpenCL-pixel.so', + 'libGLES_mali.so', + ]) + }) + + it('does not duplicate entries when prebuild runs twice', async () => { + const libraries = ['libOpenCL-pixel.so'] + const once = await applyPlugin(createManifest(), libraries) + const twice = await applyPlugin(once, libraries) + + expect(getUsesNativeLibraryNames(twice)).toEqual([ + 'libOpenCL.so', + 'libOpenCL-pixel.so', + ]) + }) + + it('keeps unrelated entries that are already present', async () => { + const existing: ManifestUsesLibrary = { + $: { 'android:name': 'libsomething-else.so', 'android:required': 'true' }, + } + const manifest = await applyPlugin(createManifest([existing]), true) + + expect(getUsesNativeLibraryNames(manifest)).toEqual([ + 'libsomething-else.so', + 'libOpenCL.so', + ]) + }) +})