From b77503b0f7ebfe111293210a5f273e9d3007019b Mon Sep 17 00:00:00 2001 From: Nicholas Mitchell Date: Sat, 22 Feb 2020 22:13:08 -0600 Subject: [PATCH 1/4] Add tests for the LDProvider --- src/LDProvider.test.tsx | 223 ++++++++++++++++++++++++++++++++++++ src/withLDProvider.test.tsx | 115 ++----------------- 2 files changed, 231 insertions(+), 107 deletions(-) create mode 100644 src/LDProvider.test.tsx diff --git a/src/LDProvider.test.tsx b/src/LDProvider.test.tsx new file mode 100644 index 0000000..e2c03bf --- /dev/null +++ b/src/LDProvider.test.tsx @@ -0,0 +1,223 @@ +jest.mock('./initLDClient', () => jest.fn()); +jest.mock('./context', () => ({ Provider: 'Provider' })); + +import * as React from 'react'; +import { create } from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import { LDFlagChangeset, LDFlagSet, LDOptions, LDUser } from 'launchdarkly-js-client-sdk'; +import initLDClient from './initLDClient'; +import { LDReactOptions, EnhancedComponent, defaultReactOptions, ProviderConfig } from './types'; +import { LDContext as HocState } from './context'; +import LDProvider from './LDProvider'; + +const clientSideID = 'deadbeef'; +const LaunchDarklyApp = (props: ProviderConfig) => ( + +
My App
+
+); +const mockInitLDClient = initLDClient as jest.Mock; +const mockFlags = { testFlag: true, anotherTestFlag: true }; +const mockLDClient = { + on: jest.fn((e: string, cb: () => void) => { + cb(); + }), +}; + +describe('LDProvider', () => { + beforeEach(() => { + mockInitLDClient.mockImplementation(() => ({ + flags: mockFlags, + ldClient: mockLDClient, + })); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('render app', () => { + const props = { clientSideID }; + const component = create(); + expect(component).toMatchSnapshot(); + }); + + test.only('ld client is initialised correctly', async () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { bootstrap: {} }; + const props = { clientSideID, user, options }; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + + await instance.componentDidMount(); + expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, undefined); + }); + + test('ldClient bootstraps with empty flags', () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { + bootstrap: {}, + }; + const props = { clientSideID, suser, options }; + const component = shallow(, { disableLifecycleMethods: true }); + const initialState = component.state() as HocState; + + expect(initialState.flags).toEqual({}); + }); + + test('ld client is bootstrapped correctly and transforms keys to camel case', () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { + bootstrap: { + 'test-flag': true, + 'another-test-flag': false, + $flagsState: { + 'test-flag': { version: 125, variation: 0, trackEvents: true }, + 'another-test-flag': { version: 18, variation: 1 }, + }, + $valid: true, + }, + }; + const props = { clientSideID, user, options }; + const component = shallow(, { disableLifecycleMethods: true }); + const initialState = component.state() as HocState; + + expect(mockInitLDClient).not.toHaveBeenCalled(); + expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); + }); + + test('ld client should not transform keys to camel case if option is disabled', () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { + bootstrap: { + 'test-flag': true, + 'another-test-flag': false, + }, + }; + const reactOptions: LDReactOptions = { + useCamelCaseFlagKeys: false, + }; + const props = { clientSideID, user, options, reactOptions }; + const component = shallow(, { disableLifecycleMethods: true }); + const initialState = component.state() as HocState; + + expect(mockInitLDClient).not.toHaveBeenCalled(); + expect(initialState.flags).toEqual({ 'test-flag': true, 'another-test-flag': false }); + }); + + test('ld client should transform keys to camel case if transform option is absent', () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { + bootstrap: { + 'test-flag': true, + 'another-test-flag': false, + }, + }; + const reactOptions: LDReactOptions = {}; + const props = { clientSideID, user, options, reactOptions }; + const component = shallow(, { disableLifecycleMethods: true }); + const initialState = component.state() as HocState; + + expect(mockInitLDClient).not.toHaveBeenCalled(); + expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); + }); + + test('ld client should transform keys to camel case if react options object is absent', () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { + bootstrap: { + 'test-flag': true, + 'another-test-flag': false, + }, + }; + const props = { clientSideID, user, options }; + const component = shallow(, { disableLifecycleMethods: true }); + const initialState = component.state() as HocState; + + expect(mockInitLDClient).not.toHaveBeenCalled(); + expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); + }); + + test('state.flags should be initialised to empty when bootstrapping from localStorage', () => { + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { + bootstrap: 'localStorage', + }; + const props = { clientSideID, user, options }; + const component = shallow(, { disableLifecycleMethods: true }); + const initialState = component.state() as HocState; + + expect(mockInitLDClient).not.toHaveBeenCalled(); + expect(initialState.flags).toEqual({}); + }); + + test('ld client is initialised correctly with target flags', async () => { + mockInitLDClient.mockImplementation(() => ({ + flags: { devTestFlag: true, launchDoggly: true }, + ldClient: mockLDClient, + })); + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const options: LDOptions = { bootstrap: {} }; + const flags = { 'dev-test-flag': false, 'launch-doggly': false }; + const props = { clientSideID, user, options, flags }; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + instance.setState = jest.fn(); + + await instance.componentDidMount(); + + expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, flags); + expect(instance.setState).toHaveBeenCalledWith({ + flags: { devTestFlag: true, launchDoggly: true }, + ldClient: mockLDClient, + }); + }); + + test('flags and ldClient are saved in state on mount', async () => { + const props = { clientSideID }; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + instance.setState = jest.fn(); + + await instance.componentDidMount(); + expect(instance.setState).toHaveBeenCalledWith({ flags: mockFlags, ldClient: mockLDClient }); + }); + + test('subscribeToChanges is called on mount', async () => { + const props = { clientSideID }; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + instance.subscribeToChanges = jest.fn(); + + await instance.componentDidMount(); + expect(instance.subscribeToChanges).toHaveBeenCalled(); + }); + + test('subscribe to changes with camelCase', async () => { + mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => { + cb({ 'test-flag': { current: false, previous: true } }); + }); + const props = { clientSideID }; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const mockSetState = jest.spyOn(instance, 'setState'); + + await instance.componentDidMount(); + const callback = mockSetState.mock.calls[1][0] as (flags: LDFlagSet) => LDFlagSet; + const newState = callback({ flags: mockFlags }); + + expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function)); + expect(newState).toEqual({ flags: { anotherTestFlag: true, testFlag: false } }); + }); + + test('subscribe to changes with kebab-case', async () => { + mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => { + cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } }); + }); + const props = { clientSideID, reactOptions: { useCamelCaseFlagKeys: false } }; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const mockSetState = jest.spyOn(instance, 'setState'); + + await instance.componentDidMount(); + const callback = mockSetState.mock.calls[1][0] as (flags: LDFlagSet) => LDFlagSet; + const newState = callback({}); + + expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function)); + expect(newState).toEqual({ flags: { 'another-test-flag': false, 'test-flag': false } }); + }); +}); diff --git a/src/withLDProvider.test.tsx b/src/withLDProvider.test.tsx index c15869c..01246a3 100644 --- a/src/withLDProvider.test.tsx +++ b/src/withLDProvider.test.tsx @@ -3,12 +3,11 @@ jest.mock('./context', () => ({ Provider: 'Provider' })); import * as React from 'react'; import { create } from 'react-test-renderer'; -import { shallow } from 'enzyme'; import { LDFlagChangeset, LDFlagSet, LDOptions, LDUser } from 'launchdarkly-js-client-sdk'; import initLDClient from './initLDClient'; import withLDProvider from './withLDProvider'; -import { LDReactOptions, EnhancedComponent, defaultReactOptions } from './types'; -import { LDContext as HocState } from './context'; +import { EnhancedComponent, defaultReactOptions } from './types'; +import LDProvider from './LDProvider'; const clientSideID = 'deadbeef'; const App = () =>
My App
; @@ -42,110 +41,12 @@ describe('withLDProvider', () => { const user: LDUser = { key: 'yus', name: 'yus ng' }; const options: LDOptions = { bootstrap: {} }; const LaunchDarklyApp = withLDProvider({ clientSideID, user, options })(App); - const instance = create().root.instance as EnhancedComponent; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; await instance.componentDidMount(); expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, undefined); }); - test.only('ldClient bootstraps with empty flags', () => { - const user: LDUser = { key: 'yus', name: 'yus ng' }; - const options: LDOptions = { - bootstrap: {}, - }; - const LaunchDarklyApp = withLDProvider({ clientSideID, user, options })(App); - const component = shallow(, { disableLifecycleMethods: true }); - const initialState = component.state() as HocState; - - expect(initialState.flags).toEqual({}); - }); - - test('ld client is bootstrapped correctly and transforms keys to camel case', () => { - const user: LDUser = { key: 'yus', name: 'yus ng' }; - const options: LDOptions = { - bootstrap: { - 'test-flag': true, - 'another-test-flag': false, - $flagsState: { - 'test-flag': { version: 125, variation: 0, trackEvents: true }, - 'another-test-flag': { version: 18, variation: 1 }, - }, - $valid: true, - }, - }; - const LaunchDarklyApp = withLDProvider({ clientSideID, user, options })(App); - const component = shallow(, { disableLifecycleMethods: true }); - const initialState = component.state() as HocState; - - expect(mockInitLDClient).not.toHaveBeenCalled(); - expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); - }); - - test('ld client should not transform keys to camel case if option is disabled', () => { - const user: LDUser = { key: 'yus', name: 'yus ng' }; - const options: LDOptions = { - bootstrap: { - 'test-flag': true, - 'another-test-flag': false, - }, - }; - const reactOptions: LDReactOptions = { - useCamelCaseFlagKeys: false, - }; - const LaunchDarklyApp = withLDProvider({ clientSideID, user, options, reactOptions })(App); - const component = shallow(, { disableLifecycleMethods: true }); - const initialState = component.state() as HocState; - - expect(mockInitLDClient).not.toHaveBeenCalled(); - expect(initialState.flags).toEqual({ 'test-flag': true, 'another-test-flag': false }); - }); - - test('ld client should transform keys to camel case if transform option is absent', () => { - const user: LDUser = { key: 'yus', name: 'yus ng' }; - const options: LDOptions = { - bootstrap: { - 'test-flag': true, - 'another-test-flag': false, - }, - }; - const reactOptions: LDReactOptions = {}; - const LaunchDarklyApp = withLDProvider({ clientSideID, user, options, reactOptions })(App); - const component = shallow(, { disableLifecycleMethods: true }); - const initialState = component.state() as HocState; - - expect(mockInitLDClient).not.toHaveBeenCalled(); - expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); - }); - - test('ld client should transform keys to camel case if react options object is absent', () => { - const user: LDUser = { key: 'yus', name: 'yus ng' }; - const options: LDOptions = { - bootstrap: { - 'test-flag': true, - 'another-test-flag': false, - }, - }; - const LaunchDarklyApp = withLDProvider({ clientSideID, user, options })(App); - const component = shallow(, { disableLifecycleMethods: true }); - const initialState = component.state() as HocState; - - expect(mockInitLDClient).not.toHaveBeenCalled(); - expect(initialState.flags).toEqual({ testFlag: true, anotherTestFlag: false }); - }); - - test('state.flags should be initialised to empty when bootstrapping from localStorage', () => { - const user: LDUser = { key: 'yus', name: 'yus ng' }; - const options: LDOptions = { - bootstrap: 'localStorage', - }; - const LaunchDarklyApp = withLDProvider({ clientSideID, user, options })(App); - const component = shallow(, { disableLifecycleMethods: true }); - const initialState = component.state() as HocState; - - expect(mockInitLDClient).not.toHaveBeenCalled(); - expect(initialState.flags).toEqual({}); - }); - test('ld client is initialised correctly with target flags', async () => { mockInitLDClient.mockImplementation(() => ({ flags: { devTestFlag: true, launchDoggly: true }, @@ -155,7 +56,7 @@ describe('withLDProvider', () => { const options: LDOptions = { bootstrap: {} }; const flags = { 'dev-test-flag': false, 'launch-doggly': false }; const LaunchDarklyApp = withLDProvider({ clientSideID, user, options, flags })(App); - const instance = create().root.instance as EnhancedComponent; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; instance.setState = jest.fn(); await instance.componentDidMount(); @@ -169,7 +70,7 @@ describe('withLDProvider', () => { test('flags and ldClient are saved in state on mount', async () => { const LaunchDarklyApp = withLDProvider({ clientSideID })(App); - const instance = create().root.instance as EnhancedComponent; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; instance.setState = jest.fn(); await instance.componentDidMount(); @@ -178,7 +79,7 @@ describe('withLDProvider', () => { test('subscribeToChanges is called on mount', async () => { const LaunchDarklyApp = withLDProvider({ clientSideID })(App); - const instance = create().root.instance as EnhancedComponent; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; instance.subscribeToChanges = jest.fn(); await instance.componentDidMount(); @@ -190,7 +91,7 @@ describe('withLDProvider', () => { cb({ 'test-flag': { current: false, previous: true } }); }); const LaunchDarklyApp = withLDProvider({ clientSideID })(App); - const instance = create().root.instance as EnhancedComponent; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; const mockSetState = jest.spyOn(instance, 'setState'); await instance.componentDidMount(); @@ -206,7 +107,7 @@ describe('withLDProvider', () => { cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } }); }); const LaunchDarklyApp = withLDProvider({ clientSideID, reactOptions: { useCamelCaseFlagKeys: false } })(App); - const instance = create().root.instance as EnhancedComponent; + const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; const mockSetState = jest.spyOn(instance, 'setState'); await instance.componentDidMount(); From 71632556292b48b4568fa7aee21a26a4ced59f2d Mon Sep 17 00:00:00 2001 From: Nicholas Mitchell Date: Sat, 22 Feb 2020 22:13:32 -0600 Subject: [PATCH 2/4] Add LDProvider component --- src/LDProvider.tsx | 62 ++++++++++++++++++++++ src/__snapshots__/LDProvider.test.tsx.snap | 16 ++++++ src/types.ts | 1 + src/withLDProvider.tsx | 58 +++----------------- 4 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 src/LDProvider.tsx create mode 100644 src/__snapshots__/LDProvider.test.tsx.snap diff --git a/src/LDProvider.tsx b/src/LDProvider.tsx new file mode 100644 index 0000000..4b51fe9 --- /dev/null +++ b/src/LDProvider.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import camelCase from 'lodash.camelcase'; +import { LDClient, LDFlagSet, LDFlagChangeset } from 'launchdarkly-js-client-sdk'; +import { EnhancedComponent, ProviderConfig, defaultReactOptions } from './types'; +import { Provider, LDContext as HocState } from './context'; +import initLDClient from './initLDClient'; +import { camelCaseKeys } from './utils'; + +class LDProvider extends React.Component implements EnhancedComponent { + readonly state: Readonly; + + constructor(props: ProviderConfig) { + super(props); + + const { options, reactOptions } = props; + + this.state = { + flags: {}, + ldClient: undefined, + }; + + if (options) { + const { bootstrap } = options; + if (bootstrap && bootstrap !== 'localStorage') { + const { useCamelCaseFlagKeys = defaultReactOptions.useCamelCaseFlagKeys } = reactOptions || {}; + const flags = useCamelCaseFlagKeys ? camelCaseKeys(bootstrap) : bootstrap; + this.state = { + flags, + ldClient: undefined, + }; + } + } + } + + subscribeToChanges = (ldClient: LDClient) => { + ldClient.on('change', (changes: LDFlagChangeset) => { + const flattened: LDFlagSet = {}; + const { reactOptions } = this.props; + for (const key in changes) { + // tslint:disable-next-line:no-unsafe-any + const { useCamelCaseFlagKeys = defaultReactOptions.useCamelCaseFlagKeys } = reactOptions || {}; + const flagKey = useCamelCaseFlagKeys ? camelCase(key) : key; + flattened[flagKey] = changes[key].current; + } + this.setState(({ flags }) => ({ flags: { ...flags, ...flattened } })); + }); + }; + + async componentDidMount() { + const { clientSideID, user, flags, reactOptions: userReactOptions, options } = this.props; + const reactOptions = { ...defaultReactOptions, ...userReactOptions }; + const { flags: fetchedFlags, ldClient } = await initLDClient(clientSideID, user, reactOptions, options, flags); + this.setState({ flags: fetchedFlags, ldClient }); + this.subscribeToChanges(ldClient); + } + + render() { + return {this.props.children}; + } +} + +export default LDProvider; diff --git a/src/__snapshots__/LDProvider.test.tsx.snap b/src/__snapshots__/LDProvider.test.tsx.snap new file mode 100644 index 0000000..1e0634b --- /dev/null +++ b/src/__snapshots__/LDProvider.test.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LDProvider render app 1`] = ` + +
+ My App +
+
+`; diff --git a/src/types.ts b/src/types.ts index b4c2f4a..57c2512 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { ProviderConfig } from './types'; import { LDClient, LDFlagSet, LDOptions, LDUser } from 'launchdarkly-js-client-sdk'; import * as React from 'react'; diff --git a/src/withLDProvider.tsx b/src/withLDProvider.tsx index 19299fa..ed4e16b 100644 --- a/src/withLDProvider.tsx +++ b/src/withLDProvider.tsx @@ -1,16 +1,12 @@ import * as React from 'react'; -import camelCase from 'lodash.camelcase'; -import { LDClient, LDFlagSet, LDFlagChangeset } from 'launchdarkly-js-client-sdk'; import { defaultReactOptions, ProviderConfig, EnhancedComponent } from './types'; -import { Provider, LDContext as HocState } from './context'; -import initLDClient from './initLDClient'; -import { camelCaseKeys } from './utils'; +import LDProvider from './LDProvider'; /** * `withLDProvider` is a function which accepts a config object which is used to * initialize `launchdarkly-js-client-sdk`. * - * This HOC does three things: + * This HOC handles passing configuration to the `LDProvider`, which does the following: * - It initializes the ldClient instance by calling `launchdarkly-js-client-sdk` initialize on `componentDidMount` * - It saves all flags and the ldClient instance in the context API * - It subscribes to flag changes and propagate them through the context API @@ -28,56 +24,16 @@ import { camelCaseKeys } from './utils'; */ export function withLDProvider(config: ProviderConfig) { return function withLDProviderHoc

(WrappedComponent: React.ComponentType

) { - const { options, reactOptions: userReactOptions } = config; + const { reactOptions: userReactOptions } = config; const reactOptions = { ...defaultReactOptions, ...userReactOptions }; + const providerProps = { ...config, reactOptions }; - return class extends React.Component implements EnhancedComponent { - readonly state: Readonly; - - constructor(props: P) { - super(props); - - this.state = { - flags: {}, - ldClient: undefined, - }; - - if (options) { - const { bootstrap } = options; - if (bootstrap && bootstrap !== 'localStorage') { - const flags = reactOptions.useCamelCaseFlagKeys ? camelCaseKeys(bootstrap) : bootstrap; - this.state = { - flags, - ldClient: undefined, - }; - } - } - } - - subscribeToChanges = (ldClient: LDClient) => { - ldClient.on('change', (changes: LDFlagChangeset) => { - const flattened: LDFlagSet = {}; - for (const key in changes) { - // tslint:disable-next-line:no-unsafe-any - const flagKey = reactOptions.useCamelCaseFlagKeys ? camelCase(key) : key; - flattened[flagKey] = changes[key].current; - } - this.setState(({ flags }) => ({ flags: { ...flags, ...flattened } })); - }); - }; - - async componentDidMount() { - const { clientSideID, user, flags } = config; - const { flags: fetchedFlags, ldClient } = await initLDClient(clientSideID, user, reactOptions, options, flags); - this.setState({ flags: fetchedFlags, ldClient }); - this.subscribeToChanges(ldClient); - } - + return class extends React.Component

{ render() { return ( - + - + ); } }; From fa16752e1f55eb952ef0c413f82c5d4b509e4211 Mon Sep 17 00:00:00 2001 From: Nicholas Mitchell Date: Sat, 22 Feb 2020 22:14:23 -0600 Subject: [PATCH 3/4] Add LDProvider to main export --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b87bb77..3e3bb37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import LDProvider from './LDProvider'; import withLDProvider from './withLDProvider'; import asyncWithLDProvider from './asyncWithLDProvider'; import withLDConsumer from './withLDConsumer'; @@ -5,4 +6,4 @@ import useFlags from './useFlags'; import useLDClient from './useLDClient'; import camelCaseKeys from './utils'; -export { withLDProvider, withLDConsumer, useFlags, useLDClient, asyncWithLDProvider, camelCaseKeys }; +export { LDProvider, withLDProvider, withLDConsumer, useFlags, useLDClient, asyncWithLDProvider, camelCaseKeys }; From 37f51d422a82a271983a1dcf0a55243ddefe4426 Mon Sep 17 00:00:00 2001 From: Nicholas Mitchell Date: Sat, 22 Feb 2020 22:45:59 -0600 Subject: [PATCH 4/4] Update provider tests --- ...r.test.tsx.snap => provider.test.tsx.snap} | 0 src/index.ts | 2 +- ...{LDProvider.test.tsx => provider.test.tsx} | 127 +++++++++++++----- src/{LDProvider.tsx => provider.tsx} | 17 +++ src/withLDProvider.test.tsx | 2 +- src/withLDProvider.tsx | 2 +- 6 files changed, 114 insertions(+), 36 deletions(-) rename src/__snapshots__/{LDProvider.test.tsx.snap => provider.test.tsx.snap} (100%) rename src/{LDProvider.test.tsx => provider.test.tsx} (65%) rename src/{LDProvider.tsx => provider.tsx} (68%) diff --git a/src/__snapshots__/LDProvider.test.tsx.snap b/src/__snapshots__/provider.test.tsx.snap similarity index 100% rename from src/__snapshots__/LDProvider.test.tsx.snap rename to src/__snapshots__/provider.test.tsx.snap diff --git a/src/index.ts b/src/index.ts index 3e3bb37..83de5e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import LDProvider from './LDProvider'; +import LDProvider from './provider'; import withLDProvider from './withLDProvider'; import asyncWithLDProvider from './asyncWithLDProvider'; import withLDConsumer from './withLDConsumer'; diff --git a/src/LDProvider.test.tsx b/src/provider.test.tsx similarity index 65% rename from src/LDProvider.test.tsx rename to src/provider.test.tsx index e2c03bf..e1c3a4d 100644 --- a/src/LDProvider.test.tsx +++ b/src/provider.test.tsx @@ -8,14 +8,10 @@ import { LDFlagChangeset, LDFlagSet, LDOptions, LDUser } from 'launchdarkly-js-c import initLDClient from './initLDClient'; import { LDReactOptions, EnhancedComponent, defaultReactOptions, ProviderConfig } from './types'; import { LDContext as HocState } from './context'; -import LDProvider from './LDProvider'; +import LDProvider from './provider'; const clientSideID = 'deadbeef'; -const LaunchDarklyApp = (props: ProviderConfig) => ( - -

My App
- -); +const App = () =>
My App
; const mockInitLDClient = initLDClient as jest.Mock; const mockFlags = { testFlag: true, anotherTestFlag: true }; const mockLDClient = { @@ -37,16 +33,26 @@ describe('LDProvider', () => { }); test('render app', () => { - const props = { clientSideID }; - const component = create(); + const props: ProviderConfig = { clientSideID }; + const LaunchDarklyApp = ( + + + + ); + const component = create(LaunchDarklyApp); expect(component).toMatchSnapshot(); }); - test.only('ld client is initialised correctly', async () => { + test('ld client is initialised correctly', async () => { const user: LDUser = { key: 'yus', name: 'yus ng' }; const options: LDOptions = { bootstrap: {} }; - const props = { clientSideID, user, options }; - const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const props: ProviderConfig = { clientSideID, user, options }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; await instance.componentDidMount(); expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, undefined); @@ -57,8 +63,13 @@ describe('LDProvider', () => { const options: LDOptions = { bootstrap: {}, }; - const props = { clientSideID, suser, options }; - const component = shallow(, { disableLifecycleMethods: true }); + const props: ProviderConfig = { clientSideID, user, options }; + const LaunchDarklyApp = ( + + + + ); + const component = shallow(LaunchDarklyApp, { disableLifecycleMethods: true }); const initialState = component.state() as HocState; expect(initialState.flags).toEqual({}); @@ -77,8 +88,13 @@ describe('LDProvider', () => { $valid: true, }, }; - const props = { clientSideID, user, options }; - const component = shallow(, { disableLifecycleMethods: true }); + const props: ProviderConfig = { clientSideID, user, options }; + const LaunchDarklyApp = ( + + + + ); + const component = shallow(LaunchDarklyApp, { disableLifecycleMethods: true }); const initialState = component.state() as HocState; expect(mockInitLDClient).not.toHaveBeenCalled(); @@ -96,8 +112,13 @@ describe('LDProvider', () => { const reactOptions: LDReactOptions = { useCamelCaseFlagKeys: false, }; - const props = { clientSideID, user, options, reactOptions }; - const component = shallow(, { disableLifecycleMethods: true }); + const props: ProviderConfig = { clientSideID, user, options, reactOptions }; + const LaunchDarklyApp = ( + + + + ); + const component = shallow(LaunchDarklyApp, { disableLifecycleMethods: true }); const initialState = component.state() as HocState; expect(mockInitLDClient).not.toHaveBeenCalled(); @@ -113,8 +134,13 @@ describe('LDProvider', () => { }, }; const reactOptions: LDReactOptions = {}; - const props = { clientSideID, user, options, reactOptions }; - const component = shallow(, { disableLifecycleMethods: true }); + const props: ProviderConfig = { clientSideID, user, options, reactOptions }; + const LaunchDarklyApp = ( + + + + ); + const component = shallow(LaunchDarklyApp, { disableLifecycleMethods: true }); const initialState = component.state() as HocState; expect(mockInitLDClient).not.toHaveBeenCalled(); @@ -129,8 +155,13 @@ describe('LDProvider', () => { 'another-test-flag': false, }, }; - const props = { clientSideID, user, options }; - const component = shallow(, { disableLifecycleMethods: true }); + const props: ProviderConfig = { clientSideID, user, options }; + const LaunchDarklyApp = ( + + + + ); + const component = shallow(LaunchDarklyApp, { disableLifecycleMethods: true }); const initialState = component.state() as HocState; expect(mockInitLDClient).not.toHaveBeenCalled(); @@ -142,8 +173,13 @@ describe('LDProvider', () => { const options: LDOptions = { bootstrap: 'localStorage', }; - const props = { clientSideID, user, options }; - const component = shallow(, { disableLifecycleMethods: true }); + const props: ProviderConfig = { clientSideID, user, options }; + const LaunchDarklyApp = ( + + + + ); + const component = shallow(LaunchDarklyApp, { disableLifecycleMethods: true }); const initialState = component.state() as HocState; expect(mockInitLDClient).not.toHaveBeenCalled(); @@ -158,8 +194,13 @@ describe('LDProvider', () => { const user: LDUser = { key: 'yus', name: 'yus ng' }; const options: LDOptions = { bootstrap: {} }; const flags = { 'dev-test-flag': false, 'launch-doggly': false }; - const props = { clientSideID, user, options, flags }; - const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const props: ProviderConfig = { clientSideID, user, options, flags }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; instance.setState = jest.fn(); await instance.componentDidMount(); @@ -172,8 +213,13 @@ describe('LDProvider', () => { }); test('flags and ldClient are saved in state on mount', async () => { - const props = { clientSideID }; - const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const props: ProviderConfig = { clientSideID }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; instance.setState = jest.fn(); await instance.componentDidMount(); @@ -181,8 +227,13 @@ describe('LDProvider', () => { }); test('subscribeToChanges is called on mount', async () => { - const props = { clientSideID }; - const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const props: ProviderConfig = { clientSideID }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; instance.subscribeToChanges = jest.fn(); await instance.componentDidMount(); @@ -193,8 +244,13 @@ describe('LDProvider', () => { mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => { cb({ 'test-flag': { current: false, previous: true } }); }); - const props = { clientSideID }; - const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const props: ProviderConfig = { clientSideID }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; const mockSetState = jest.spyOn(instance, 'setState'); await instance.componentDidMount(); @@ -209,8 +265,13 @@ describe('LDProvider', () => { mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => { cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } }); }); - const props = { clientSideID, reactOptions: { useCamelCaseFlagKeys: false } }; - const instance = create().root.findByType(LDProvider).instance as EnhancedComponent; + const props: ProviderConfig = { clientSideID, reactOptions: { useCamelCaseFlagKeys: false } }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; const mockSetState = jest.spyOn(instance, 'setState'); await instance.componentDidMount(); diff --git a/src/LDProvider.tsx b/src/provider.tsx similarity index 68% rename from src/LDProvider.tsx rename to src/provider.tsx index 4b51fe9..b8df0d6 100644 --- a/src/LDProvider.tsx +++ b/src/provider.tsx @@ -6,6 +6,23 @@ import { Provider, LDContext as HocState } from './context'; import initLDClient from './initLDClient'; import { camelCaseKeys } from './utils'; +/** + * The `LDProvider` is a component which accepts a config object which is used to + * initialize `launchdarkly-js-client-sdk`. + * + * This Provider does three things: + * - It initializes the ldClient instance by calling `launchdarkly-js-client-sdk` initialize on `componentDidMount` + * - It saves all flags and the ldClient instance in the context API + * - It subscribes to flag changes and propagate them through the context API + * + * Because the `launchdarkly-js-client-sdk` in only initialized on `componentDidMount`, your flags and the + * ldClient are only available after your app has mounted. This can result in a flicker due to flag changes at + * startup time. + * + * This component can be used as a standalone provider. However, be mindful to only include the component once + * within your application. This provider is used inside the `withLDProviderHOC` and can be used instead to initialize + * the `launchdarkly-js-client-sdk`. For async initialization, check out the `asyncWithLDProvider` function + */ class LDProvider extends React.Component implements EnhancedComponent { readonly state: Readonly; diff --git a/src/withLDProvider.test.tsx b/src/withLDProvider.test.tsx index 01246a3..777d0ff 100644 --- a/src/withLDProvider.test.tsx +++ b/src/withLDProvider.test.tsx @@ -7,7 +7,7 @@ import { LDFlagChangeset, LDFlagSet, LDOptions, LDUser } from 'launchdarkly-js-c import initLDClient from './initLDClient'; import withLDProvider from './withLDProvider'; import { EnhancedComponent, defaultReactOptions } from './types'; -import LDProvider from './LDProvider'; +import LDProvider from './provider'; const clientSideID = 'deadbeef'; const App = () =>
My App
; diff --git a/src/withLDProvider.tsx b/src/withLDProvider.tsx index ed4e16b..841cb0d 100644 --- a/src/withLDProvider.tsx +++ b/src/withLDProvider.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { defaultReactOptions, ProviderConfig, EnhancedComponent } from './types'; -import LDProvider from './LDProvider'; +import LDProvider from './provider'; /** * `withLDProvider` is a function which accepts a config object which is used to