Skip to content

Commit

Permalink
fix: make sure flagsReady doesn't go before isEnabled (#105)
Browse files Browse the repository at this point in the history
* fix: make sure flagsReady doesn't go before isEnabled

* add new integration tests

* update provider config setup
  • Loading branch information
Tymek authored Feb 15, 2023
1 parent 88a8b09 commit 0bc6f64
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 28 deletions.
25 changes: 20 additions & 5 deletions src/FlagProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface IFlagProvider {
startClient?: boolean;
}

const offlineConfig = {
const offlineConfig: IConfig = {
bootstrap: [],
disableRefresh: true,
disableMetrics: true,
Expand All @@ -20,15 +20,22 @@ const offlineConfig = {
};

const FlagProvider: React.FC<React.PropsWithChildren<IFlagProvider>> = ({
config,
config: customConfig,
children,
unleashClient,
startClient = true,
}) => {
const config = customConfig || offlineConfig;
const client = React.useRef<UnleashClient>(
unleashClient || new UnleashClient(config || offlineConfig)
unleashClient || new UnleashClient(config)
);
const [flagsReady, setFlagsReady] = React.useState(
Boolean(
unleashClient
? customConfig?.bootstrap && customConfig?.bootstrapOverride !== false
: config.bootstrap && config.bootstrapOverride !== false
)
);
const [flagsReady, setFlagsReady] = React.useState(false);
const [flagsError, setFlagsError] = React.useState(null);
const flagsErrorRef = React.useRef(null);

Expand All @@ -49,8 +56,13 @@ const FlagProvider: React.FC<React.PropsWithChildren<IFlagProvider>> = ({
setFlagsError(e);
}
};

let timeout: any;
const readyCallback = () => {
setFlagsReady(true);
// wait for flags to resolve after useFlag gets the same event
timeout = setTimeout(() => {
setFlagsReady(true);
}, 0);
};

client.current.on('ready', readyCallback);
Expand All @@ -71,6 +83,9 @@ const FlagProvider: React.FC<React.PropsWithChildren<IFlagProvider>> = ({
client.current.off('ready', readyCallback);
client.current.stop();
}
if (timeout) {
clearTimeout(timeout);
}
};
}, []);

Expand Down
225 changes: 202 additions & 23 deletions src/integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* @jest-environment jsdom
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import React, { useContext } from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { EVENTS, UnleashClient } from 'unleash-proxy-client';
import '@testing-library/jest-dom';

Expand All @@ -13,31 +13,32 @@ import useFlagsStatus from './useFlagsStatus';
import { act } from 'react-dom/test-utils';
import useFlag from './useFlag';
import useVariant from './useVariant';
import FlagContext from './FlagContext';

test('should render toggles', async () => {
const fetchMock = jest.fn(() => {
return Promise.resolve({
ok: true,
status: 200,
headers: new Headers({}),
json: () => {
return Promise.resolve({
toggles: [
{
name: 'test-flag',
const fetchMock = jest.fn(() => {
return Promise.resolve({
ok: true,
status: 200,
headers: new Headers({}),
json: () => {
return Promise.resolve({
toggles: [
{
name: 'test-flag',
enabled: true,
variant: {
name: 'A',
payload: { type: 'string', value: 'A' },
enabled: true,
variant: {
name: 'A',
payload: { type: 'string', value: 'A' },
enabled: true,
},
},
],
});
},
});
},
],
});
},
});
});

test('should render toggles', async () => {
const client = new UnleashClient({
url: 'http://localhost:4242/api/frontend',
appName: 'test',
Expand Down Expand Up @@ -77,7 +78,9 @@ test('should render toggles', async () => {
await act(
() =>
new Promise((resolve) => {
client.on(EVENTS.READY, resolve);
client.on(EVENTS.READY, () => {
setTimeout(resolve, 1);
});
})
);

Expand All @@ -90,3 +93,179 @@ test('should render toggles', async () => {
'{"name":"A","payload":{"type":"string","value":"A"},"enabled":true}'
);
});

test('should be ready from the start if bootstrapped', () => {
const Component = React.memo(() => {
const { flagsReady } = useContext(FlagContext);

return <>{flagsReady ? 'ready' : ''}</>;
});

render(
<FlagProvider
config={{
url: 'http://localhost:4242/api/frontend',
appName: 'test',
clientKey: 'test',
bootstrap: [
{
name: 'test',
enabled: true,
variant: {
name: 'A',
enabled: true,
payload: { type: 'string', value: 'A' },
},
impressionData: false,
},
],
fetch: fetchMock,
}}
startClient={false}
>
<Component />
</FlagProvider>
);

expect(screen.getByText('ready')).toBeInTheDocument();
});

test('should immediately return value if boostrapped', () => {
const Component = () => {
const enabled = useFlag('test-flag');

return <>{enabled ? 'enabled' : ''}</>;
};

render(
<FlagProvider
config={{
url: 'http://localhost:4242/api/frontend',
appName: 'test',
clientKey: 'test',
bootstrap: [
{
name: 'test-flag',
enabled: true,
variant: {
name: 'A',
enabled: true,
payload: { type: 'string', value: 'A' },
},
impressionData: false,
},
],
fetch: fetchMock,
}}
startClient={false}
>
<Component />
</FlagProvider>
);

expect(screen.queryByText('enabled')).toBeInTheDocument();
});

test('should render limited times when bootstrapped', async () => {
let renders = 0;
const config = {
url: 'http://localhost:4242/api/frontend',
appName: 'test',
clientKey: 'test',
bootstrap: [
{
name: 'test-flag',
enabled: true,
variant: {
name: 'A',
enabled: true,
payload: { type: 'string', value: 'A' },
},
impressionData: false,
},
],
fetch: fetchMock,
};
const client = new UnleashClient(config);

const Component = () => {
const enabled = useFlag('test-flag');
const { flagsReady } = useContext(FlagContext);

renders += 1;

return (
<>
<span>{flagsReady ? 'flagsReady' : ''}</span>
<span>{enabled ? 'enabled' : ''}</span>
</>
);
};

render(
<FlagProvider unleashClient={client} config={config}>
<Component />
</FlagProvider>
);

expect(screen.queryByText('enabled')).toBeInTheDocument();
expect(screen.queryByText('flagsReady')).toBeInTheDocument();
expect(renders).toBe(1);

// Wait for client initialization
await act(
() =>
new Promise((resolve) => {
client.on(EVENTS.READY, () => {
setTimeout(resolve, 1);
});
})
);

expect(renders).toBe(1);
});

test('should resolve values before setting flagsReady', async () => {
const client = new UnleashClient({
url: 'http://localhost:4242/api/frontend',
appName: 'test',
clientKey: 'test',
fetch: fetchMock,
});
let renders = 0;

const Component = () => {
const enabled = useFlag('test-flag');
const { flagsReady } = useContext(FlagContext);

renders += 1;

return (
<>
<span>{flagsReady ? 'flagsReady' : ''}</span>
<span>{enabled ? 'enabled' : ''}</span>
</>
);
};

const ui = (
<FlagProvider unleashClient={client}>
<Component />
</FlagProvider>
);

render(ui);
expect(renders).toBe(1);
expect(screen.queryByText('flagsReady')).not.toBeInTheDocument();
expect(screen.queryByText('enabled')).not.toBeInTheDocument();
await waitFor(() =>
expect(screen.queryByText('enabled')).toBeInTheDocument()
);
expect(screen.queryByText('flagsReady')).toBeNull();
expect(renders).toBe(2);
await waitFor(() =>
expect(screen.queryByText('flagsReady')).toBeInTheDocument()
);
expect(screen.queryByText('enabled')).toBeInTheDocument();
expect(renders).toBe(3);
});

0 comments on commit 0bc6f64

Please sign in to comment.