From 865cf14f2daa71a3cb94e37b5041c8328b100753 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 19 Jul 2024 15:47:46 +0200 Subject: [PATCH 1/9] Add progress to Realm fallback --- packages/realm-react/src/RealmProvider.tsx | 14 ++- .../src/__tests__/RealmProvider.test.tsx | 55 +++++++++-- packages/realm-react/src/__tests__/mocks.ts | 94 +++++++++++++++++++ 3 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 packages/realm-react/src/__tests__/mocks.ts diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index f70e6d6198..fae9d16271 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -27,6 +27,10 @@ type PartialRealmConfiguration = Omit, "sync"> & { sync?: Partial; }; +export type RealmFallbackComponent = React.ComponentType<{ + progress: number | null; +}>; + /** Props used for a configuration-based Realm provider */ type RealmProviderConfigurationProps = { /** @@ -42,7 +46,7 @@ type RealmProviderConfigurationProps = { /** * The fallback component to render if the Realm is not open. */ - fallback?: React.ComponentType | React.ReactElement | null | undefined; + fallback?: RealmFallbackComponent | React.ComponentType | React.ReactElement | null | undefined; children: React.ReactNode; } & PartialRealmConfiguration; @@ -158,6 +162,8 @@ export function createRealmProviderFromConfig( } }, [realm]); + const [progress, setProgress] = useState(null); + useEffect(() => { const realmRef = currentRealm.current; // Check if we currently have an open Realm. If we do not (i.e. it is the first @@ -165,7 +171,9 @@ export function createRealmProviderFromConfig( // 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) { @@ -184,7 +192,7 @@ export function createRealmProviderFromConfig( if (!realm) { if (typeof Fallback === "function") { - return ; + return ; } return <>{Fallback}; } diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx index f43112ab58..28df63d876 100644 --- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx +++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx @@ -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", @@ -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 ( - }> + }> ); @@ -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 = ; const App = () => { return ( - + + + + ); + }; + const { queryByTestId } = render(); + + 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 {progress}; + }; + const App = () => { + return ( + ); @@ -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]); }); }); }); diff --git a/packages/realm-react/src/__tests__/mocks.ts b/packages/realm-react/src/__tests__/mocks.ts new file mode 100644 index 0000000000..b1feb2de18 --- /dev/null +++ b/packages/realm-react/src/__tests__/mocks.ts @@ -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 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; +} From 867925a83927b572fc44f53a24d03038d3f7a15e Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 19 Jul 2024 15:59:05 +0200 Subject: [PATCH 2/9] Add changelog --- packages/realm-react/CHANGELOG.md | 17 ++++++++++++++++- packages/realm-react/src/RealmProvider.tsx | 4 ++-- .../src/__tests__/RealmProvider.test.tsx | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/realm-react/CHANGELOG.md b/packages/realm-react/CHANGELOG.md index f0e663638e..aca36fa1cd 100644 --- a/packages/realm-react/CHANGELOG.md +++ b/packages/realm-react/CHANGELOG.md @@ -4,7 +4,22 @@ * None ### Enhancements -* None +* Added the ability to get `progress` information in `fallback` component of `RealmProvider` when opening a synced Realm. ([#6785](https://github.com/realm/realm-js/issues/6785)) +```tsx +import { RealmProvider, RealmProviderFallback } from "@realm/react"; + +const Fallback: RealmProviderFallback = ({ progress }) => { + return Loading: {progress}/1.0; +} + +const MyApp() = () => { + return ( + + ... + + ); +} +``` ### Fixed * ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?) diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index fae9d16271..927ce45860 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -27,7 +27,7 @@ type PartialRealmConfiguration = Omit, "sync"> & { sync?: Partial; }; -export type RealmFallbackComponent = React.ComponentType<{ +export type RealmProviderFallback = React.ComponentType<{ progress: number | null; }>; @@ -46,7 +46,7 @@ type RealmProviderConfigurationProps = { /** * The fallback component to render if the Realm is not open. */ - fallback?: RealmFallbackComponent | React.ComponentType | React.ReactElement | null | undefined; + fallback?: RealmProviderFallback | React.ComponentType | React.ReactElement | null | undefined; children: React.ReactNode; } & PartialRealmConfiguration; diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx index 28df63d876..df0def89be 100644 --- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx +++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx @@ -23,7 +23,7 @@ import { act, fireEvent, render, renderHook, waitFor } from "@testing-library/re import { RealmProvider, createRealmContext } from ".."; import { - RealmFallbackComponent, + RealmProviderFallback, RealmProviderFromRealm, areConfigurationsIdentical, mergeRealmConfiguration, @@ -289,7 +289,7 @@ describe("RealmProvider", () => { const slowRealmOpen = mockRealmOpen({ progressValues: expectedProgressValues }); const renderedProgressValues: (number | null)[] = []; - const Fallback: RealmFallbackComponent = ({ progress }) => { + const Fallback: RealmProviderFallback = ({ progress }) => { renderedProgressValues.push(progress); return {progress}; }; From 3ec2d0fad05c106d3bafd0d03f53b9b64e5a02cf Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Tue, 23 Jul 2024 10:44:46 +0200 Subject: [PATCH 3/9] Add version warning --- packages/realm-react/src/RealmProvider.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index 927ce45860..39d360cc4f 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -171,9 +171,17 @@ export function createRealmProviderFromConfig( // need to open a new Realm. const shouldInitRealm = realmRef === null; const initRealm = async () => { - const openRealm = await Realm.open(configuration.current).progress((estimate: number) => { - setProgress(estimate); - }); + const openRealmPromise = Realm.open(configuration.current); + if (configuration.current.sync?.flexible) { + try { + openRealmPromise.progress((estimate: number) => { + setProgress(estimate); + }); + } catch (error) { + console.warn("Progress information with @realm/react work with realm version >=12.12.0."); + } + } + const openRealm = await openRealmPromise; setRealm(openRealm); }; if (shouldInitRealm) { From 437725f397d6f5cb7c9eb85867e32732d0b94648 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Tue, 23 Jul 2024 10:49:45 +0200 Subject: [PATCH 4/9] Update RealmProvider.tsx --- packages/realm-react/src/RealmProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index 39d360cc4f..fb86d6a57e 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -178,7 +178,7 @@ export function createRealmProviderFromConfig( setProgress(estimate); }); } catch (error) { - console.warn("Progress information with @realm/react work with realm version >=12.12.0."); + console.warn("Progress information with @realm/react work only with realm version >=12.12.0."); } } const openRealm = await openRealmPromise; From a0180895b494fedddfcb57b1ef8a3901fc4db5f1 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Tue, 23 Jul 2024 12:29:18 +0200 Subject: [PATCH 5/9] change compatibility --- COMPATIBILITY.md | 1 + packages/realm-react/CHANGELOG.md | 1 + packages/realm-react/package.json | 2 +- packages/realm-react/src/RealmProvider.tsx | 10 +++------- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index ab6980b9ac..056d0ea465 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -2,6 +2,7 @@ | Realm JavaScript | @realm/react | React Native | Expo | Hermes | npm | node | |---------------------|-------------------|--------------------|----------|--------|--------|--------| +| 12.12.0 | >= 0.10.0 | >= 0.71.4 | >= 48 | ✅ | >= 10 | >= 20 | | 12.6.0 | >= 0.5.0 | >= 0.71.4 | >= 48 | ✅ | >= 10 | >= 20 | | 12.5.1 | >= 0.5.0 | >= 0.71.4 | >= 48 | ✅ | >= 7 | >= 13 | | 12.5.0 | >= 0.5.0 | >= 0.71.4 | >= 48 | ✅ | >= 7 | >= 13 | diff --git a/packages/realm-react/CHANGELOG.md b/packages/realm-react/CHANGELOG.md index aca36fa1cd..74de71b842 100644 --- a/packages/realm-react/CHANGELOG.md +++ b/packages/realm-react/CHANGELOG.md @@ -26,6 +26,7 @@ const MyApp() = () => { * None ### Compatibility +* Realm JavaScript >= v12.12.0 * React Native >= v0.71.4 * Realm Studio v15.0.0. * File format: generates Realms with format v24 (reads and upgrades file format v10). diff --git a/packages/realm-react/package.json b/packages/realm-react/package.json index 5f6f2eba49..305824956c 100644 --- a/packages/realm-react/package.json +++ b/packages/realm-react/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "react": ">=17.0.2", - "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0" + "realm": ">=12.12.0" }, "optionalDependencies": { "@babel/runtime": ">=7", diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index fb86d6a57e..8a18cc36f4 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -173,13 +173,9 @@ export function createRealmProviderFromConfig( const initRealm = async () => { const openRealmPromise = Realm.open(configuration.current); if (configuration.current.sync?.flexible) { - try { - openRealmPromise.progress((estimate: number) => { - setProgress(estimate); - }); - } catch (error) { - console.warn("Progress information with @realm/react work only with realm version >=12.12.0."); - } + openRealmPromise.progress((estimate: number) => { + setProgress(estimate); + }); } const openRealm = await openRealmPromise; setRealm(openRealm); From a98a1cd3f0da18011a3a699f0e619d770edd4ea9 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Tue, 23 Jul 2024 12:44:03 +0200 Subject: [PATCH 6/9] Return to old change --- packages/realm-react/src/RealmProvider.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index 8a18cc36f4..fae9d16271 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -27,7 +27,7 @@ type PartialRealmConfiguration = Omit, "sync"> & { sync?: Partial; }; -export type RealmProviderFallback = React.ComponentType<{ +export type RealmFallbackComponent = React.ComponentType<{ progress: number | null; }>; @@ -46,7 +46,7 @@ type RealmProviderConfigurationProps = { /** * The fallback component to render if the Realm is not open. */ - fallback?: RealmProviderFallback | React.ComponentType | React.ReactElement | null | undefined; + fallback?: RealmFallbackComponent | React.ComponentType | React.ReactElement | null | undefined; children: React.ReactNode; } & PartialRealmConfiguration; @@ -171,13 +171,9 @@ export function createRealmProviderFromConfig( // need to open a new Realm. const shouldInitRealm = realmRef === null; const initRealm = async () => { - const openRealmPromise = Realm.open(configuration.current); - if (configuration.current.sync?.flexible) { - openRealmPromise.progress((estimate: number) => { - setProgress(estimate); - }); - } - const openRealm = await openRealmPromise; + const openRealm = await Realm.open(configuration.current).progress((estimate: number) => { + setProgress(estimate); + }); setRealm(openRealm); }; if (shouldInitRealm) { From 1ad08db13f33736fb5d8dc05e6f38d3c2751909e Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 2 Aug 2024 16:09:28 +0200 Subject: [PATCH 7/9] Changes from review --- COMPATIBILITY.md | 1 - packages/realm-react/CHANGELOG.md | 2 +- packages/realm-react/src/RealmProvider.tsx | 8 +- .../src/__tests__/RealmProvider.test.tsx | 12 +- packages/realm-react/src/__tests__/mocks.ts | 106 ++++++++++++------ 5 files changed, 82 insertions(+), 47 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 056d0ea465..ab6980b9ac 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -2,7 +2,6 @@ | Realm JavaScript | @realm/react | React Native | Expo | Hermes | npm | node | |---------------------|-------------------|--------------------|----------|--------|--------|--------| -| 12.12.0 | >= 0.10.0 | >= 0.71.4 | >= 48 | ✅ | >= 10 | >= 20 | | 12.6.0 | >= 0.5.0 | >= 0.71.4 | >= 48 | ✅ | >= 10 | >= 20 | | 12.5.1 | >= 0.5.0 | >= 0.71.4 | >= 48 | ✅ | >= 7 | >= 13 | | 12.5.0 | >= 0.5.0 | >= 0.71.4 | >= 48 | ✅ | >= 7 | >= 13 | diff --git a/packages/realm-react/CHANGELOG.md b/packages/realm-react/CHANGELOG.md index 74de71b842..a30c3bf55c 100644 --- a/packages/realm-react/CHANGELOG.md +++ b/packages/realm-react/CHANGELOG.md @@ -9,7 +9,7 @@ import { RealmProvider, RealmProviderFallback } from "@realm/react"; const Fallback: RealmProviderFallback = ({ progress }) => { - return Loading: {progress}/1.0; + return Loading:{(100 * progress).toFixed()}%; } const MyApp() = () => { diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index fae9d16271..29162c1be7 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -27,8 +27,8 @@ type PartialRealmConfiguration = Omit, "sync"> & { sync?: Partial; }; -export type RealmFallbackComponent = React.ComponentType<{ - progress: number | null; +export type RealmProviderFallback = React.ComponentType<{ + progress: number; }>; /** Props used for a configuration-based Realm provider */ @@ -46,7 +46,7 @@ type RealmProviderConfigurationProps = { /** * The fallback component to render if the Realm is not open. */ - fallback?: RealmFallbackComponent | React.ComponentType | React.ReactElement | null | undefined; + fallback?: RealmProviderFallback | React.ComponentType | React.ReactElement | null | undefined; children: React.ReactNode; } & PartialRealmConfiguration; @@ -162,7 +162,7 @@ export function createRealmProviderFromConfig( } }, [realm]); - const [progress, setProgress] = useState(null); + const [progress, setProgress] = useState(0); useEffect(() => { const realmRef = currentRealm.current; diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx index df0def89be..490ac7f1c4 100644 --- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx +++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx @@ -30,7 +30,7 @@ import { } from "../RealmProvider"; import { randomRealmPath } from "./helpers"; import { RealmContext } from "../RealmContext"; -import { mockRealmOpen } from "./mocks"; +import { MockedProgressRealmPromiseWithDelay, mockRealmOpen } from "./mocks"; const dogSchema: Realm.ObjectSchema = { name: "dog", @@ -286,8 +286,10 @@ describe("RealmProvider", () => { 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 slowRealmOpen = mockRealmOpen( + new MockedProgressRealmPromiseWithDelay({ progressValues: expectedProgressValues }), + ); + const renderedProgressValues: number[] = []; const Fallback: RealmProviderFallback = ({ progress }) => { renderedProgressValues.push(progress); @@ -304,14 +306,14 @@ describe("RealmProvider", () => { expect(queryByTestId("fallbackContainer")).not.toBeNull(); expect(queryByTestId("testContainer")).toBeNull(); - expect(renderedProgressValues).toStrictEqual([null, expectedProgressValues[0]]); + expect(renderedProgressValues).toStrictEqual([expectedProgressValues[0]]); await act(async () => await slowRealmOpen); expect(queryByTestId("fallbackContainer")).toBeNull(); expect(queryByTestId("testContainer")).not.toBeNull(); - expect(renderedProgressValues).toStrictEqual([null, ...expectedProgressValues]); + expect(renderedProgressValues).toStrictEqual(expectedProgressValues); }); }); }); diff --git a/packages/realm-react/src/__tests__/mocks.ts b/packages/realm-react/src/__tests__/mocks.ts index b1feb2de18..ee5efb719b 100644 --- a/packages/realm-react/src/__tests__/mocks.ts +++ b/packages/realm-react/src/__tests__/mocks.ts @@ -25,69 +25,103 @@ import { EstimateProgressNotificationCallback, ProgressRealmPromise, Realm } fro */ export class MockedProgressRealmPromise extends Promise implements ProgressRealmPromise { private progressHandler?: (callback: EstimateProgressNotificationCallback) => void; + private cancelHandler?: () => void; constructor( callback: (resolve: (value: Realm) => void) => void, options?: { progress?: (callback: EstimateProgressNotificationCallback) => void; + cancel?: () => void; }, ) { super(callback); this.progressHandler = options?.progress; + this.cancelHandler = options?.cancel; } get [Symbol.toStringTag]() { return "MockedProgressRealmPromise"; } - cancel = () => this; + cancel = () => { + if (!this.cancelHandler) { + throw new Error("cancel handler not set"); + } + this.cancelHandler(); + }; progress = (callback: EstimateProgressNotificationCallback) => { - this.progressHandler?.call(this, callback); + if (!this.progressHandler) { + throw new Error("progress handler not set"); + } + this.progressHandler(callback); return this; }; } +/** + * Mocked {@link ProgressRealmPromise} which resolves after a set delay. + * If `options.progressValues` is specified, passes it through an + * equal interval to `Realm.open(...).progress(...)` callback. + */ +export class MockedProgressRealmPromiseWithDelay extends MockedProgressRealmPromise { + public currentProgressIndex = 0; + public progressValues: number[] | undefined; + private progressTimeout: NodeJS.Timeout | undefined; + + constructor( + options: { + delay?: number; + /** Progress values which the `Realm.open(...).progress(...)` will receive in an equal interval. */ + progressValues?: number[]; + } = {}, + ) { + const { progressValues, delay = 100 } = options; + super( + (resolve) => { + setTimeout(() => resolve(new Realm()), delay); + }, + { + progress: (callback) => { + callMockedProgressNotifications(callback, delay, progressValues); + }, + cancel: () => clearTimeout(this.progressTimeout), + }, + ); + this.progressValues = progressValues; + } +} + +/** Calls given callbacks with progressValues in an equal interval */ +export function callMockedProgressNotifications( + callback: EstimateProgressNotificationCallback, + timeFrame: number, + progressValues: number[] = [0, 0.25, 0.5, 0.75, 1], +): NodeJS.Timeout { + let progressIndex = 0; + let progressInterval: NodeJS.Timeout | undefined = undefined; + 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 + clearInterval(progressInterval); + } + }; + progressInterval = setInterval(sendProgress, timeFrame / (progressValues.length + 1)); + sendProgress(); + return progressInterval; +} + /** * 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; - } = {}, + progressRealmPromise: MockedProgressRealmPromise = new MockedProgressRealmPromiseWithDelay(), ): 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; From f0d858661d78cc9589cb26b969d36e2984e0afd7 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Mon, 5 Aug 2024 10:34:00 +0200 Subject: [PATCH 8/9] use restore --- packages/realm-react/src/__tests__/RealmProvider.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx index 490ac7f1c4..d6cc4d94f9 100644 --- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx +++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx @@ -239,7 +239,7 @@ describe("RealmProvider", () => { // We need to test synced realm in order to produce the fallback describe("initially renders a fallback, until realm exists", () => { afterEach(() => { - jest.clearAllMocks(); + jest.restoreAllMocks(); }); it("as a component", async () => { From 5c82baa4066d9cfbb7d20bc3c48fbc8abf0b6b24 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Mon, 5 Aug 2024 11:19:18 +0200 Subject: [PATCH 9/9] Fix promise extension --- packages/realm-react/src/__tests__/mocks.ts | 26 +++++++++++++++++---- packages/realm-react/src/helpers.ts | 9 +++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/realm-react/src/__tests__/mocks.ts b/packages/realm-react/src/__tests__/mocks.ts index ee5efb719b..ad15cb17e0 100644 --- a/packages/realm-react/src/__tests__/mocks.ts +++ b/packages/realm-react/src/__tests__/mocks.ts @@ -18,6 +18,7 @@ import { act } from "@testing-library/react-native"; import { EstimateProgressNotificationCallback, ProgressRealmPromise, Realm } from "realm"; +import { sleep } from "../helpers"; /** * Mocks {@link Realm.ProgressRealmPromise} with a custom @@ -26,15 +27,22 @@ import { EstimateProgressNotificationCallback, ProgressRealmPromise, Realm } fro export class MockedProgressRealmPromise extends Promise implements ProgressRealmPromise { private progressHandler?: (callback: EstimateProgressNotificationCallback) => void; private cancelHandler?: () => void; + private realmPromise!: Promise; constructor( - callback: (resolve: (value: Realm) => void) => void, + getRealm: () => Promise, options?: { progress?: (callback: EstimateProgressNotificationCallback) => void; cancel?: () => void; }, ) { - super(callback); + let realmPromise: Promise; + super((resolve) => { + realmPromise = getRealm(); + realmPromise.then((realm) => resolve(realm)); + }); + // @ts-expect-error realmPromise value will be assigned right away + this.realmPromise = realmPromise; this.progressHandler = options?.progress; this.cancelHandler = options?.cancel; } @@ -50,6 +58,13 @@ export class MockedProgressRealmPromise extends Promise implements Progre this.cancelHandler(); }; + then( + onfulfilled?: ((value: Realm) => TResult1 | PromiseLike) | null | undefined, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null | undefined, + ): Promise { + return this.realmPromise.then(onfulfilled, onrejected); + } + progress = (callback: EstimateProgressNotificationCallback) => { if (!this.progressHandler) { throw new Error("progress handler not set"); @@ -78,12 +93,13 @@ export class MockedProgressRealmPromiseWithDelay extends MockedProgressRealmProm ) { const { progressValues, delay = 100 } = options; super( - (resolve) => { - setTimeout(() => resolve(new Realm()), delay); + async () => { + await sleep(delay); + return new Realm(); }, { progress: (callback) => { - callMockedProgressNotifications(callback, delay, progressValues); + this.progressTimeout = callMockedProgressNotifications(callback, delay, progressValues); }, cancel: () => clearTimeout(this.progressTimeout), }, diff --git a/packages/realm-react/src/helpers.ts b/packages/realm-react/src/helpers.ts index ed6c2d8d24..8158443349 100644 --- a/packages/realm-react/src/helpers.ts +++ b/packages/realm-react/src/helpers.ts @@ -58,3 +58,12 @@ export type RestrictivePick = Pick & { [RestrictedKe export function isClassModelConstructor(value: unknown): value is RealmClassType { return Object.getPrototypeOf(value) === Realm.Object; } + +/** + * Adapted from integration-tests + * @param ms For how long should the promise be pending? + * @returns A promise that returns after `ms` milliseconds. + */ +export function sleep(ms = 1000): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +}