From d96d94fe87a466e591afac6824742f2000c70c15 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 15 Sep 2025 10:34:05 +0200 Subject: [PATCH 1/4] Revert "fix: CORS errors due to header changes (#342)" This reverts commit 5c89532a202bcd60cbd250c4f00acdcb0dbd5faa. --- flagsmith-core.ts | 4 ++++ test/init.test.ts | 1 + utils/version.ts | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flagsmith-core.ts b/flagsmith-core.ts index b3a0f4f..877e946 100644 --- a/flagsmith-core.ts +++ b/flagsmith-core.ts @@ -829,6 +829,10 @@ const Flagsmith = class { options.headers['Flagsmith-Application-Version'] = this.applicationMetadata.version; } + if (SDK_VERSION) { + options.headers['Flagsmith-SDK-user-agent'] = `flagsmith-js-sdk/${SDK_VERSION}` + } + if (headers) { Object.assign(options.headers, headers); } diff --git a/test/init.test.ts b/test/init.test.ts index aa1823d..6d9d66e 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -292,6 +292,7 @@ describe('Flagsmith.init', () => { headers: expect.objectContaining({ 'Flagsmith-Application-Name': 'Test App', 'Flagsmith-Application-Version': '1.2.3', + 'Flagsmith-SDK-user-agent': `flagsmith-js-sdk/${SDK_VERSION}`, }), }), ); diff --git a/utils/version.ts b/utils/version.ts index 01abe05..f34f547 100644 --- a/utils/version.ts +++ b/utils/version.ts @@ -1,2 +1,2 @@ // Auto-generated by write-version.js -export const SDK_VERSION = "9.3.2"; +export const SDK_VERSION = "9.3.1"; From 4ded71a5df0a19cde6301201fb3f678ac630650d Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 30 Oct 2025 11:04:12 +0100 Subject: [PATCH 2/4] fix: surface-init-error-in-react-wrappers --- react.tsx | 213 ++++++++++++++++++++------------------------ test/react.test.tsx | 39 ++++++++ 2 files changed, 136 insertions(+), 116 deletions(-) diff --git a/react.tsx b/react.tsx index 53a3de6..12fda5b 100644 --- a/react.tsx +++ b/react.tsx @@ -1,145 +1,129 @@ -import React, { - createContext, - FC, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import Emitter from './utils/emitter'; const events = new Emitter(); -import { IFlagsmith, IFlagsmithTrait, IFlagsmithFeature, IState } from './types' +import { IFlagsmith, IFlagsmithTrait, IFlagsmithFeature, IState } from './types'; -export const FlagsmithContext = createContext | null>(null) +export const FlagsmithContext = createContext | null>(null); export type FlagsmithContextType = { - flagsmith: IFlagsmith // The flagsmith instance - options?: Parameters[0] // Initialisation options, if you do not provide this you will have to call init manually - serverState?: IState + flagsmith: IFlagsmith; // The flagsmith instance + options?: Parameters[0]; // Initialisation options, if you do not provide this you will have to call init manually + serverState?: IState; children: React.ReactNode; -} +}; -export const FlagsmithProvider: FC = ({ - flagsmith, options, serverState, children, - }) => { - const firstRenderRef = useRef(true) +export const FlagsmithProvider: FC = ({ flagsmith, options, serverState, children }) => { + const firstRenderRef = useRef(true); if (flagsmith && !flagsmith?._trigger) { flagsmith._trigger = () => { // @ts-expect-error using internal function, consumers would never call this - flagsmith.log("React - trigger event received") + flagsmith?.log('React - trigger event received'); events.emit('event'); - } + }; } if (flagsmith && !flagsmith?._triggerLoadingState) { flagsmith._triggerLoadingState = () => { events.emit('loading_event'); - } + }; } if (serverState && !flagsmith.initialised) { - flagsmith.setState(serverState) + flagsmith.setState(serverState); } if (firstRenderRef.current) { - firstRenderRef.current = false + firstRenderRef.current = false; if (options) { - flagsmith.init({ - ...options, - state: options.state || serverState, - onChange: (...args) => { - if (options.onChange) { - options.onChange(...args) - } - }, - }) + flagsmith + .init({ + ...options, + state: options.state || serverState, + onChange: (...args) => { + if (options.onChange) { + options.onChange(...args); + } + }, + }) + .catch((error) => { + // @ts-expect-error using internal function, consumers would never call this + flagsmith?.log('React - Failed to initialize flagsmith', error) + events.emit('event'); + }); } } - return ( - - {children} - - ) -} + return {children}; +}; const useConstant = function (value: T): T { - const ref = useRef(value) + const ref = useRef(value); if (!ref.current) { - ref.current = value + ref.current = value; } - return ref.current -} - + return ref.current; +}; const flagsAsArray = (_flags: any): string[] => { if (typeof _flags === 'string') { - return [_flags] + return [_flags]; } else if (typeof _flags === 'object') { // eslint-disable-next-line no-prototype-builtins if (_flags.hasOwnProperty('length')) { - return _flags + return _flags; } } - throw new Error( - 'Flagsmith: please supply an array of strings or a single string of flag keys to useFlags', - ) -} + throw new Error('Flagsmith: please supply an array of strings or a single string of flag keys to useFlags'); +}; const getRenderKey = (flagsmith: IFlagsmith, flags: string[], traits: string[] = []) => { return flags .map((k) => { - return `${flagsmith.getValue(k)}${flagsmith.hasFeature(k)}` - }).concat(traits.map((t) => ( - `${flagsmith.getTrait(t)}` - ))) - .join(',') -} + return `${flagsmith.getValue(k)}${flagsmith.hasFeature(k)}`; + }) + .concat(traits.map((t) => `${flagsmith.getTrait(t)}`)) + .join(','); +}; export function useFlagsmithLoading() { const flagsmith = useContext(FlagsmithContext); const [loadingState, setLoadingState] = useState(flagsmith?.loadingState); const [subscribed, setSubscribed] = useState(false); - const refSubscribed = useRef(subscribed) + const refSubscribed = useRef(subscribed); const eventListener = useCallback(() => { setLoadingState(flagsmith?.loadingState); - }, [flagsmith]) + }, [flagsmith]); if (!refSubscribed.current) { - events.on('loading_event', eventListener) - refSubscribed.current = true + events.on('loading_event', eventListener); + refSubscribed.current = true; } useEffect(() => { if (!subscribed && flagsmith?.initialised) { - events.on('loading_event', eventListener) - setSubscribed(true) + events.on('loading_event', eventListener); + setSubscribed(true); } return () => { if (subscribed) { - events.off('loading_event', eventListener) + events.off('loading_event', eventListener); } }; - }, [flagsmith, subscribed, eventListener]) + }, [flagsmith, subscribed, eventListener]); - return loadingState + return loadingState; } -type UseFlagsReturn< - F extends string | Record, - T extends string -> = [F] extends [string] +type UseFlagsReturn, T extends string> = F extends string ? { - [K in F]: IFlagsmithFeature; -} & { - [K in T]: IFlagsmithTrait; -} + [K in F]: IFlagsmithFeature; + } & { + [K in T]: IFlagsmithTrait; + } : { - [K in keyof F]: IFlagsmithFeature; -} & { - [K in T]: IFlagsmithTrait; -}; + [K in keyof F]: IFlagsmithFeature; + } & { + [K in T]: IFlagsmithTrait; + }; /** * Example usage: @@ -154,66 +138,63 @@ type UseFlagsReturn< * } * useFlags(["featureOne", "featureTwo"]); */ -export function useFlags< - F extends string | Record, - T extends string = string ->( - _flags: readonly (F | keyof F)[], _traits: readonly T[] = [] -){ - const firstRender = useRef(true) - const flags = useConstant(flagsAsArray(_flags)) - const traits = useConstant(flagsAsArray(_traits)) - const flagsmith = useContext(FlagsmithContext) +export function useFlags, T extends string = string>( + _flags: readonly (F | keyof F)[], + _traits: readonly T[] = [], +) { + const firstRender = useRef(true); + const flags = useConstant(flagsAsArray(_flags)); + const traits = useConstant(flagsAsArray(_traits)); + const flagsmith = useContext(FlagsmithContext); const [renderRef, setRenderRef] = useState(getRenderKey(flagsmith as IFlagsmith, flags, traits)); const eventListener = useCallback(() => { - const newRenderKey = getRenderKey(flagsmith as IFlagsmith, flags, traits) + const newRenderKey = getRenderKey(flagsmith as IFlagsmith, flags, traits); if (newRenderKey !== renderRef) { // @ts-expect-error using internal function, consumers would never call this - flagsmith?.log("React - useFlags flags and traits have changed") - setRenderRef(newRenderKey) + flagsmith?.log('React - useFlags flags and traits have changed'); + setRenderRef(newRenderKey); } - }, [renderRef]) + }, [renderRef]); const emitterRef = useRef(events.once('event', eventListener)); - - if (firstRender.current) { firstRender.current = false; // @ts-expect-error using internal function, consumers would never call this - flagsmith?.log("React - Initialising event listeners") + flagsmith?.log('React - Initialising event listeners'); } - useEffect(()=>{ + useEffect(() => { return () => { - emitterRef.current?.() - } - }, []) + emitterRef.current?.(); + }; + }, []); const res = useMemo(() => { - const res: any = {} - flags.map((k) => { + const res: any = {}; + flags + .map((k) => { res[k] = { enabled: flagsmith!.hasFeature(k), value: flagsmith!.getValue(k), - } - }).concat(traits?.map((v) => { - res[v] = flagsmith!.getTrait(v) - })) - return res - }, [renderRef]) - - return res as UseFlagsReturn + }; + }) + .concat( + traits?.map((v) => { + res[v] = flagsmith!.getTrait(v); + }), + ); + return res; + }, [renderRef]); + + return res as UseFlagsReturn; } -export function useFlagsmith< - F extends string | Record, - T extends string = string ->() { - const context = useContext(FlagsmithContext) +export function useFlagsmith, T extends string = string>() { + const context = useContext(FlagsmithContext); if (!context) { - throw new Error('useFlagsmith must be used with in a FlagsmithProvider') + throw new Error('useFlagsmith must be used with in a FlagsmithProvider'); } - return context as unknown as IFlagsmith + return context as unknown as IFlagsmith; } diff --git a/test/react.test.tsx b/test/react.test.tsx index a91482c..ad1bd4f 100644 --- a/test/react.test.tsx +++ b/test/react.test.tsx @@ -192,3 +192,42 @@ describe('FlagsmithProvider', () => { expect(JSON.parse(screen.getByTestId("flags").innerHTML).hero.enabled).toBe(true) }); }); + +it('should not crash when server returns 500 error', async () => { + const onChange = jest.fn(); + const onError = jest.fn(); + + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ + onChange, + onError, + }); + + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + status: 500, + headers: { get: () => null }, + text: () => Promise.resolve('API Response: 500') + }) + ); + + expect(() => { + render( + + + + ); + }).not.toThrow(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + + await waitFor(() => { + // Loading should complete with error + const loadingState = JSON.parse(screen.getByTestId("loading-state").innerHTML); + expect(loadingState.isLoading).toBe(false); + expect(loadingState.isFetching).toBe(false); + expect(loadingState.error).toBeTruthy(); + }); + + // onError callback should have been called + expect(onError).toHaveBeenCalledTimes(1); +}); \ No newline at end of file From f3acfd9423349673faf3445280d91066d5f61d7d Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 30 Oct 2025 11:42:59 +0100 Subject: [PATCH 3/4] fix: added-unhandled-error-test --- test/react.test.tsx | 42 ++++++++++++++++++++++++++++++++++++++++++ utils/version.ts | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/test/react.test.tsx b/test/react.test.tsx index ad1bd4f..1987ddb 100644 --- a/test/react.test.tsx +++ b/test/react.test.tsx @@ -230,4 +230,46 @@ it('should not crash when server returns 500 error', async () => { // onError callback should have been called expect(onError).toHaveBeenCalledTimes(1); +}); + +it('should not throw unhandled promise rejection when server returns 500 error', async () => { + const onChange = jest.fn(); + const onError = jest.fn(); + const unhandledRejectionHandler = jest.fn(); + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ + onChange, + onError, + }); + window.addEventListener('unhandledrejection', unhandledRejectionHandler); + + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + status: 500, + headers: { get: () => null }, + text: () => Promise.resolve('API Response: 500') + }) + ); + + expect(() => { + render( + + + + ); + }).not.toThrow(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + + await waitFor(() => { + // Loading should complete with error + const loadingState = JSON.parse(screen.getByTestId("loading-state").innerHTML); + expect(loadingState.isLoading).toBe(false); + expect(loadingState.isFetching).toBe(false); + expect(loadingState.error).toBeTruthy(); + }); + + // onError callback should have been called + expect(onError).toHaveBeenCalledTimes(1); + window.removeEventListener('unhandledrejection', unhandledRejectionHandler); + }); \ No newline at end of file diff --git a/utils/version.ts b/utils/version.ts index f34f547..01abe05 100644 --- a/utils/version.ts +++ b/utils/version.ts @@ -1,2 +1,2 @@ // Auto-generated by write-version.js -export const SDK_VERSION = "9.3.1"; +export const SDK_VERSION = "9.3.2"; From 4effd886e69313e8eda733063dc5dc92ccc301ef Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 11 Nov 2025 10:19:29 +0100 Subject: [PATCH 4/4] fix: linter --- .prettierrc.json | 2 +- react.tsx | 160 +++++++++---------- test/react.test.tsx | 366 ++++++++++++++++++++++++-------------------- 3 files changed, 285 insertions(+), 243 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index b225054..eacce1d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,8 +2,8 @@ "proseWrap": "always", "singleQuote": true, "printWidth": 120, - "trailingComma": "all", "tabWidth": 4, + "semi": false, "overrides": [ { "files": "*.md", diff --git a/react.tsx b/react.tsx index 12fda5b..eb2d0c3 100644 --- a/react.tsx +++ b/react.tsx @@ -1,39 +1,39 @@ -import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import Emitter from './utils/emitter'; -const events = new Emitter(); +import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import Emitter from './utils/emitter' +const events = new Emitter() -import { IFlagsmith, IFlagsmithTrait, IFlagsmithFeature, IState } from './types'; +import { IFlagsmith, IFlagsmithTrait, IFlagsmithFeature, IState } from './types' -export const FlagsmithContext = createContext | null>(null); +export const FlagsmithContext = createContext | null>(null) export type FlagsmithContextType = { - flagsmith: IFlagsmith; // The flagsmith instance - options?: Parameters[0]; // Initialisation options, if you do not provide this you will have to call init manually - serverState?: IState; - children: React.ReactNode; -}; + flagsmith: IFlagsmith // The flagsmith instance + options?: Parameters[0] // Initialisation options, if you do not provide this you will have to call init manually + serverState?: IState + children: React.ReactNode +} export const FlagsmithProvider: FC = ({ flagsmith, options, serverState, children }) => { - const firstRenderRef = useRef(true); + const firstRenderRef = useRef(true) if (flagsmith && !flagsmith?._trigger) { flagsmith._trigger = () => { // @ts-expect-error using internal function, consumers would never call this - flagsmith?.log('React - trigger event received'); - events.emit('event'); - }; + flagsmith?.log('React - trigger event received') + events.emit('event') + } } if (flagsmith && !flagsmith?._triggerLoadingState) { flagsmith._triggerLoadingState = () => { - events.emit('loading_event'); - }; + events.emit('loading_event') + } } if (serverState && !flagsmith.initialised) { - flagsmith.setState(serverState); + flagsmith.setState(serverState) } if (firstRenderRef.current) { - firstRenderRef.current = false; + firstRenderRef.current = false if (options) { flagsmith .init({ @@ -41,89 +41,89 @@ export const FlagsmithProvider: FC = ({ flagsmith, options state: options.state || serverState, onChange: (...args) => { if (options.onChange) { - options.onChange(...args); + options.onChange(...args) } }, }) .catch((error) => { // @ts-expect-error using internal function, consumers would never call this flagsmith?.log('React - Failed to initialize flagsmith', error) - events.emit('event'); - }); + events.emit('event') + }) } } - return {children}; -}; + return {children} +} const useConstant = function (value: T): T { - const ref = useRef(value); + const ref = useRef(value) if (!ref.current) { - ref.current = value; + ref.current = value } - return ref.current; -}; + return ref.current +} const flagsAsArray = (_flags: any): string[] => { if (typeof _flags === 'string') { - return [_flags]; + return [_flags] } else if (typeof _flags === 'object') { // eslint-disable-next-line no-prototype-builtins if (_flags.hasOwnProperty('length')) { - return _flags; + return _flags } } - throw new Error('Flagsmith: please supply an array of strings or a single string of flag keys to useFlags'); -}; + throw new Error('Flagsmith: please supply an array of strings or a single string of flag keys to useFlags') +} const getRenderKey = (flagsmith: IFlagsmith, flags: string[], traits: string[] = []) => { return flags .map((k) => { - return `${flagsmith.getValue(k)}${flagsmith.hasFeature(k)}`; + return `${flagsmith.getValue(k)}${flagsmith.hasFeature(k)}` }) .concat(traits.map((t) => `${flagsmith.getTrait(t)}`)) - .join(','); -}; + .join(',') +} export function useFlagsmithLoading() { - const flagsmith = useContext(FlagsmithContext); - const [loadingState, setLoadingState] = useState(flagsmith?.loadingState); - const [subscribed, setSubscribed] = useState(false); - const refSubscribed = useRef(subscribed); + const flagsmith = useContext(FlagsmithContext) + const [loadingState, setLoadingState] = useState(flagsmith?.loadingState) + const [subscribed, setSubscribed] = useState(false) + const refSubscribed = useRef(subscribed) const eventListener = useCallback(() => { - setLoadingState(flagsmith?.loadingState); - }, [flagsmith]); + setLoadingState(flagsmith?.loadingState) + }, [flagsmith]) if (!refSubscribed.current) { - events.on('loading_event', eventListener); - refSubscribed.current = true; + events.on('loading_event', eventListener) + refSubscribed.current = true } useEffect(() => { if (!subscribed && flagsmith?.initialised) { - events.on('loading_event', eventListener); - setSubscribed(true); + events.on('loading_event', eventListener) + setSubscribed(true) } return () => { if (subscribed) { - events.off('loading_event', eventListener); + events.off('loading_event', eventListener) } - }; - }, [flagsmith, subscribed, eventListener]); + } + }, [flagsmith, subscribed, eventListener]) - return loadingState; + return loadingState } type UseFlagsReturn, T extends string> = F extends string ? { - [K in F]: IFlagsmithFeature; + [K in F]: IFlagsmithFeature } & { - [K in T]: IFlagsmithTrait; + [K in T]: IFlagsmithTrait } : { - [K in keyof F]: IFlagsmithFeature; + [K in keyof F]: IFlagsmithFeature } & { - [K in T]: IFlagsmithTrait; - }; + [K in T]: IFlagsmithTrait + } /** * Example usage: @@ -140,61 +140,61 @@ type UseFlagsReturn, T extends string> = */ export function useFlags, T extends string = string>( _flags: readonly (F | keyof F)[], - _traits: readonly T[] = [], + _traits: readonly T[] = [] ) { - const firstRender = useRef(true); - const flags = useConstant(flagsAsArray(_flags)); - const traits = useConstant(flagsAsArray(_traits)); - const flagsmith = useContext(FlagsmithContext); - const [renderRef, setRenderRef] = useState(getRenderKey(flagsmith as IFlagsmith, flags, traits)); + const firstRender = useRef(true) + const flags = useConstant(flagsAsArray(_flags)) + const traits = useConstant(flagsAsArray(_traits)) + const flagsmith = useContext(FlagsmithContext) + const [renderRef, setRenderRef] = useState(getRenderKey(flagsmith as IFlagsmith, flags, traits)) const eventListener = useCallback(() => { - const newRenderKey = getRenderKey(flagsmith as IFlagsmith, flags, traits); + const newRenderKey = getRenderKey(flagsmith as IFlagsmith, flags, traits) if (newRenderKey !== renderRef) { // @ts-expect-error using internal function, consumers would never call this - flagsmith?.log('React - useFlags flags and traits have changed'); - setRenderRef(newRenderKey); + flagsmith?.log('React - useFlags flags and traits have changed') + setRenderRef(newRenderKey) } - }, [renderRef]); - const emitterRef = useRef(events.once('event', eventListener)); + }, [renderRef]) + const emitterRef = useRef(events.once('event', eventListener)) if (firstRender.current) { - firstRender.current = false; + firstRender.current = false // @ts-expect-error using internal function, consumers would never call this - flagsmith?.log('React - Initialising event listeners'); + flagsmith?.log('React - Initialising event listeners') } useEffect(() => { return () => { - emitterRef.current?.(); - }; - }, []); + emitterRef.current?.() + } + }, []) const res = useMemo(() => { - const res: any = {}; + const res: any = {} flags .map((k) => { res[k] = { enabled: flagsmith!.hasFeature(k), value: flagsmith!.getValue(k), - }; + } }) .concat( traits?.map((v) => { - res[v] = flagsmith!.getTrait(v); - }), - ); - return res; - }, [renderRef]); + res[v] = flagsmith!.getTrait(v) + }) + ) + return res + }, [renderRef]) - return res as UseFlagsReturn; + return res as UseFlagsReturn } export function useFlagsmith, T extends string = string>() { - const context = useContext(FlagsmithContext); + const context = useContext(FlagsmithContext) if (!context) { - throw new Error('useFlagsmith must be used with in a FlagsmithProvider'); + throw new Error('useFlagsmith must be used with in a FlagsmithProvider') } - return context as unknown as IFlagsmith; + return context as unknown as IFlagsmith } diff --git a/test/react.test.tsx b/test/react.test.tsx index 1987ddb..cc996e0 100644 --- a/test/react.test.tsx +++ b/test/react.test.tsx @@ -1,6 +1,6 @@ -import React, { FC } from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { FlagsmithProvider, useFlags, useFlagsmithLoading } from '../lib/flagsmith/react'; +import React, { FC } from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { FlagsmithProvider, useFlags, useFlagsmithLoading } from '../lib/flagsmith/react' import { defaultState, delay, @@ -9,108 +9,128 @@ import { getMockFetchWithValue, identityState, testIdentity, -} from './test-constants'; -import removeIds from './test-utils/remove-ids'; -const FlagsmithPage: FC = () => { +} from './test-constants' +import removeIds from './test-utils/remove-ids' + +const FlagsmithPage: FC> = () => { const flags = useFlags(Object.keys(defaultState.flags)) const loadingState = useFlagsmithLoading() return ( <> -
- {JSON.stringify(flags)} -
-
- {JSON.stringify(loadingState)} -
+
{JSON.stringify(flags)}
+
{JSON.stringify(loadingState)}
- ); -}; - + ) +} -export default FlagsmithPage; +export default FlagsmithPage describe('FlagsmithProvider', () => { it('renders without crashing', () => { - - const onChange = jest.fn(); - const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange}) + const onChange = jest.fn() + const { flagsmith, initConfig } = getFlagsmith({ onChange }) render( - + - ); - }); + ) + }) it('renders default state without any cache or default flags', () => { - - const onChange = jest.fn(); - const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange}) + const onChange = jest.fn() + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange }) render( - + - ); + ) - expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual({"hero":{"enabled":false,"value":null},"font_size":{"enabled":false,"value":null},"json_value":{"enabled":false,"value":null},"number_value":{"enabled":false,"value":null},"off_value":{"enabled":false,"value":null}}) - expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":true,"isFetching":true,"error":null,"source":"NONE"}) - }); + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual({ + hero: { enabled: false, value: null }, + font_size: { enabled: false, value: null }, + json_value: { enabled: false, value: null }, + number_value: { enabled: false, value: null }, + off_value: { enabled: false, value: null }, + }) + expect(JSON.parse(screen.getByTestId('loading-state').innerHTML)).toEqual({ + isLoading: true, + isFetching: true, + error: null, + source: 'NONE', + }) + }) it('fetches and renders flags', async () => { - - const onChange = jest.fn(); - const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange}) + const onChange = jest.fn() + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange }) render( - + - ); + ) - expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledTimes(1) await waitFor(() => { - expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":false,"isFetching":false,"error":null,"source":"SERVER"}); - expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(defaultState.flags)); - }); - }); + expect(JSON.parse(screen.getByTestId('loading-state').innerHTML)).toEqual({ + isLoading: false, + isFetching: false, + error: null, + source: 'SERVER', + }) + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(defaultState.flags)) + }) + }) it('fetches and renders flags for an identified user', async () => { - - const onChange = jest.fn(); - const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange, identity:testIdentity}) + const onChange = jest.fn() + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange, identity: testIdentity }) render( - + - ); + ) - expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledTimes(1) await waitFor(() => { - expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":false,"isFetching":false,"error":null,"source":"SERVER"}); - expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(identityState.flags)); - }); - }); + expect(JSON.parse(screen.getByTestId('loading-state').innerHTML)).toEqual({ + isLoading: false, + isFetching: false, + error: null, + source: 'SERVER', + }) + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(identityState.flags)) + }) + }) it('renders cached flags', async () => { - - const onChange = jest.fn(); + const onChange = jest.fn() const { flagsmith, initConfig, AsyncStorage } = getFlagsmith({ onChange, cacheFlags: true, preventFetch: true, - defaultFlags: defaultState.flags - }); - await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ - ...defaultState - }) ) + defaultFlags: defaultState.flags, + }) + await AsyncStorage.setItem( + FLAGSMITH_KEY, + JSON.stringify({ + ...defaultState, + }) + ) render( - + - ); + ) await waitFor(() => { - expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":false,"isFetching":false,"error":null,"source":"CACHE"}); - expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(defaultState.flags)); - }); - }); + expect(JSON.parse(screen.getByTestId('loading-state').innerHTML)).toEqual({ + isLoading: false, + isFetching: false, + error: null, + source: 'CACHE', + }) + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(defaultState.flags)) + }) + }) it('renders cached flags by custom key', async () => { - const customKey = 'custom_key'; - const onChange = jest.fn(); + const customKey = 'custom_key' + const onChange = jest.fn() const { flagsmith, initConfig, AsyncStorage } = getFlagsmith({ onChange, cacheFlags: true, @@ -119,157 +139,179 @@ describe('FlagsmithProvider', () => { cacheOptions: { storageKey: customKey, }, - }); - await AsyncStorage.setItem(customKey, JSON.stringify({ - ...defaultState - }) ) + }) + await AsyncStorage.setItem( + customKey, + JSON.stringify({ + ...defaultState, + }) + ) render( - + - ); + ) await waitFor(() => { - expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":false,"isFetching":false,"error":null,"source":"CACHE"}); - expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(defaultState.flags)); - }); - }); + expect(JSON.parse(screen.getByTestId('loading-state').innerHTML)).toEqual({ + isLoading: false, + isFetching: false, + error: null, + source: 'CACHE', + }) + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(defaultState.flags)) + }) + }) it('renders default flags', async () => { - - const onChange = jest.fn(); - const { flagsmith, initConfig, AsyncStorage } = getFlagsmith({ + const onChange = jest.fn() + const { flagsmith, initConfig } = getFlagsmith({ onChange, preventFetch: true, - defaultFlags: defaultState.flags - }); + defaultFlags: defaultState.flags, + }) render( - + - ); + ) await waitFor(() => { - expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":false,"isFetching":false,"error":null,"source":"DEFAULT_FLAGS"}); - expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(defaultState.flags)); - }); - }); + expect(JSON.parse(screen.getByTestId('loading-state').innerHTML)).toEqual({ + isLoading: false, + isFetching: false, + error: null, + source: 'DEFAULT_FLAGS', + }) + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(defaultState.flags)) + }) + }) it('ignores init response if identify gets called and resolves first', async () => { - - const onChange = jest.fn(); - const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange}) - getMockFetchWithValue(mockFetch, [{ - enabled: false, - feature_state_value: null, - feature: { - id: 1, - name: "hero" - } - }],300) // resolves after flagsmith.identify, it should be ignored + const onChange = jest.fn() + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange }) + getMockFetchWithValue( + mockFetch, + [ + { + enabled: false, + feature_state_value: null, + feature: { + id: 1, + name: 'hero', + }, + }, + ], + 300 + ) // resolves after flagsmith.identify, it should be ignored render( - + - ); - expect(mockFetch).toHaveBeenCalledTimes(1); - getMockFetchWithValue(mockFetch, { - flags: [{ - enabled: true, - feature_state_value: null, - feature: { - id: 1, - name: "hero" - } - }] - },0) + ) + expect(mockFetch).toHaveBeenCalledTimes(1) + getMockFetchWithValue( + mockFetch, + { + flags: [ + { + enabled: true, + feature_state_value: null, + feature: { + id: 1, + name: 'hero', + }, + }, + ], + }, + 0 + ) await flagsmith.identify(testIdentity) - expect(mockFetch).toHaveBeenCalledTimes(2); + expect(mockFetch).toHaveBeenCalledTimes(2) await waitFor(() => { - expect(JSON.parse(screen.getByTestId("flags").innerHTML).hero.enabled).toBe(true) - }); + expect(JSON.parse(screen.getByTestId('flags').innerHTML).hero.enabled).toBe(true) + }) await delay(500) - expect(JSON.parse(screen.getByTestId("flags").innerHTML).hero.enabled).toBe(true) - }); -}); + expect(JSON.parse(screen.getByTestId('flags').innerHTML).hero.enabled).toBe(true) + }) +}) it('should not crash when server returns 500 error', async () => { - const onChange = jest.fn(); - const onError = jest.fn(); - + const onChange = jest.fn() + const onError = jest.fn() + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange, onError, - }); - + }) + mockFetch.mockImplementationOnce(() => Promise.resolve({ status: 500, headers: { get: () => null }, - text: () => Promise.resolve('API Response: 500') + text: () => Promise.resolve('API Response: 500'), }) - ); - + ) + expect(() => { render( - + - ); - }).not.toThrow(); - - expect(mockFetch).toHaveBeenCalledTimes(1); - + ) + }).not.toThrow() + + expect(mockFetch).toHaveBeenCalledTimes(1) + await waitFor(() => { // Loading should complete with error - const loadingState = JSON.parse(screen.getByTestId("loading-state").innerHTML); - expect(loadingState.isLoading).toBe(false); - expect(loadingState.isFetching).toBe(false); - expect(loadingState.error).toBeTruthy(); - }); - + const loadingState = JSON.parse(screen.getByTestId('loading-state').innerHTML) + expect(loadingState.isLoading).toBe(false) + expect(loadingState.isFetching).toBe(false) + expect(loadingState.error).toBeTruthy() + }) + // onError callback should have been called - expect(onError).toHaveBeenCalledTimes(1); -}); + expect(onError).toHaveBeenCalledTimes(1) +}) it('should not throw unhandled promise rejection when server returns 500 error', async () => { - const onChange = jest.fn(); - const onError = jest.fn(); - const unhandledRejectionHandler = jest.fn(); + const onChange = jest.fn() + const onError = jest.fn() + const unhandledRejectionHandler = jest.fn() const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange, onError, - }); - window.addEventListener('unhandledrejection', unhandledRejectionHandler); + }) + window.addEventListener('unhandledrejection', unhandledRejectionHandler) mockFetch.mockImplementationOnce(() => Promise.resolve({ status: 500, headers: { get: () => null }, - text: () => Promise.resolve('API Response: 500') + text: () => Promise.resolve('API Response: 500'), }) - ); - + ) + expect(() => { render( - + - ); - }).not.toThrow(); - - expect(mockFetch).toHaveBeenCalledTimes(1); - + ) + }).not.toThrow() + + expect(mockFetch).toHaveBeenCalledTimes(1) + await waitFor(() => { // Loading should complete with error - const loadingState = JSON.parse(screen.getByTestId("loading-state").innerHTML); - expect(loadingState.isLoading).toBe(false); - expect(loadingState.isFetching).toBe(false); - expect(loadingState.error).toBeTruthy(); - }); - - // onError callback should have been called - expect(onError).toHaveBeenCalledTimes(1); - window.removeEventListener('unhandledrejection', unhandledRejectionHandler); + const loadingState = JSON.parse(screen.getByTestId('loading-state').innerHTML) + expect(loadingState.isLoading).toBe(false) + expect(loadingState.isFetching).toBe(false) + expect(loadingState.error).toBeTruthy() + }) -}); \ No newline at end of file + // onError callback should have been called + expect(onError).toHaveBeenCalledTimes(1) + window.removeEventListener('unhandledrejection', unhandledRejectionHandler) +})