@pomwik/afterdb-react 0.0.0-stage → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGES-FROM-UPSTREAM.md +57 -0
  2. package/LICENSE +177 -0
  3. package/NOTICE +9 -0
  4. package/README.md +10 -2
  5. package/dist/RealmContext.d.ts +153 -0
  6. package/dist/RealmContext.d.ts.map +1 -0
  7. package/dist/RealmProvider.d.ts +95 -0
  8. package/dist/RealmProvider.d.ts.map +1 -0
  9. package/dist/cachedCollection.d.ts +58 -0
  10. package/dist/cachedCollection.d.ts.map +1 -0
  11. package/dist/cachedObject.d.ts +48 -0
  12. package/dist/cachedObject.d.ts.map +1 -0
  13. package/dist/helpers.d.ts +22 -0
  14. package/dist/helpers.d.ts.map +1 -0
  15. package/dist/index.cjs +2818 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.ts +100 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +2809 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/tsconfig.types.tsbuildinfo +1 -0
  22. package/dist/types.d.ts +64 -0
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/useObject.d.ts +30 -0
  25. package/dist/useObject.d.ts.map +1 -0
  26. package/dist/useQuery.d.ts +31 -0
  27. package/dist/useQuery.d.ts.map +1 -0
  28. package/dist/useRealm.d.ts +12 -0
  29. package/dist/useRealm.d.ts.map +1 -0
  30. package/package.json +117 -4
  31. package/src/RealmContext.ts +196 -0
  32. package/src/RealmProvider.tsx +251 -0
  33. package/src/__tests__/RealmProvider.test.tsx +583 -0
  34. package/src/__tests__/createRealmTestContext.ts +68 -0
  35. package/src/__tests__/helpers.ts +35 -0
  36. package/src/__tests__/mocks.ts +154 -0
  37. package/src/__tests__/profileHook.tsx +76 -0
  38. package/src/__tests__/setup.ts +22 -0
  39. package/src/__tests__/tsconfig.json +14 -0
  40. package/src/__tests__/useObjectHook.test.tsx +158 -0
  41. package/src/__tests__/useObjectRender.test.tsx +630 -0
  42. package/src/__tests__/useQueryHook.test.tsx +209 -0
  43. package/src/__tests__/useQueryRender.test.tsx +463 -0
  44. package/src/cachedCollection.ts +210 -0
  45. package/src/cachedObject.ts +164 -0
  46. package/src/helpers.ts +60 -0
  47. package/src/index.tsx +130 -0
  48. package/src/runtime.d.ts +31 -0
  49. package/src/types.ts +92 -0
  50. package/src/useObject.tsx +227 -0
  51. package/src/useQuery.tsx +205 -0
  52. package/src/useRealm.tsx +41 -0
@@ -0,0 +1,209 @@
1
+ ////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // Copyright 2021 Realm Inc.
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+ //
17
+ ////////////////////////////////////////////////////////////////////////////
18
+
19
+ import Realm from "realm";
20
+ import { renderHook } from "@testing-library/react-native";
21
+
22
+ import { createUseQuery } from "../useQuery";
23
+ import { profileHook } from "./profileHook";
24
+ import { createRealmTestContext } from "./createRealmTestContext";
25
+
26
+ const dogSchema: Realm.ObjectSchema = {
27
+ name: "dog",
28
+ primaryKey: "_id",
29
+ properties: {
30
+ _id: "int",
31
+ name: "string",
32
+ color: "string",
33
+ age: "int",
34
+ gender: "string",
35
+ },
36
+ };
37
+
38
+ interface IDog {
39
+ _id: number;
40
+ name: string;
41
+ color: string;
42
+ age: number;
43
+ gender: string;
44
+ }
45
+
46
+ const testDataSet = [
47
+ { _id: 1, name: "Vincent", color: "black and white", gender: "male", age: 4 },
48
+ { _id: 2, name: "River", color: "brown", gender: "female", age: 12 },
49
+ { _id: 3, name: "Schatzi", color: "beige", gender: "female", age: 10 },
50
+ { _id: 4, name: "Victor", color: "dark brown", gender: "male", age: 18 },
51
+ { _id: 5, name: "Jazz", color: "dark brown", gender: "female", age: 12 },
52
+ { _id: 6, name: "Sadie", color: "gold", gender: "female", age: 5 },
53
+ ];
54
+
55
+ describe("useQuery", () => {
56
+ const context = createRealmTestContext({ schema: [dogSchema] });
57
+ const useQuery = createUseQuery(context.useRealm);
58
+
59
+ beforeEach(() => {
60
+ const realm = context.openRealm();
61
+ realm.write(() => {
62
+ realm.deleteAll();
63
+ testDataSet.forEach((data) => {
64
+ realm.create("dog", data);
65
+ });
66
+ });
67
+ });
68
+
69
+ afterEach(() => {
70
+ context.cleanup();
71
+ });
72
+
73
+ it("can retrieve collections using useQuery", () => {
74
+ const { result } = renderHook(() => useQuery<IDog>("dog"));
75
+ const collection = result.current;
76
+
77
+ const [dog1, dog2, dog3] = testDataSet;
78
+
79
+ expect(collection).not.toBeNull();
80
+ expect(collection.length).toBe(6);
81
+ expect(collection[0]).toMatchObject(dog1);
82
+ expect(collection[1]).toMatchObject(dog2);
83
+ expect(collection[2]).toMatchObject(dog3);
84
+ });
85
+ it("returns the same collection reference if there are no changes", () => {
86
+ const { result } = renderHook(() => useQuery<IDog>("dog"));
87
+ const collection = result.current;
88
+
89
+ expect(collection).not.toBeNull();
90
+ expect(collection.length).toBe(6);
91
+ expect(collection[0]).toEqual(collection?.[0]);
92
+ });
93
+ it("should return undefined indexes that are out of bounds", () => {
94
+ const { result } = renderHook(() => useQuery<IDog>("dog"));
95
+ const collection = result.current;
96
+
97
+ expect(collection[99]).toBe(undefined);
98
+ });
99
+
100
+ it("can filter objects via type and callback", () => {
101
+ const { result } = renderHook(() => useQuery<IDog>("dog", (dogs) => dogs.filtered("age > 10")));
102
+ expect(result.current.length).toBe(3);
103
+ });
104
+
105
+ describe("passing function as 1st argument, deps as 2nd and options as 3rd", () => {
106
+ it("can filter objects via a 'query' property", () => {
107
+ const { result, renders } = profileHook(() =>
108
+ useQuery<IDog>((dogs) => dogs.filtered("age > 10"), [], { type: "dog" }),
109
+ );
110
+ expect(result.current.length).toBe(3);
111
+ expect(renders).toHaveLength(1);
112
+ });
113
+ });
114
+ describe("passing an object of options as argument", () => {
115
+ it("can filter objects via a 'query' property", () => {
116
+ const { result, renders } = profileHook(() =>
117
+ useQuery<IDog>({
118
+ type: "dog",
119
+ query: (dogs) => dogs.filtered("age > 10"),
120
+ }),
121
+ );
122
+ expect(result.current.length).toBe(3);
123
+ expect(renders).toHaveLength(1);
124
+ });
125
+
126
+ it("can update filter objects via a 'query' property", () => {
127
+ const { result, renders, rerender } = profileHook(
128
+ ({ age }) =>
129
+ useQuery<IDog>(
130
+ {
131
+ type: "dog",
132
+ query: (dogs) => dogs.filtered("age > $0", age),
133
+ },
134
+ [age],
135
+ ),
136
+ { initialProps: { age: 10 } },
137
+ );
138
+ expect(result.current.length).toBe(3);
139
+ expect(renders).toHaveLength(1);
140
+
141
+ // Update the query to filter for a different age
142
+ rerender({ age: 15 });
143
+ expect(result.current.length).toBe(1);
144
+ expect(renders).toHaveLength(2);
145
+ });
146
+
147
+ it("can filter notifications using key-path array", async () => {
148
+ const { write } = context;
149
+ const { result, renders } = profileHook(() =>
150
+ useQuery<IDog>({
151
+ type: "dog",
152
+ query: (dogs) => dogs.filtered("age > 10"),
153
+ keyPaths: ["name"],
154
+ }),
155
+ );
156
+
157
+ const initialCollection = result.current;
158
+ expect(initialCollection).toHaveLength(3);
159
+ expect(renders).toHaveLength(1);
160
+
161
+ // Updating a name in the database and expect a render
162
+ const [firstDog] = result.current;
163
+ expect(firstDog.name).toEqual("River");
164
+ write(() => {
165
+ firstDog.name = "Rivery!";
166
+ });
167
+ expect(renders).toHaveLength(2);
168
+ expect(initialCollection).not.toBe(result.current);
169
+
170
+ // Updating an age in the database and don't expect a render
171
+ expect(firstDog.age).toEqual(12);
172
+ write(() => {
173
+ firstDog.age = 13;
174
+ });
175
+ expect(renders).toHaveLength(2);
176
+ });
177
+
178
+ it("can filter notifications using key-path string", async () => {
179
+ const { write } = context;
180
+ const { result, renders } = profileHook(() =>
181
+ useQuery<IDog>({
182
+ type: "dog",
183
+ query: (dogs) => dogs.filtered("name != $0", "Vincent"),
184
+ keyPaths: "age",
185
+ }),
186
+ );
187
+
188
+ const initialCollection = result.current;
189
+ expect(initialCollection).toHaveLength(5);
190
+ expect(renders).toHaveLength(1);
191
+
192
+ // Updating an age in the database and expect a render
193
+ const [firstDog] = result.current;
194
+ expect(firstDog.name).toEqual("River");
195
+ write(() => {
196
+ firstDog.age = 16;
197
+ });
198
+ expect(renders).toHaveLength(2);
199
+ expect(initialCollection).not.toBe(result.current);
200
+
201
+ // Updating a name in the database and don't expect a render
202
+ expect(firstDog.age).toEqual(16);
203
+ write(() => {
204
+ firstDog.name = "Rivery!";
205
+ });
206
+ expect(renders).toHaveLength(2);
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,463 @@
1
+ ////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // Copyright 2021 Realm Inc.
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+ //
17
+ ////////////////////////////////////////////////////////////////////////////
18
+
19
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
20
+ import Realm from "realm";
21
+ import { act, fireEvent, render, waitFor } from "@testing-library/react-native";
22
+ import { FlatList, ListRenderItem, Text, TextInput, TouchableHighlight, View } from "react-native";
23
+
24
+ import { createRealmContext } from "..";
25
+ import { randomRealmPath } from "./helpers";
26
+
27
+ class Item extends Realm.Object {
28
+ id!: number;
29
+ name!: string;
30
+ tags!: Realm.List<Tag>;
31
+
32
+ static schema = {
33
+ name: "Item",
34
+ primaryKey: "id",
35
+ properties: {
36
+ id: "int",
37
+ name: "string",
38
+ tags: "Tag[]",
39
+ },
40
+ };
41
+ }
42
+
43
+ class Tag extends Realm.Object {
44
+ id!: number;
45
+ name!: string;
46
+
47
+ static schema = {
48
+ name: "Tag",
49
+ primaryKey: "id",
50
+ properties: {
51
+ id: "int",
52
+ name: "string",
53
+ },
54
+ };
55
+ }
56
+
57
+ // TODO: It would be better not to have to rely on this, but at the moment I see no other alternatives
58
+ function forceSynchronousNotifications(realm: Realm) {
59
+ // Starting a transaction will force all listeners to advance to the latest version
60
+ // and deliver notifications. We don't need to commit the transaction for this to work.
61
+ realm.beginTransaction();
62
+ realm.cancelTransaction();
63
+ }
64
+
65
+ const testCollection = [...new Array(30)].map((_, index) => ({ id: index, name: `${index}` }));
66
+
67
+ const configuration: Realm.Configuration = {
68
+ schema: [Item, Tag],
69
+ inMemory: true,
70
+ path: randomRealmPath(),
71
+ };
72
+
73
+ const itemRenderCounter = jest.fn();
74
+ const tagRenderCounter = jest.fn();
75
+ const queryObjectChangeCounter = jest.fn();
76
+
77
+ let testRealm: Realm = new Realm(configuration);
78
+
79
+ const { useQuery, useObject, RealmProvider, useRealm } = createRealmContext(configuration);
80
+
81
+ enum QueryType {
82
+ filtered,
83
+ sorted,
84
+ normal,
85
+ }
86
+
87
+ const App = ({ queryType = QueryType.normal, useUseObject = false }) => {
88
+ return (
89
+ <RealmProvider>
90
+ <SetupComponent>
91
+ <View testID="testContainer">
92
+ <TestComponent queryType={queryType} useUseObject={useUseObject} />
93
+ </View>
94
+ </SetupComponent>
95
+ </RealmProvider>
96
+ );
97
+ };
98
+
99
+ const SetupComponent = ({ children }: { children: JSX.Element }): JSX.Element | null => {
100
+ const realm = useRealm();
101
+ const [setupComplete, setSetupComplete] = useState(false);
102
+ useEffect(() => {
103
+ realm.write(() => {
104
+ realm.deleteAll();
105
+ testCollection.forEach((object) => new Item(realm, object));
106
+ });
107
+ setSetupComplete(true);
108
+ }, [realm]);
109
+
110
+ if (!setupComplete) {
111
+ return null;
112
+ }
113
+
114
+ return children;
115
+ };
116
+
117
+ const UseObjectItemComponent: React.FC<{ item: Item | (Item & Realm.Object) }> = React.memo(({ item }) => {
118
+ // Testing that useObject also works and properly handles renders
119
+ const localItem = useObject(Item, item.id);
120
+ if (!localItem) {
121
+ return null;
122
+ }
123
+ return <ItemComponent item={localItem}></ItemComponent>;
124
+ });
125
+
126
+ const renderTag: ListRenderItem<Tag> = ({ item }) => <TagComponent tag={item} />;
127
+ const tagKeyExtractor = (item: Tag) => `tag-${item.id}`;
128
+
129
+ const ItemComponent: React.FC<{ item: Item | (Item & Realm.Object) }> = React.memo(({ item }) => {
130
+ itemRenderCounter();
131
+ const realm = useRealm();
132
+
133
+ return (
134
+ <View testID={`result${item.id}`}>
135
+ <View testID={`name${item.name}`}>
136
+ <Text>{item.name}</Text>
137
+ </View>
138
+ <TextInput
139
+ testID={`input${item.id}`}
140
+ value={item.name}
141
+ onChangeText={(text) => {
142
+ realm.write(() => {
143
+ item.name = text;
144
+ });
145
+ }}
146
+ ></TextInput>
147
+ <TouchableHighlight
148
+ testID={`deleteButton${item.id}`}
149
+ onPress={() => {
150
+ realm.write(() => {
151
+ realm.delete(item);
152
+ });
153
+ }}
154
+ >
155
+ <Text>Delete</Text>
156
+ </TouchableHighlight>
157
+ <FlatList
158
+ horizontal={true} // Make this internal list horizontal to avoid invariant error
159
+ testID={`tagList-${item.id}`}
160
+ data={item.tags}
161
+ keyExtractor={tagKeyExtractor}
162
+ renderItem={renderTag}
163
+ />
164
+ </View>
165
+ );
166
+ });
167
+
168
+ const TagComponent: React.FC<{ tag: Tag }> = React.memo(({ tag }) => {
169
+ tagRenderCounter();
170
+ return (
171
+ <View testID={`tagResult${tag.id}`}>
172
+ <View testID={`tagName${tag.id}`}>
173
+ <Text>{tag.name}</Text>
174
+ </View>
175
+ </View>
176
+ );
177
+ });
178
+
179
+ const FILTER_ARGS: [string] = ["id < 20"];
180
+ const SORTED_ARGS: [string, boolean] = ["id", true];
181
+
182
+ const keyExtractor = (item: Item & Realm.Object) => `${item.id}`;
183
+
184
+ const TestComponent = ({ queryType, useUseObject }: { queryType: QueryType; useUseObject: boolean }) => {
185
+ const collection = useQuery(Item);
186
+
187
+ const [counter, setCounter] = useState(0);
188
+
189
+ const result = useMemo(() => {
190
+ switch (queryType) {
191
+ case QueryType.filtered:
192
+ return collection.filtered(...FILTER_ARGS);
193
+ case QueryType.sorted:
194
+ return collection.sorted(...SORTED_ARGS);
195
+ case QueryType.normal:
196
+ return collection;
197
+ }
198
+ }, [queryType, collection]);
199
+
200
+ // This useEffect is to test that the list object reference is not changing when
201
+ // the component is re-rendered.
202
+ useEffect(() => {
203
+ queryObjectChangeCounter();
204
+ }, [result]);
205
+
206
+ const renderItem = useCallback<ListRenderItem<Item & Realm.Object>>(
207
+ ({ item }) => (useUseObject ? <UseObjectItemComponent item={item} /> : <ItemComponent item={item} />),
208
+ [useUseObject],
209
+ );
210
+
211
+ return (
212
+ <>
213
+ <FlatList testID={"list"} data={result} keyExtractor={keyExtractor} renderItem={renderItem} />;
214
+ <TouchableHighlight
215
+ testID={`rerenderButton`}
216
+ onPress={() => {
217
+ setCounter(counter + 1);
218
+ }}
219
+ >
220
+ <Text>Rerender</Text>
221
+ </TouchableHighlight>
222
+ </>
223
+ );
224
+ };
225
+
226
+ function getTestCollection(queryType: QueryType) {
227
+ switch (queryType) {
228
+ case QueryType.filtered:
229
+ return testRealm.objects(Item).filtered(...FILTER_ARGS);
230
+ case QueryType.sorted:
231
+ return testRealm.objects(Item).sorted(...SORTED_ARGS);
232
+ case QueryType.normal:
233
+ return testRealm.objects(Item);
234
+ }
235
+ }
236
+
237
+ type setupOptions = {
238
+ queryType?: QueryType;
239
+ useUseObject?: boolean;
240
+ };
241
+
242
+ const setupTest = async ({ queryType = QueryType.normal, useUseObject = false }: setupOptions) => {
243
+ const renderResult = render(<App queryType={queryType} useUseObject={useUseObject} />);
244
+ await waitFor(() => renderResult.getByTestId("testContainer"));
245
+
246
+ const collection = getTestCollection(queryType);
247
+ expect(itemRenderCounter).toHaveBeenCalledTimes(10);
248
+
249
+ return { ...renderResult, collection };
250
+ };
251
+
252
+ describe.each`
253
+ queryTypeName | queryType
254
+ ${"normal"} | ${QueryType.normal}
255
+ ${"filtered"} | ${QueryType.filtered}
256
+ ${"sorted"} | ${QueryType.sorted}
257
+ `("useQueryRender: $queryTypeName", ({ queryType }) => {
258
+ beforeEach(() => {
259
+ testRealm = new Realm(configuration);
260
+ });
261
+ afterEach(() => {
262
+ itemRenderCounter.mockClear();
263
+ tagRenderCounter.mockClear();
264
+ queryObjectChangeCounter.mockClear();
265
+ Realm.clearTestState();
266
+ });
267
+
268
+ it("renders data in one render cycle per visible object in collection", async () => {
269
+ const { getByTestId } = render(<App queryType={queryType} />);
270
+
271
+ await waitFor(() => getByTestId("list"));
272
+
273
+ expect(itemRenderCounter).toHaveBeenCalledTimes(10);
274
+ });
275
+ it("change to data will rerender", async () => {
276
+ const { getByTestId, getByText, collection } = await setupTest({ queryType });
277
+
278
+ const firstItem = collection[0];
279
+ const id = firstItem.id;
280
+
281
+ const nameElement = getByTestId(`name${id}`);
282
+ const input = getByTestId(`input${id}`);
283
+
284
+ expect(nameElement).toHaveTextContent(`${id}`);
285
+
286
+ fireEvent.changeText(input, "apple");
287
+
288
+ // Wait for change events to finish their callbacks
289
+ await act(async () => {
290
+ forceSynchronousNotifications(testRealm);
291
+ });
292
+
293
+ expect(nameElement).toHaveTextContent("apple");
294
+ expect(itemRenderCounter).toHaveBeenCalledTimes(11);
295
+ });
296
+
297
+ // TODO: This is a known issue that we have to live with until it is possible
298
+ // to retrieve the objectId from a deleted object in a listener callback
299
+ it.skip("handles deletions", async () => {
300
+ const { getByTestId, collection } = await setupTest({ queryType });
301
+
302
+ const firstItem = collection[0];
303
+ const id = firstItem.id;
304
+
305
+ const nextVisible = collection[10];
306
+
307
+ const deleteButton = getByTestId(`deleteButton${id}`);
308
+ const nameElement = getByTestId(`name${id}`);
309
+
310
+ expect(nameElement).toHaveTextContent(`${id}`);
311
+ expect(itemRenderCounter).toHaveBeenCalledTimes(10);
312
+
313
+ fireEvent.press(deleteButton);
314
+
315
+ await waitFor(() => getByTestId(`name${nextVisible.id}`));
316
+
317
+ expect(itemRenderCounter).toHaveBeenCalledTimes(11);
318
+ });
319
+ it("an implicit update to an item in the FlatList view area causes a rerender", async () => {
320
+ const { collection } = await setupTest({ queryType });
321
+
322
+ testRealm.write(() => {
323
+ collection[0].name = "apple";
324
+ });
325
+
326
+ // One could wait for "apple", but I want to mirror the underlying non-rerender test
327
+ await act(async () => {
328
+ forceSynchronousNotifications(testRealm);
329
+ });
330
+
331
+ expect(itemRenderCounter).toHaveBeenCalledTimes(11);
332
+ });
333
+
334
+ it("does not rerender if the update is out of the FlatList view area", async () => {
335
+ const { collection } = await setupTest({ queryType });
336
+
337
+ testRealm.write(() => {
338
+ const lastIndex = collection.length - 1;
339
+ collection[lastIndex].name = "apple";
340
+ });
341
+
342
+ await act(async () => {
343
+ forceSynchronousNotifications(testRealm);
344
+ });
345
+
346
+ expect(itemRenderCounter).toHaveBeenCalledTimes(10);
347
+ });
348
+ it("collection objects rerender on changes to their linked objects", async () => {
349
+ const { collection, getByText, queryByText } = await setupTest({ queryType });
350
+
351
+ // Insert some tags into visible Items
352
+ testRealm.write(() => {
353
+ const tag1 = testRealm.create(Tag, { id: 1, name: "a123" });
354
+ const tag2 = testRealm.create(Tag, { id: 2, name: "b234" });
355
+ const tag3 = testRealm.create(Tag, { id: 3, name: "c567" });
356
+
357
+ collection[0].tags.push(tag1);
358
+ collection[0].tags.push(tag2);
359
+
360
+ collection[1].tags.push(tag1);
361
+ collection[1].tags.push(tag2);
362
+
363
+ collection[2].tags.push(tag1);
364
+ collection[2].tags.push(tag2);
365
+ collection[2].tags.push(tag3);
366
+ });
367
+
368
+ act(() => {
369
+ forceSynchronousNotifications(testRealm);
370
+ });
371
+
372
+ expect(getByText("c567")).not.toBeNull();
373
+
374
+ expect(tagRenderCounter).toHaveBeenCalledTimes(7);
375
+
376
+ // Changes to a linked object will re-render once
377
+ testRealm.write(() => {
378
+ collection[2].tags[2].name = "765c";
379
+ });
380
+
381
+ act(() => {
382
+ forceSynchronousNotifications(testRealm);
383
+ });
384
+
385
+ expect(getByText("765c")).not.toBeNull();
386
+
387
+ testRealm.write(() => {
388
+ testRealm.delete(collection[2].tags[2]);
389
+ });
390
+
391
+ act(() => {
392
+ forceSynchronousNotifications(testRealm);
393
+ });
394
+
395
+ expect(queryByText("756c")).toBeNull();
396
+ });
397
+ // This replicates the issue https://github.com/realm/realm-js/issues/4375
398
+ it("will handle multiple async transactions", async () => {
399
+ const { queryByTestId, unmount } = await setupTest({ queryType, useUseObject: true });
400
+ const performTest = async () => {
401
+ testRealm.write(() => {
402
+ testRealm.deleteAll();
403
+ });
404
+ let i = 0;
405
+ while (i < 10) {
406
+ await new Promise<void>((resolve) => setTimeout(resolve, 10));
407
+ const id = i;
408
+ testRealm.write(() => {
409
+ return new Item(testRealm, { id, name: `${id}` });
410
+ });
411
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
412
+ testRealm.write(() => {
413
+ const item = testRealm.objectForPrimaryKey(Item, id);
414
+ if (item) {
415
+ item.name = `${id + 100}`;
416
+ }
417
+ });
418
+ i++;
419
+ }
420
+ };
421
+
422
+ await act(async () => {
423
+ await performTest();
424
+ });
425
+
426
+ await waitFor(() => queryByTestId(`name${109}`));
427
+
428
+ unmount();
429
+ });
430
+
431
+ it("will return the same reference when state changes", async () => {
432
+ const { getByTestId } = await setupTest({ queryType });
433
+
434
+ // Force a rerender
435
+ const rerenderButton = getByTestId("rerenderButton");
436
+
437
+ // Update the state twice to ensure the object reference is the same and we don't have a false positive
438
+ fireEvent.press(rerenderButton);
439
+ expect(queryObjectChangeCounter).toHaveBeenCalledTimes(1);
440
+
441
+ fireEvent.press(rerenderButton);
442
+ expect(queryObjectChangeCounter).toHaveBeenCalledTimes(1);
443
+ });
444
+
445
+ it("will rerender when the realm database is cleared", async () => {
446
+ await setupTest({ queryType });
447
+
448
+ const initialCount = 1;
449
+
450
+ expect(queryObjectChangeCounter).toHaveBeenCalledTimes(initialCount);
451
+
452
+ await act(async () => {
453
+ testRealm.write(() => {
454
+ testRealm.deleteAll();
455
+ });
456
+ forceSynchronousNotifications(testRealm);
457
+ });
458
+
459
+ await new Promise<void>((resolve) => setTimeout(resolve, 1000));
460
+
461
+ expect(queryObjectChangeCounter).toHaveBeenCalledTimes(initialCount + 1);
462
+ });
463
+ });