-
Notifications
You must be signed in to change notification settings - Fork 31
feat: Implement jest mocks for react-native. #535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 24 commits
75725a5
9e0ea68
799200d
7833f46
db3697f
4f04b46
d248241
4944307
f4ee714
e8b3649
a877514
2531181
8f9cec6
b2657ff
75583b3
efbf89c
60cd0a1
a5341e6
ad47e51
fbc8178
7750714
3e3252a
f737935
26692ce
af3fcc4
f4b52b9
99c789f
d95b070
d4bbdeb
c578a19
1d92d09
0728fdf
4dc3bf2
3ef8baa
b32dedf
d36a2ad
1f2163e
6e3caf7
3e146e8
78fd823
3d45157
3f5b48c
61aefc7
a4fad23
23e7dac
beef6b0
4fdbdbd
e9493ed
d2cb48e
0760dab
7bd782f
210f710
2fc00e4
32d4a22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files | ||
|
|
||
| # dependencies | ||
| node_modules/ | ||
|
|
||
| # Expo | ||
| .expo/ | ||
| dist/ | ||
| web-build/ | ||
|
|
||
| # Native | ||
| *.orig.* | ||
| *.jks | ||
| *.p8 | ||
| *.p12 | ||
| *.key | ||
| *.mobileprovision | ||
|
|
||
| # Metro | ||
| .metro-health-check* | ||
|
|
||
| # debug | ||
| npm-debug.* | ||
| yarn-debug.* | ||
| yarn-error.* | ||
|
|
||
| # macOS | ||
| .DS_Store | ||
| *.pem | ||
|
|
||
| # local env files | ||
| .env*.local | ||
|
|
||
| # typescript | ||
| *.tsbuildinfo | ||
|
|
||
| # vscode | ||
| .vscode |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { StyleSheet } from 'react-native'; | ||
|
|
||
| import { mockReactNativeLDClient } from '@launchdarkly/jest/react-native'; | ||
| import { LDProvider } from '@launchdarkly/react-native-client-sdk'; | ||
|
|
||
| import Welcome from './src/welcome'; | ||
|
|
||
| const featureClient = mockReactNativeLDClient(); | ||
|
|
||
| const userContext = { kind: 'user', key: '', anonymous: true }; | ||
|
|
||
| export default function App() { | ||
| featureClient.identify(userContext).catch((e: any) => console.log(e)); | ||
|
|
||
| return ( | ||
| <LDProvider client={featureClient}> | ||
| <Welcome /> | ||
| </LDProvider> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| backgroundColor: '#fff', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "expo": { | ||
| "name": "react-native-jest-example", | ||
| "slug": "react-native-jest-example", | ||
| "version": "0.0.1", | ||
| "orientation": "portrait", | ||
| "icon": "./assets/icon.png", | ||
| "userInterfaceStyle": "light", | ||
| "splash": { | ||
| "image": "./assets/splash.png", | ||
| "resizeMode": "contain", | ||
| "backgroundColor": "#ffffff" | ||
| }, | ||
| "ios": { | ||
| "supportsTablet": true, | ||
| "bundleIdentifier": "com.anonymous.reactnativejestexample" | ||
| }, | ||
| "android": { | ||
| "adaptiveIcon": { | ||
| "foregroundImage": "./assets/adaptive-icon.png", | ||
| "backgroundColor": "#ffffff" | ||
| }, | ||
| "package": "com.anonymous.reactnativejestexample" | ||
| }, | ||
| "web": { | ||
| "favicon": "./assets/favicon.png" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| module.exports = function (api) { | ||
| api.cache(true); | ||
| return { | ||
| presets: ['babel-preset-expo'], | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // We have to use a custom entrypoint for monorepo workspaces to work. | ||
| // https://docs.expo.dev/guides/monorepos/#change-default-entrypoint | ||
| import { registerRootComponent } from 'expo'; | ||
|
|
||
| import App from './App'; | ||
|
|
||
| // registerRootComponent calls AppRegistry.registerComponent('main', () => App); | ||
| // It also ensures that whether you load the app in Expo Go or in a native build, | ||
| // the environment is set up appropriately | ||
| registerRootComponent(App); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| module.exports = { | ||
| setupFiles: ['@launchdarkly/jest/react-native'], | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // We need to use a custom metro config for monorepo workspaces to work. | ||
| // https://docs.expo.dev/guides/monorepos/#modify-the-metro-config | ||
| /** | ||
| * @type {import('expo/metro-config')} | ||
| */ | ||
| const { getDefaultConfig } = require('expo/metro-config'); | ||
| const path = require('path'); | ||
|
|
||
| // Find the project and workspace directories | ||
| const projectRoot = __dirname; | ||
|
|
||
| const findWorkspaceRoot = require('find-yarn-workspace-root'); | ||
|
|
||
| const workspaceRoot = findWorkspaceRoot(__dirname); // Absolute path or null | ||
|
|
||
| const config = getDefaultConfig(projectRoot); | ||
|
|
||
| // 1. Watch all files within the monorepo | ||
| config.watchFolders = [workspaceRoot]; | ||
| // 2. Let Metro know where to resolve packages and in what order | ||
| config.resolver.nodeModulesPaths = [ | ||
| path.resolve(projectRoot, 'node_modules'), | ||
| path.resolve(workspaceRoot, 'node_modules'), | ||
| ]; | ||
| // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` | ||
| config.resolver.disableHierarchicalLookup = true; | ||
|
|
||
| module.exports = config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "name": "react-native-jest-example", | ||
| "version": "0.0.1", | ||
| "main": "index.js", | ||
| "scripts": { | ||
| "start": "expo start", | ||
| "android": "expo run:android", | ||
| "ios": "expo run:ios", | ||
| "web": "expo start --web" | ||
| }, | ||
| "dependencies": { | ||
| "@launchdarkly/react-native-client-sdk": "workspace:^", | ||
| "expo": "~51.0.20", | ||
| "expo-status-bar": "~1.12.1", | ||
| "find-yarn-workspace-root": "^2.0.0", | ||
| "react": "18.2.0", | ||
| "react-native": "0.74.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@babel/core": "^7.20.0", | ||
| "@launchdarkly/jest": "workspace:^", | ||
| "@types/react": "~18.2.45", | ||
| "typescript": "^5.1.3" | ||
| }, | ||
| "private": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { StyleSheet, Text, View } from 'react-native'; | ||
|
|
||
| import { mockFlags, mockUseLDClient } from '@launchdarkly/jest/react-native'; | ||
|
|
||
| export default function Welcome() { | ||
| const flagValue = mockFlags({ 'dev-test-flag': true }); | ||
|
||
|
|
||
| const ldClient = mockUseLDClient(); | ||
|
|
||
| ldClient.track('test'); | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <Text>Welcome to LaunchDarkly</Text> | ||
| <Text>Flag value is {`${flagValue}`}</Text> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| backgroundColor: '#fff', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "extends": "expo/tsconfig.base", | ||
| "compilerOptions": { | ||
| "strict": true, | ||
| "moduleResolution": "bundler", | ||
| "jsx": "react-jsx" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,5 +59,9 @@ | |
| "ts-jest": "^29.1.0", | ||
| "typedoc": "0.25.0", | ||
| "typescript": "5.1.6" | ||
| }, | ||
| "dependencies": { | ||
| "@launchdarkly/react-native-client-sdk": "workspace:^", | ||
|
||
| "@testing-library/react-hooks": "^8.0.1" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,58 @@ | ||
| import { renderHook } from '@testing-library/react-hooks'; | ||
|
|
||
| import { | ||
| ldClientMock, | ||
| mockFlags, | ||
| mockLDProvider, | ||
| mockReactNativeLDClient, | ||
| mockUseLDClient, | ||
| } from '.'; | ||
|
|
||
| describe('react-native', () => { | ||
| test.todo('Add react-native tests'); | ||
| test('mock boolean flag correctly', () => { | ||
| mockFlags({ 'bool-flag': true }); | ||
| }); | ||
|
|
||
| test('mock number flag correctly', () => { | ||
| mockFlags({ 'number-flag': 42 }); | ||
| }); | ||
|
|
||
| test('mock string flag correctly', () => { | ||
| mockFlags({ 'string-flag': 'hello' }); | ||
| }); | ||
|
|
||
| test('mock json flag correctly', () => { | ||
| mockFlags({ 'json-flag': { key: 'value' } }); | ||
| }); | ||
|
|
||
| test('mock LDProvider correctly', () => { | ||
| expect(mockLDProvider).toBeDefined(); | ||
| }); | ||
|
|
||
| test('mock ReactNativeLDClient correctly', () => { | ||
| expect(mockReactNativeLDClient).toBeDefined(); | ||
| }); | ||
|
|
||
| test('mock ldClient correctly', () => { | ||
| const { | ||
| result: { current }, | ||
| } = renderHook(() => mockUseLDClient()); | ||
|
|
||
| current?.track('event'); | ||
| expect(ldClientMock.track).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| test('mock ldClient complete set of methods correctly', () => { | ||
| expect(ldClientMock.identify).toBeDefined(); | ||
| expect(ldClientMock.allFlags.mock).toBeDefined(); | ||
| expect(ldClientMock.close.mock).toBeDefined(); | ||
| expect(ldClientMock.flush).toBeDefined(); | ||
| expect(ldClientMock.getContext.mock).toBeDefined(); | ||
| expect(ldClientMock.off.mock).toBeDefined(); | ||
| expect(ldClientMock.on.mock).toBeDefined(); | ||
| expect(ldClientMock.setConnectionMode.mock).toBeDefined(); | ||
| expect(ldClientMock.track.mock).toBeDefined(); | ||
| expect(ldClientMock.variation.mock).toBeDefined(); | ||
| expect(ldClientMock.variationDetail.mock).toBeDefined(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,73 @@ | ||
| jest.mock('@launchdarkly/react-client-sdk', () => { | ||
| // TODO: | ||
| }); | ||
| import { | ||
| LDFlagSet, | ||
| LDProvider, | ||
| ReactNativeLDClient, | ||
| useLDClient, | ||
| } from '@launchdarkly/react-native-client-sdk'; | ||
|
|
||
| jest.mock('@launchdarkly/react-native-client-sdk', () => ({ | ||
| LDProvider: jest.fn(), | ||
| ReactNativeLDClient: jest.fn(), | ||
| useLDClient: jest.fn(), | ||
| })); | ||
|
|
||
| const mockLDProvider = LDProvider as jest.Mock; | ||
| const mockReactNativeLDClient = ReactNativeLDClient as jest.Mock; | ||
| const mockUseLDClient = useLDClient as jest.Mock; | ||
|
|
||
| export const ldClientMock = { | ||
| allFlags: jest.fn(), | ||
| boolVariation: jest.fn(), | ||
| boolVariationDetail: jest.fn(), | ||
| close: jest.fn(), | ||
| flush: jest.fn(() => Promise.resolve()), | ||
| getConnectionMode: jest.fn(), | ||
| getContext: jest.fn(), | ||
| identify: jest.fn(() => Promise.resolve()), | ||
| jsonVariation: jest.fn(), | ||
| jsonVariationDetail: jest.fn(), | ||
| logger: jest.fn(), | ||
| numberVariation: jest.fn(), | ||
| numberVariationDetail: jest.fn(), | ||
| off: jest.fn(), | ||
| on: jest.fn(), | ||
| setConnectionMode: jest.fn(), | ||
| stringVariation: jest.fn(), | ||
| stringVariationDetail: jest.fn(), | ||
| track: jest.fn(), | ||
| variation: jest.fn(), | ||
| variationDetail: jest.fn(), | ||
| }; | ||
|
|
||
| mockLDProvider.mockImplementation((props: any) => props.children); | ||
| mockUseLDClient.mockImplementation(() => ldClientMock); | ||
| mockReactNativeLDClient.mockImplementation(() => ldClientMock); | ||
|
|
||
| export { mockLDProvider, mockReactNativeLDClient, mockUseLDClient }; | ||
|
|
||
| export const mockFlags = (flags: LDFlagSet) => { | ||
| ldClientMock.boolVariation.mockImplementation((flagKey: string) => { | ||
| if (typeof flags[flagKey] !== 'boolean') { | ||
| throw new Error(`Flag ${flagKey} is not a boolean. Flag value, ${flags[flagKey]}`); | ||
| } | ||
| return flags[flagKey] as boolean; | ||
| }); | ||
| ldClientMock.numberVariation.mockImplementation((flagKey: string) => { | ||
| if (typeof flags[flagKey] !== 'number') { | ||
| throw new Error(`Flag ${flagKey} is not a number. Flag value, ${flags[flagKey]}`); | ||
| } | ||
| return flags[flagKey] as number; | ||
| }); | ||
| ldClientMock.stringVariation.mockImplementation((flagKey: string) => { | ||
| if (typeof flags[flagKey] !== 'string') { | ||
| throw new Error(`Flag ${flagKey} is not a string. Flag value, ${flags[flagKey]}`); | ||
| } | ||
| return flags[flagKey] as string; | ||
| }); | ||
| ldClientMock.jsonVariation.mockImplementation((flagKey: string) => { | ||
| if (typeof flags[flagKey] !== 'object') { | ||
| throw new Error(`Flag ${flagKey} is not a JSON. Flag value, ${flags[flagKey]}`); | ||
| } | ||
| return flags[flagKey] as object; | ||
| }); | ||
| }; |
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.