@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.
- package/CHANGES-FROM-UPSTREAM.md +57 -0
- package/LICENSE +177 -0
- package/NOTICE +9 -0
- package/README.md +10 -2
- package/dist/RealmContext.d.ts +153 -0
- package/dist/RealmContext.d.ts.map +1 -0
- package/dist/RealmProvider.d.ts +95 -0
- package/dist/RealmProvider.d.ts.map +1 -0
- package/dist/cachedCollection.d.ts +58 -0
- package/dist/cachedCollection.d.ts.map +1 -0
- package/dist/cachedObject.d.ts +48 -0
- package/dist/cachedObject.d.ts.map +1 -0
- package/dist/helpers.d.ts +22 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/index.cjs +2818 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2809 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.types.tsbuildinfo +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/useObject.d.ts +30 -0
- package/dist/useObject.d.ts.map +1 -0
- package/dist/useQuery.d.ts +31 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useRealm.d.ts +12 -0
- package/dist/useRealm.d.ts.map +1 -0
- package/package.json +117 -4
- package/src/RealmContext.ts +196 -0
- package/src/RealmProvider.tsx +251 -0
- package/src/__tests__/RealmProvider.test.tsx +583 -0
- package/src/__tests__/createRealmTestContext.ts +68 -0
- package/src/__tests__/helpers.ts +35 -0
- package/src/__tests__/mocks.ts +154 -0
- package/src/__tests__/profileHook.tsx +76 -0
- package/src/__tests__/setup.ts +22 -0
- package/src/__tests__/tsconfig.json +14 -0
- package/src/__tests__/useObjectHook.test.tsx +158 -0
- package/src/__tests__/useObjectRender.test.tsx +630 -0
- package/src/__tests__/useQueryHook.test.tsx +209 -0
- package/src/__tests__/useQueryRender.test.tsx +463 -0
- package/src/cachedCollection.ts +210 -0
- package/src/cachedObject.ts +164 -0
- package/src/helpers.ts +60 -0
- package/src/index.tsx +130 -0
- package/src/runtime.d.ts +31 -0
- package/src/types.ts +92 -0
- package/src/useObject.tsx +227 -0
- package/src/useQuery.tsx +205 -0
- 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
|
+
});
|