Skip to content

Commit

Permalink
fix(react): issue with multiple context updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Billlynch committed Oct 13, 2023
1 parent 34e82b0 commit 2a2122b
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 209 deletions.
6 changes: 5 additions & 1 deletion packages/integration-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
"@spotify-confidence/openfeature-web-provider": "^0.0.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/node-fetch": "^2.6.4"
"@types/node-fetch": "^2.6.4",
"@types/use-sync-external-store": "^0.0.4"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"use-sync-external-store": "^1.2.0"
}
}
98 changes: 45 additions & 53 deletions packages/integration-react/src/ReactAdapter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { render, screen, act } from '@testing-library/react';
import { OpenFeatureClient, ProviderEvents } from '@openfeature/web-sdk';
import {
useStringValue,
ClientManager,
ClientManagerContext,
FeaturesStore,
FeatureStoreContext,
useNumberValue,
useObjectValue,
useBooleanValue,
Expand All @@ -28,20 +28,27 @@ const fakeClient = {
} as jest.MockedObject<OpenFeatureClient>;

describe('hooks', () => {
let fireReady: Function | undefined;
let fireError: Function | undefined;
let fireStale: Function | undefined;
const handlerMap = new Map<ProviderEvents, Set<Function>>();
const fire = (event: ProviderEvents) => {
const handlers = handlerMap.get(event);
if (handlers) {
for (const handler of handlers) {
handler();
}
}
};

beforeEach(() => {
fakeClient.addHandler.mockImplementation((event, cb) => {
if (event === ProviderEvents.Error) {
fireError = cb;
}
if (event === ProviderEvents.Stale) {
fireStale = cb;
}
if (event === ProviderEvents.Ready) {
fireReady = cb;
let handlers = handlerMap.get(event);
if (!handlers) handlerMap.set(event, (handlers = new Set()));
handlers.add(cb);
});
fakeClient.removeHandler.mockImplementation((event, cb) => {
const handlers = handlerMap.get(event);
if (handlers) {
handlers.delete(cb);
if (handlers.size === 0) handlerMap.delete(event);
}
});
});
Expand All @@ -66,31 +73,25 @@ describe('hooks', () => {
return <p>{JSON.stringify(val)}</p>;
};

const manager = new ClientManager(fakeClient);
manager.ref();
const featureStore = FeaturesStore.forClient(fakeClient);

const { unmount } = render(
<ClientManagerContext.Provider value={() => manager}>
<FeatureStoreContext.Provider value={featureStore}>
<React.Suspense fallback={<p>suspense</p>}>
<TestComponent />
</React.Suspense>
</ClientManagerContext.Provider>,
</FeatureStoreContext.Provider>,
);

expect(screen.getByText('suspense')).toBeInTheDocument();

await act(async () => {
fireReady!();
await manager.promise;
});
fire(ProviderEvents.Ready);

expect(screen.getByText(JSON.stringify(mockValue))).toBeInTheDocument();
expect(await screen.findByText(JSON.stringify(mockValue))).toBeInTheDocument();

unmount();

manager.unref();
expect(fakeClient.addHandler).toBeCalledTimes(3);
expect(fakeClient.removeHandler).toBeCalledTimes(3);
expect(handlerMap.size).toBe(0);
},
);
});
Expand All @@ -109,44 +110,39 @@ describe('hooks', () => {
`(
`($name): should show suspense until the value is resolved and reload when the context is changed, show new value and cleanup`,
async ({ hookUnderTest, defaultValue, mockValue1, mockValue2, mockFunc }) => {
mockFunc.mockReturnValueOnce(mockValue1).mockReturnValueOnce(mockValue2);
mockFunc.mockReturnValue(mockValue1);
const TestComponent = () => {
const val = hookUnderTest('flag', defaultValue);
return <p>{JSON.stringify(val)}</p>;
};

const manager = new ClientManager(fakeClient);
manager.ref();
const featureStore = FeaturesStore.forClient(fakeClient);

const { unmount } = render(
<ClientManagerContext.Provider value={() => manager}>
<FeatureStoreContext.Provider value={featureStore}>
<React.Suspense fallback={<p>suspense</p>}>
<TestComponent />
</React.Suspense>
</ClientManagerContext.Provider>,
</FeatureStoreContext.Provider>,
);

expect(screen.getByText('suspense')).toBeInTheDocument();

await act(async () => {
fireReady!();
await manager.promise;
});
fire(ProviderEvents.Ready);

expect(screen.getByText(JSON.stringify(mockValue1))).toBeInTheDocument();
expect(await screen.findByText(JSON.stringify(mockValue1))).toBeInTheDocument();

await act(async () => {
fireStale!();
fireReady!();
act(() => {
fire(ProviderEvents.Stale);
mockFunc.mockReturnValue(mockValue2);
fire(ProviderEvents.Ready);
});

expect(screen.getByText(JSON.stringify(mockValue2))).toBeInTheDocument();
expect(await screen.findByText(JSON.stringify(mockValue2))).toBeInTheDocument();

unmount();

manager.unref();
expect(fakeClient.addHandler).toBeCalledTimes(3);
expect(fakeClient.removeHandler).toBeCalledTimes(3);
expect(handlerMap.size).toBe(0);
},
);
});
Expand All @@ -169,31 +165,27 @@ describe('hooks', () => {
return <p>{JSON.stringify(val)}</p>;
};

const manager = new ClientManager(fakeClient);
manager.ref();
const featureStore = FeaturesStore.forClient(fakeClient);

const { unmount } = render(
<ClientManagerContext.Provider value={() => manager}>
<FeatureStoreContext.Provider value={featureStore}>
<React.Suspense fallback={<p>suspense</p>}>
<TestComponent />
</React.Suspense>
</ClientManagerContext.Provider>,
</FeatureStoreContext.Provider>,
);

expect(screen.getByText('suspense')).toBeInTheDocument();

await act(async () => {
fireError!();
await manager.promise;
act(() => {
fire(ProviderEvents.Error);
});

expect(screen.getByText(JSON.stringify(defaultValue))).toBeInTheDocument();
expect(await screen.findByText(JSON.stringify(defaultValue))).toBeInTheDocument();

unmount();

manager.unref();
expect(fakeClient.addHandler).toBeCalledTimes(3);
expect(fakeClient.removeHandler).toBeCalledTimes(3);
expect(handlerMap.size).toBe(0);
});
});
});
Loading

0 comments on commit 2a2122b

Please sign in to comment.