Skip to content

Commit

Permalink
Add progress to Realm fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
gagik committed Jul 19, 2024
1 parent 13e70d7 commit 865cf14
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 9 deletions.
14 changes: 11 additions & 3 deletions packages/realm-react/src/RealmProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type PartialRealmConfiguration = Omit<Partial<Realm.Configuration>, "sync"> & {
sync?: Partial<Realm.SyncConfiguration>;
};

export type RealmFallbackComponent = React.ComponentType<{
progress: number | null;
}>;

/** Props used for a configuration-based Realm provider */
type RealmProviderConfigurationProps = {
/**
Expand All @@ -42,7 +46,7 @@ type RealmProviderConfigurationProps = {
/**
* The fallback component to render if the Realm is not open.
*/
fallback?: React.ComponentType<unknown> | React.ReactElement | null | undefined;
fallback?: RealmFallbackComponent | React.ComponentType | React.ReactElement | null | undefined;
children: React.ReactNode;
} & PartialRealmConfiguration;

Expand Down Expand Up @@ -158,14 +162,18 @@ export function createRealmProviderFromConfig(
}
}, [realm]);

const [progress, setProgress] = useState<number | null>(null);

useEffect(() => {
const realmRef = currentRealm.current;
// Check if we currently have an open Realm. If we do not (i.e. it is the first
// render, or the Realm has been closed due to a config change), then we
// need to open a new Realm.
const shouldInitRealm = realmRef === null;
const initRealm = async () => {
const openRealm = await Realm.open(configuration.current);
const openRealm = await Realm.open(configuration.current).progress((estimate: number) => {
setProgress(estimate);
});
setRealm(openRealm);
};
if (shouldInitRealm) {
Expand All @@ -184,7 +192,7 @@ export function createRealmProviderFromConfig(

if (!realm) {
if (typeof Fallback === "function") {
return <Fallback />;
return <Fallback progress={progress} />;
}
return <>{Fallback}</>;
}
Expand Down
55 changes: 49 additions & 6 deletions packages/realm-react/src/__tests__/RealmProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ import { Button, Text, View } from "react-native";
import { act, fireEvent, render, renderHook, waitFor } from "@testing-library/react-native";

import { RealmProvider, createRealmContext } from "..";
import { RealmProviderFromRealm, areConfigurationsIdentical, mergeRealmConfiguration } from "../RealmProvider";
import {
RealmFallbackComponent,
RealmProviderFromRealm,
areConfigurationsIdentical,
mergeRealmConfiguration,
} from "../RealmProvider";
import { randomRealmPath } from "./helpers";
import { RealmContext } from "../RealmContext";
import { mockRealmOpen } from "./mocks";

const dogSchema: Realm.ObjectSchema = {
name: "dog",
Expand Down Expand Up @@ -231,11 +237,16 @@ describe("RealmProvider", () => {

// TODO: Now that local realm is immediately set, the fallback never renders.
// We need to test synced realm in order to produce the fallback
describe.skip("initially renders a fallback, until realm exists", () => {
describe("initially renders a fallback, until realm exists", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("as a component", async () => {
const slowRealmOpen = mockRealmOpen();
const App = () => {
return (
<RealmProvider fallback={() => <View testID="fallbackContainer" />}>
<RealmProvider sync={{}} fallback={() => <View testID="fallbackContainer" />}>
<View testID="testContainer" />
</RealmProvider>
);
Expand All @@ -245,17 +256,46 @@ describe("RealmProvider", () => {
expect(queryByTestId("fallbackContainer")).not.toBeNull();
expect(queryByTestId("testContainer")).toBeNull();

await waitFor(() => queryByTestId("testContainer"));
await act(async () => await slowRealmOpen);

expect(queryByTestId("fallbackContainer")).toBeNull();
expect(queryByTestId("testContainer")).not.toBeNull();
});

it("as an element", async () => {
const slowRealmOpen = mockRealmOpen();

const Fallback = <View testID="fallbackContainer" />;
const App = () => {
return (
<RealmProvider fallback={Fallback}>
<RealmProvider sync={{}} fallback={Fallback}>
<View testID="testContainer" />
</RealmProvider>
);
};
const { queryByTestId } = render(<App />);

expect(queryByTestId("fallbackContainer")).not.toBeNull();
expect(queryByTestId("testContainer")).toBeNull();

await act(async () => await slowRealmOpen);

expect(queryByTestId("fallbackContainer")).toBeNull();
expect(queryByTestId("testContainer")).not.toBeNull();
});

it("should receive progress information", async () => {
const expectedProgressValues = [0, 0.25, 0.5, 0.75, 1];
const slowRealmOpen = mockRealmOpen({ progressValues: expectedProgressValues });
const renderedProgressValues: (number | null)[] = [];

const Fallback: RealmFallbackComponent = ({ progress }) => {
renderedProgressValues.push(progress);
return <View testID="fallbackContainer">{progress}</View>;
};
const App = () => {
return (
<RealmProvider sync={{}} fallback={Fallback}>
<View testID="testContainer" />
</RealmProvider>
);
Expand All @@ -264,11 +304,14 @@ describe("RealmProvider", () => {

expect(queryByTestId("fallbackContainer")).not.toBeNull();
expect(queryByTestId("testContainer")).toBeNull();
expect(renderedProgressValues).toStrictEqual([null, expectedProgressValues[0]]);

await waitFor(() => queryByTestId("testContainer"));
await act(async () => await slowRealmOpen);

expect(queryByTestId("fallbackContainer")).toBeNull();
expect(queryByTestId("testContainer")).not.toBeNull();

expect(renderedProgressValues).toStrictEqual([null, ...expectedProgressValues]);
});
});
});
Expand Down
94 changes: 94 additions & 0 deletions packages/realm-react/src/__tests__/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2024 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import { act } from "@testing-library/react-native";
import { EstimateProgressNotificationCallback, ProgressRealmPromise, Realm } from "realm";

/**
* Mocks {@link Realm.ProgressRealmPromise} with a custom
* promise completion and progress handler.
*/
export class MockedProgressRealmPromise extends Promise<Realm> implements ProgressRealmPromise {
private progressHandler?: (callback: EstimateProgressNotificationCallback) => void;

constructor(
callback: (resolve: (value: Realm) => void) => void,
options?: {
progress?: (callback: EstimateProgressNotificationCallback) => void;
},
) {
super(callback);
this.progressHandler = options?.progress;
}

get [Symbol.toStringTag]() {
return "MockedProgressRealmPromise";
}

cancel = () => this;

progress = (callback: EstimateProgressNotificationCallback) => {
this.progressHandler?.call(this, callback);
return this;
};
}

/**
* Mocks the Realm.open operation with a delayed, predictable Realm creation.
* If `options.progressValues` is specified, passes it through an equal interval to
* `Realm.open(...).progress(...)` callback.
* @returns Promise which resolves when the Realm is opened.
*/
export function mockRealmOpen(
options: {
/** Progress values which the `Realm.open(...).progress(...)` will receive in an equal interval. */
progressValues?: number[];
/** Duration of the Realm.open in milliseconds */
delay?: number;
} = {},
): MockedProgressRealmPromise {
const { progressValues, delay = 100 } = options;
let progressIndex = 0;

const progressRealmPromise = new MockedProgressRealmPromise(
(resolve) => {
setTimeout(() => resolve(new Realm()), delay);
},
{
progress: (callback) => {
if (progressValues instanceof Array) {
const sendProgress = () => {
// Uses act as this causes a component state update.
act(() => callback(progressValues[progressIndex]));
progressIndex++;

if (progressIndex <= progressValues.length) {
// Send the next progress update in equidistant time
setTimeout(sendProgress, delay / progressValues.length);
}
};
sendProgress();
}
},
},
);

const delayedRealmOpen = jest.spyOn(Realm, "open");
delayedRealmOpen.mockImplementation(() => progressRealmPromise);
return progressRealmPromise;
}

0 comments on commit 865cf14

Please sign in to comment.