Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix race conditions in UseIModelData #136

Merged
merged 13 commits into from
Jan 9, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/imodel-browser-react",
"comment": "Fixed race conditions in useIModelData",
"type": "patch"
}
],
"packageName": "@itwin/imodel-browser-react"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { renderHook } from "@testing-library/react-hooks";
import { rest } from "msw";
import { act } from "react";

import { server } from "../../tests/mocks/server";
import { DataStatus, IModelSortOptionsKeys } from "../../types";
Expand Down Expand Up @@ -219,4 +220,146 @@ describe("useIModelData hook", () => {
opts
);
});

it("returns properly paged iModels", async () => {
const fetchSpy = jest.spyOn(window, "fetch");

let callNum = 0;
const mockIModels = Array.from({ length: 101 }, (_, i) => ({
id: `fakeId${i + 1}`,
displayName: `fakeName${i + 1}`,
}));

const watcher = jest.fn();
server.use(
rest.get("https://api.bentley.com/imodels/", (req, res, ctx) => {
watcher();
return res(
ctx.json({
iModels:
++callNum === 1
? mockIModels.slice(0, 100)
: mockIModels.slice(100),
})
);
})
);

const { result, waitForValueToChange } = renderHook(() =>
useIModelData({
iTwinId: "iTwinId",
accessToken: "accessToken",
Dismissed Show dismissed Hide dismissed
})
);

await waitForValueToChange(
() => result.current.status === DataStatus.Complete
);

expect(fetchSpy).toHaveBeenLastCalledWith(
expect.stringContaining(`$skip=0`) && expect.stringContaining(`$top=100`),
expect.any(Object)
);
expect(result.current.iModels.length).toBe(100);
expect(result.current.fetchMore).toBeDefined();
expect(watcher).toHaveBeenCalledTimes(1);

act(() => {
result.current.fetchMore?.();
});
await waitForValueToChange(
() => result.current.status === DataStatus.Complete
);

expect(fetchSpy).toHaveBeenLastCalledWith(
expect.stringContaining(`$skip=100`) &&
expect.stringContaining(`$top=100`),
expect.any(Object)
);
expect(result.current.iModels.length).toBe(101);
expect(result.current.fetchMore).toBeUndefined();
expect(watcher).toHaveBeenCalledTimes(2);
});
});

it("fetches data with searchText", async () => {
const fetchSpy = jest.spyOn(window, "fetch").mockImplementation(
() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ iModels: [] }),
}) as any
);
const searchText = "testSearch";

const { result, waitForNextUpdate } = renderHook(() =>
useIModelData({
iTwinId: "iTwinId",
accessToken: "accessToken",
Dismissed Show dismissed Hide dismissed
searchText,
})
);

await waitForNextUpdate();

expect(result.current.status).toEqual(DataStatus.Complete);
expect(fetchSpy).toHaveBeenCalledWith(
expect.stringContaining(`&$search=${searchText}`),
expect.any(Object)
);
});

it("fetches data with serverEnvironmentPrefix", async () => {
const fetchSpy = jest.spyOn(window, "fetch").mockImplementation(
() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ iModels: [] }),
}) as any
);
const serverEnvironmentPrefix = "dev";

const { result, waitForNextUpdate } = renderHook(() =>
useIModelData({
iTwinId: "iTwinId",
accessToken: "accessToken",
Dismissed Show dismissed Hide dismissed
apiOverrides: {
serverEnvironmentPrefix,
},
})
);

await waitForNextUpdate();

expect(result.current.status).toEqual(DataStatus.Complete);
expect(fetchSpy).toHaveBeenCalledWith(
expect.stringContaining(`${serverEnvironmentPrefix}`),
expect.any(Object)
);
});

it("aborts previous fetch when new fetch is initiated", async () => {
jest.spyOn(window, "fetch").mockImplementation(
() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ iModels: [] }),
}) as any
);
const abortSpy = jest.spyOn(AbortController.prototype, "abort");

const { rerender, waitForNextUpdate } = renderHook(
({ iTwinId, accessToken }) =>
useIModelData({
iTwinId,
accessToken,
}),
{
initialProps: { iTwinId: "iTwinId1", accessToken: "accessToken" },
}
);

rerender({ iTwinId: "iTwinId2", accessToken: "accessToken" });
await waitForNextUpdate();
expect(abortSpy).toHaveBeenCalled();
});
Loading
Loading