From 1a22d9a55f1bec9bbc2096e52236cf4fd743186b Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:04:56 +0200 Subject: [PATCH 01/11] test: flag status for initialized client --- src/useFlagStatus.test.tsx | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/useFlagStatus.test.tsx diff --git a/src/useFlagStatus.test.tsx b/src/useFlagStatus.test.tsx new file mode 100644 index 0000000..053ae79 --- /dev/null +++ b/src/useFlagStatus.test.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import useFlagsStatus from "./useFlagsStatus"; +import FlagProvider from './FlagProvider'; +import { EVENTS, type UnleashClient } from 'unleash-proxy-client'; + +const TestComponent = () => { + const { flagsReady } = useFlagsStatus(); + + return
{flagsReady ? 'flagsReady' : 'loading'}
; +} + +const mockClient = { + on: vi.fn(), + off: vi.fn(), + start: vi.fn(), + stop: vi.fn(), + updateContext: vi.fn(), + isEnabled: vi.fn(), + getVariant: vi.fn(), +} as unknown as UnleashClient; + +test('should initialize', async () => { + const onEventHandler = (event: string, callback: () => void) => { + if (event === 'ready') { + callback(); + } + } + + mockClient.on = onEventHandler as typeof mockClient.on; + + const ui = ( + + + + ) + + render(ui); + + await waitFor(() => { + expect(screen.queryByText('flagsReady')).toBeInTheDocument(); + }); +}); + +// https://github.com/Unleash/proxy-client-react/issues/168 +test('should start when already initialized client is passed', async () => { + let initialized = false; + const onEventHandler = (event: string, callback: () => void) => { + if (event === EVENTS.READY && !initialized) { + initialized = true; + callback(); + } + } + + mockClient.on = onEventHandler as typeof mockClient.on; + + await new Promise((resolve) => mockClient.on(EVENTS.READY, () => resolve)); + + const ui = ( + + + + ) + + render(ui); + + await waitFor(() => { + expect(screen.queryByText('flagsReady')).toBeInTheDocument(); + }); +}); From ba95fea4d984debcb480565f0995bbf5484c81de Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:14:16 +0200 Subject: [PATCH 02/11] add client stop param --- src/FlagContext.ts | 2 +- src/FlagProvider.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/FlagContext.ts b/src/FlagContext.ts index 597996c..537720f 100644 --- a/src/FlagContext.ts +++ b/src/FlagContext.ts @@ -4,7 +4,7 @@ import type { UnleashClient } from 'unleash-proxy-client'; export interface IFlagContextValue extends Pick< UnleashClient, - 'on' | 'updateContext' | 'isEnabled' | 'getVariant' + 'on' | 'off' | 'updateContext' | 'isEnabled' | 'getVariant' > { client: UnleashClient; flagsReady: boolean; diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index bb3f9c3..db8dd4d 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -29,6 +29,7 @@ const FlagProvider: React.FC> = ({ children, unleashClient, startClient = true, + stopClient = true, }) => { const config = customConfig || offlineConfig; const client = React.useRef( @@ -90,8 +91,10 @@ const FlagProvider: React.FC> = ({ if (client.current) { client.current.off('error', errorCallback); client.current.off('ready', readyCallback); - client.current.off('recovered', clearErrorCallback) - client.current.stop(); + client.current.off('recovered', clearErrorCallback); + if (stopClient) { + client.current.stop(); + } } if (timeout) { clearTimeout(timeout); From 934371abd84c9531be067360d0b52d389e22cfb1 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:15:53 +0200 Subject: [PATCH 03/11] refactor: reorder context methods --- src/FlagProvider.tsx | 46 +++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index db8dd4d..a65d1c6 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -1,13 +1,14 @@ /** @format */ -import * as React from 'react'; -import { IConfig, UnleashClient } from 'unleash-proxy-client'; -import FlagContext, { IFlagContextValue } from './FlagContext'; +import React, { type FC, type PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { type IConfig, UnleashClient } from 'unleash-proxy-client'; +import FlagContext, { type IFlagContextValue } from './FlagContext'; export interface IFlagProvider { config?: IConfig; unleashClient?: UnleashClient; startClient?: boolean; + stopClient?: boolean; } const offlineConfig: IConfig = { @@ -24,7 +25,7 @@ const _startTransition = 'startTransition'; // fallback for React <18 which doesn't support startTransition const startTransition = React[_startTransition] || (fn => fn()); -const FlagProvider: React.FC> = ({ +const FlagProvider: FC> = ({ config: customConfig, children, unleashClient, @@ -38,13 +39,13 @@ const FlagProvider: React.FC> = ({ const [flagsReady, setFlagsReady] = React.useState( Boolean( unleashClient - ? customConfig?.bootstrap && customConfig?.bootstrapOverride !== false + ? (customConfig?.bootstrap && customConfig?.bootstrapOverride !== false) || unleashClient.isReady() : config.bootstrap && config.bootstrapOverride !== false ) ); - const [flagsError, setFlagsError] = React.useState(null); + const [flagsError, setFlagsError] = useState(null); - React.useEffect(() => { + useEffect(() => { if (!config && !unleashClient) { console.error( `You must provide either a config or an unleash client to the flag provider. @@ -63,9 +64,9 @@ const FlagProvider: React.FC> = ({ startTransition(() => { setFlagsError(null); }); - } + } - let timeout: any; + let timeout: ReturnType | null = null; const readyCallback = () => { // wait for flags to resolve after useFlag gets the same event timeout = setTimeout(() => { @@ -102,28 +103,13 @@ const FlagProvider: React.FC> = ({ }; }, []); - const updateContext: IFlagContextValue['updateContext'] = async (context) => { - await client.current.updateContext(context); - }; - - const isEnabled: IFlagContextValue['isEnabled'] = (toggleName) => { - return client.current.isEnabled(toggleName); - }; - - const getVariant: IFlagContextValue['getVariant'] = (toggleName) => { - return client.current.getVariant(toggleName); - }; - - const on: IFlagContextValue['on'] = (event, callback, ctx) => { - return client.current.on(event, callback, ctx); - }; - - const context = React.useMemo( + const context = useMemo( () => ({ - on, - updateContext, - isEnabled, - getVariant, + on: ((event, callback, ctx) => client.current.on(event, callback, ctx)) as IFlagContextValue['on'], + off: ((event, callback) => client.current.off(event, callback)) as IFlagContextValue['off'], + updateContext: async (context) => await client.current.updateContext(context), + isEnabled: (toggleName) => client.current.isEnabled(toggleName), + getVariant: (toggleName) => client.current.getVariant(toggleName), client: client.current, flagsReady, flagsError, From 046014a4133d39d8ebfacc2a5c7c0324e125e844 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:57:19 +0200 Subject: [PATCH 04/11] chore: bump unleash js client --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 25210e2..c890c97 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.4.0", + "unleash-proxy-client": "^3.5.0", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.4.0" + "unleash-proxy-client": "^3.5.0" } } diff --git a/yarn.lock b/yarn.lock index b3bb1eb..c14f509 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.4.0.tgz#c9c4a8b0f18d77dc0b041eb76478c6ce74c98c1e" - integrity sha512-ivCzm//z+S2T3gSBSZY7HN+5GfoLXZIovMyH6lIZRe2/vCicEdXtXD6cnLTQ2LAiXGV7DpoSM1m8WZGoiLRzkw== +unleash-proxy-client@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.0.tgz#52ef4bc96c6c2bae445f6704c457584d4d3e4549" + integrity sha512-/LXs6+uvht/6/pO3pEdxW0n4yft8e+Ngw3/4z63f49gjTb0FE8rZvSosQcBajBiiCivpFx6eXAAoCcREGVCSJg== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From aa6d36ad1cce762c575b600c4905e32ec4ec0e61 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:58:16 +0200 Subject: [PATCH 05/11] use client ready state --- src/FlagProvider.tsx | 4 +- src/useFlagStatus.test.tsx | 102 ++++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index a65d1c6..adb1ff9 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -39,11 +39,11 @@ const FlagProvider: FC> = ({ const [flagsReady, setFlagsReady] = React.useState( Boolean( unleashClient - ? (customConfig?.bootstrap && customConfig?.bootstrapOverride !== false) || unleashClient.isReady() + ? (customConfig?.bootstrap && customConfig?.bootstrapOverride !== false) || unleashClient.isReady?.() : config.bootstrap && config.bootstrapOverride !== false ) ); - const [flagsError, setFlagsError] = useState(null); + const [flagsError, setFlagsError] = useState(client?.getError?.() || null); useEffect(() => { if (!config && !unleashClient) { diff --git a/src/useFlagStatus.test.tsx b/src/useFlagStatus.test.tsx index 053ae79..91e0498 100644 --- a/src/useFlagStatus.test.tsx +++ b/src/useFlagStatus.test.tsx @@ -1,70 +1,80 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; -import useFlagsStatus from "./useFlagsStatus"; +import useFlagsStatus from './useFlagsStatus'; import FlagProvider from './FlagProvider'; -import { EVENTS, type UnleashClient } from 'unleash-proxy-client'; +import { EVENTS, UnleashClient } from 'unleash-proxy-client'; const TestComponent = () => { - const { flagsReady } = useFlagsStatus(); + const { flagsReady } = useFlagsStatus(); - return
{flagsReady ? 'flagsReady' : 'loading'}
; -} + return
{flagsReady ? 'flagsReady' : 'loading'}
; +}; const mockClient = { - on: vi.fn(), - off: vi.fn(), - start: vi.fn(), - stop: vi.fn(), - updateContext: vi.fn(), - isEnabled: vi.fn(), - getVariant: vi.fn(), + on: vi.fn(), + off: vi.fn(), + start: vi.fn(), + stop: vi.fn(), + updateContext: vi.fn(), + isEnabled: vi.fn(), + getVariant: vi.fn(), + isReady: vi.fn(), } as unknown as UnleashClient; test('should initialize', async () => { - const onEventHandler = (event: string, callback: () => void) => { - if (event === 'ready') { - callback(); - } + const onEventHandler = (event: string, callback: () => void) => { + if (event === 'ready') { + callback(); } + }; - mockClient.on = onEventHandler as typeof mockClient.on; + mockClient.on = onEventHandler as typeof mockClient.on; - const ui = ( - - - - ) + const ui = ( + + + + ); - render(ui); + render(ui); - await waitFor(() => { - expect(screen.queryByText('flagsReady')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.queryByText('flagsReady')).toBeInTheDocument(); + }); }); // https://github.com/Unleash/proxy-client-react/issues/168 test('should start when already initialized client is passed', async () => { - let initialized = false; - const onEventHandler = (event: string, callback: () => void) => { - if (event === EVENTS.READY && !initialized) { - initialized = true; - callback(); - } - } - - mockClient.on = onEventHandler as typeof mockClient.on; - - await new Promise((resolve) => mockClient.on(EVENTS.READY, () => resolve)); + const client = new UnleashClient({ + url: 'http://localhost:4242/api', + fetch: async () => + new Promise((resolve) => { + setTimeout(() => + resolve({ + status: 200, + ok: true, + json: async () => ({ + toggles: [], + }), + headers: new Headers(), + }) + ); + }), + clientKey: '123', + appName: 'test', + }); + await client.start(); + expect(client.isReady()).toBe(true); - const ui = ( - - - - ) + const ui = ( + + + + ); - render(ui); + render(ui); - await waitFor(() => { - expect(screen.queryByText('flagsReady')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.queryByText('flagsReady')).toBeInTheDocument(); + }); }); From 570062bb63b51b02908d23d66b53ce7f43f28862 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:49:19 +0200 Subject: [PATCH 06/11] chore: bump proxy client --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c890c97..968e253 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "react-dom": "^18.2.0", "react-test-renderer": "^18.2.0", "typescript": "^5.3.2", - "unleash-proxy-client": "^3.5.0", + "unleash-proxy-client": "^3.5.1", "vite": "^4.5.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" }, "peerDependencies": { - "unleash-proxy-client": "^3.5.0" + "unleash-proxy-client": "^3.5.1" } } diff --git a/yarn.lock b/yarn.lock index c14f509..f44da1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,10 +1972,10 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unleash-proxy-client@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.0.tgz#52ef4bc96c6c2bae445f6704c457584d4d3e4549" - integrity sha512-/LXs6+uvht/6/pO3pEdxW0n4yft8e+Ngw3/4z63f49gjTb0FE8rZvSosQcBajBiiCivpFx6eXAAoCcREGVCSJg== +unleash-proxy-client@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/unleash-proxy-client/-/unleash-proxy-client-3.5.1.tgz#603af23b4c30e3509b8123e7a9ae99a9c38f335d" + integrity sha512-vfWAozp5O16ZedPPH7wFobsZaj8TQQEp/pfj+4jpWZTnOXyFpH6fAgrztRHO26bQ6iC95vVtfeVRQvgw9lo5zA== dependencies: tiny-emitter "^2.1.0" uuid "^9.0.1" From a3229c6efeaa5a93384a32b171cf692725933248 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:51:29 +0200 Subject: [PATCH 07/11] fix: freeze dependencies --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58b08b5..b0bd495 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: 📌 Install - run: yarn + run: yarn install --frozen-lockfile - name: 🔨 Build run: yarn build From 30acd8754d3d3b407dfbf54e1f1e530f21895c8e Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:09:09 +0200 Subject: [PATCH 08/11] add node v22 to build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0bd495..12d4486 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x, 21.x] + node-version: [18.x, 20.x, 21.x, 22.x] steps: - uses: actions/checkout@v2 From aba75257d24c80d1a239c039806003c00abc466d Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:10:37 +0200 Subject: [PATCH 09/11] test: client with errors --- src/FlagProvider.tsx | 2 +- src/useFlagStatus.test.tsx | 42 +++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index adb1ff9..4c43f9f 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -43,7 +43,7 @@ const FlagProvider: FC> = ({ : config.bootstrap && config.bootstrapOverride !== false ) ); - const [flagsError, setFlagsError] = useState(client?.getError?.() || null); + const [flagsError, setFlagsError] = useState(client.current.getError?.() || null); useEffect(() => { if (!config && !unleashClient) { diff --git a/src/useFlagStatus.test.tsx b/src/useFlagStatus.test.tsx index 91e0498..25663ee 100644 --- a/src/useFlagStatus.test.tsx +++ b/src/useFlagStatus.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import useFlagsStatus from './useFlagsStatus'; import FlagProvider from './FlagProvider'; -import { EVENTS, UnleashClient } from 'unleash-proxy-client'; +import { UnleashClient } from 'unleash-proxy-client'; const TestComponent = () => { const { flagsReady } = useFlagsStatus(); @@ -10,6 +10,13 @@ const TestComponent = () => { return
{flagsReady ? 'flagsReady' : 'loading'}
; }; +const ErrorTestComponent = () => { + const { flagsError } = useFlagsStatus(); + + return
{flagsError ? 'flagsError' : 'no issue'}
; +}; + + const mockClient = { on: vi.fn(), off: vi.fn(), @@ -78,3 +85,36 @@ test('should start when already initialized client is passed', async () => { expect(screen.queryByText('flagsReady')).toBeInTheDocument(); }); }); + +test('should handle client errors', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const client = new UnleashClient({ + url: 'http://localhost:4242/api', + fetch: async () => { + throw new Error('test error'); + }, + clientKey: '123', + appName: 'test', + }); + + await client.start(); + + const ui = ( + + + + ); + + render(ui); + + await waitFor(() => { + expect(screen.queryByText('flagsError')).toBeInTheDocument(); + }); + + expect(consoleError).toHaveBeenCalledWith( + 'Unleash: unable to fetch feature toggles', + expect.any(Error) + ); + consoleError.mockRestore(); +}); From f69a6b88d02524e071dc2de121d39feb4e2c53bc Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:15:47 +0200 Subject: [PATCH 10/11] fix: flags error type --- src/FlagProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FlagProvider.tsx b/src/FlagProvider.tsx index 4c43f9f..3e5a742 100644 --- a/src/FlagProvider.tsx +++ b/src/FlagProvider.tsx @@ -56,7 +56,7 @@ const FlagProvider: FC> = ({ const errorCallback = (e: any) => { startTransition(() => { - setFlagsError(currentError => currentError || e); + setFlagsError((currentError: any) => currentError || e); }); }; From a030bc6942617c0dec0c4c5ce3d3be699872b326 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:08:26 +0200 Subject: [PATCH 11/11] Apply suggestions from code review --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12d4486..58b08b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x, 21.x, 22.x] + node-version: [18.x, 20.x, 21.x] steps: - uses: actions/checkout@v2 @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: 📌 Install - run: yarn install --frozen-lockfile + run: yarn - name: 🔨 Build run: yarn build