diff --git a/packages/sdk/react-native/example/app.json b/packages/sdk/react-native/example/app.json index b797a3a7ec..681b269d3b 100644 --- a/packages/sdk/react-native/example/app.json +++ b/packages/sdk/react-native/example/app.json @@ -11,9 +11,7 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "com.anonymous.reactnativeexample" diff --git a/packages/sdk/react-native/jest.config.json b/packages/sdk/react-native/jest.config.json deleted file mode 100644 index 6174807746..0000000000 --- a/packages/sdk/react-native/jest.config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "transform": { "^.+\\.ts?$": "ts-jest" }, - "testMatch": ["**/*.test.ts?(x)"], - "testPathIgnorePatterns": ["node_modules", "example", "dist"], - "modulePathIgnorePatterns": ["dist"], - "testEnvironment": "node", - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], - "collectCoverageFrom": ["src/**/*.ts"] -} diff --git a/packages/sdk/react-native/jest.config.ts b/packages/sdk/react-native/jest.config.ts new file mode 100644 index 0000000000..a740765ad3 --- /dev/null +++ b/packages/sdk/react-native/jest.config.ts @@ -0,0 +1,17 @@ +import type { JestConfigWithTsJest } from 'ts-jest'; + +const jestConfig: JestConfigWithTsJest = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.test.json', + }, + ], + }, + testPathIgnorePatterns: ['node_modules', 'example', 'dist'], +}; + +export default jestConfig; diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index 34cd5b6d7d..1b536f35e0 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -32,7 +32,7 @@ "start": "rimraf dist && yarn tsw", "lint": "eslint . --ext .ts", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", - "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --ci --runInBand", + "test": "jest", "coverage": "yarn test --coverage", "check": "yarn prettier && yarn lint && yarn build && yarn test", "link-dev": "./link-dev.sh", @@ -50,8 +50,9 @@ "event-target-shim": "^6.0.2" }, "devDependencies": { + "@testing-library/react": "^14.1.2", "@trivago/prettier-plugin-sort-imports": "^4.1.1", - "@types/jest": "^29.5.0", + "@types/jest": "^29.5.11", "@types/react": "^18.2.31", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", @@ -61,13 +62,15 @@ "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.5.0", + "jest": "^29.7.0", "launchdarkly-js-test-helpers": "^2.2.0", "prettier": "^3.0.0", "react": "^18.2.0", + "react-dom": "^18.2.0", "react-native": "^0.73.1", "rimraf": "^5.0.5", - "ts-jest": "^29.1.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", "typedoc": "0.25.0", "typescript": "5.1.6" }, diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts index 1bbcb4f0ae..f7c9f717f0 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts @@ -6,11 +6,11 @@ describe('ReactNativeLDClient', () => { let ldc: ReactNativeLDClient; beforeEach(() => { - ldc = new ReactNativeLDClient('mob-test', { sendEvents: false }); + ldc = new ReactNativeLDClient('mobile-key', { sendEvents: false }); }); test('constructing a new client', () => { - expect(ldc.sdkKey).toEqual('mob-test'); + expect(ldc.sdkKey).toEqual('mobile-key'); expect(ldc.config.serviceEndpoints).toEqual({ analyticsEventPath: '/mobile', diagnosticEventPath: '/mobile/events/diagnostic', diff --git a/packages/sdk/react-native/src/provider/LDProvider.test.tsx b/packages/sdk/react-native/src/provider/LDProvider.test.tsx new file mode 100644 index 0000000000..146e4673c7 --- /dev/null +++ b/packages/sdk/react-native/src/provider/LDProvider.test.tsx @@ -0,0 +1,97 @@ +import { render } from '@testing-library/react'; + +import type { LDContext, LDOptions } from '@launchdarkly/js-client-sdk-common'; + +import { useLDClient } from '../hooks'; +import ReactNativeLDClient from '../ReactNativeLDClient'; +import LDProvider from './LDProvider'; +import setupListeners from './setupListeners'; + +jest.mock('./setupListeners'); +jest.mock('../ReactNativeLDClient'); + +const TestApp = () => { + const ldClient = useLDClient(); + return ( + <> +

ldClient {ldClient ? 'defined' : 'undefined'}

+

mobileKey {ldClient.sdkKey ? ldClient.sdkKey : 'undefined'}

+

context {ldClient.getContext() ? 'defined' : 'undefined'}

+ + ); +}; +describe('LDProvider', () => { + let ldc: ReactNativeLDClient; + let context: LDContext; + let mockSetupListeners = setupListeners as jest.Mock; + + beforeEach(() => { + jest.useFakeTimers(); + (ReactNativeLDClient as jest.Mock).mockImplementation( + (mobileKey: string, _options?: LDOptions) => { + let context: LDContext; + + return { + sdkKey: mobileKey, + identify: jest.fn((c: LDContext) => { + context = c; + return Promise.resolve(); + }), + getContext: jest.fn(() => context), + on: jest.fn(), + logger: { + debug: jest.fn(), + }, + }; + }, + ); + mockSetupListeners.mockImplementation((client: ReactNativeLDClient, setState: any) => { + setState({ client }); + }); + ldc = new ReactNativeLDClient('mobile-key'); + context = { kind: 'user', key: 'test-user-key-1' }; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('client is correctly set', () => { + const { getByText } = render( + + + , + ); + + expect(getByText(/ldclient defined/i)).toBeTruthy(); + expect(getByText(/mobilekey mobile-key/i)).toBeTruthy(); + expect(getByText(/context undefined/i)).toBeTruthy(); + }); + + test('specified context is identified', async () => { + const { getByText } = render( + + + , + ); + + expect(mockSetupListeners).toHaveBeenCalledWith(ldc, expect.any(Function)); + expect(ldc.identify).toHaveBeenCalledWith(context); + expect(ldc.getContext()).toEqual(context); + expect(getByText(/context defined/i)).toBeTruthy(); + }); + + test('identify errors are caught', async () => { + (ldc.identify as jest.Mock).mockImplementation(() => { + return Promise.reject('faking error when identifying'); + }); + const { getByText } = render( + + + , + ); + await jest.runAllTimersAsync(); + + expect(ldc.logger.debug).toHaveBeenCalledWith(expect.stringMatching(/identify error/)); + }); +}); diff --git a/packages/sdk/react-native/src/provider/LDProvider.tsx b/packages/sdk/react-native/src/provider/LDProvider.tsx index ec2db27988..18396bbec6 100644 --- a/packages/sdk/react-native/src/provider/LDProvider.tsx +++ b/packages/sdk/react-native/src/provider/LDProvider.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, useEffect, useState } from 'react'; +import React, { PropsWithChildren, useEffect, useState } from 'react'; import { type LDContext } from '@launchdarkly/js-client-sdk-common'; diff --git a/packages/sdk/react-native/src/provider/reactContext.ts b/packages/sdk/react-native/src/provider/reactContext.ts index 2ada7da80d..e1b4526fb8 100644 --- a/packages/sdk/react-native/src/provider/reactContext.ts +++ b/packages/sdk/react-native/src/provider/reactContext.ts @@ -1,9 +1,9 @@ import { createContext } from 'react'; -import { LDClient } from '@launchdarkly/js-client-sdk-common'; +import type ReactNativeLDClient from '../ReactNativeLDClient'; export type ReactContext = { - client: LDClient; + client: ReactNativeLDClient; }; export const context = createContext({ diff --git a/packages/sdk/react-native/src/provider/setupListeners.test.ts b/packages/sdk/react-native/src/provider/setupListeners.test.ts new file mode 100644 index 0000000000..763195e00c --- /dev/null +++ b/packages/sdk/react-native/src/provider/setupListeners.test.ts @@ -0,0 +1,32 @@ +import ReactNativeLDClient from '../ReactNativeLDClient'; +import setupListeners from './setupListeners'; + +import resetAllMocks = jest.resetAllMocks; + +jest.mock('../ReactNativeLDClient'); + +describe('setupListeners', () => { + let ldc: ReactNativeLDClient; + let mockSetState: jest.Mock; + + beforeEach(() => { + mockSetState = jest.fn(); + ldc = new ReactNativeLDClient('mob-test-key'); + }); + + afterEach(() => resetAllMocks()); + + test('change listener is setup', () => { + setupListeners(ldc, mockSetState); + expect(ldc.on).toHaveBeenCalledWith('change', expect.any(Function)); + }); + + test('client is set on change event', () => { + setupListeners(ldc, mockSetState); + + const changeHandler = (ldc.on as jest.Mock).mock.calls[0][1]; + changeHandler(); + + expect(mockSetState).toHaveBeenCalledWith({ client: ldc }); + }); +}); diff --git a/packages/sdk/react-native/tsconfig.json b/packages/sdk/react-native/tsconfig.json index 234db0cdb0..e5a9204bee 100644 --- a/packages/sdk/react-native/tsconfig.json +++ b/packages/sdk/react-native/tsconfig.json @@ -3,11 +3,8 @@ "allowSyntheticDefaultImports": true, "declaration": true, "declarationMap": true, - "jsx": "react-native", - "lib": [ - "es6", - "dom" - ], + "jsx": "react-jsx", + "lib": ["es6", "dom"], "module": "ES6", "moduleResolution": "node", "noImplicitOverride": true, @@ -21,7 +18,7 @@ "strict": true, "stripInternal": true, "target": "ES2017", - "types": ["jest", "node"] + "types": ["node"] }, - "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"] + "exclude": ["**/*.test.ts*", "dist", "node_modules", "__tests__", "example"] } diff --git a/packages/sdk/react-native/tsconfig.test.json b/packages/sdk/react-native/tsconfig.test.json new file mode 100644 index 0000000000..2c617dcaa7 --- /dev/null +++ b/packages/sdk/react-native/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react-jsx", + "lib": ["es6", "dom"], + "module": "ES6", + "moduleResolution": "node", + "resolveJsonModule": true, + "rootDir": ".", + "strict": true, + "types": ["jest", "node"] + }, + "exclude": ["dist", "node_modules", "__tests__", "example"] +} diff --git a/release-please-config.json b/release-please-config.json index d6d4d9560f..9aa9c36498 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,16 +1,12 @@ { "packages": { "packages/shared/common": {}, - "packages/shared/sdk-client": { - "bump-minor-pre-major": true - }, + "packages/shared/sdk-client": {}, "packages/shared/sdk-server": {}, "packages/shared/sdk-server-edge": {}, "packages/shared/akamai-edgeworker-sdk": {}, "packages/sdk/cloudflare": {}, - "packages/sdk/react-native": { - "bump-minor-pre-major": true - }, + "packages/sdk/react-native": {}, "packages/sdk/server-node": {}, "packages/sdk/vercel": { "extra-files": ["src/createPlatformInfo.ts"]