Skip to content

Commit 459d524

Browse files
authored
Improve IDs passed to useObservable (#167)
* add realtime database tests * better observable ids * throw an error if no observableid is provided * fix comment about emulators * update the docs
1 parent 0a4789a commit 459d524

File tree

10 files changed

+274
-37
lines changed

10 files changed

+274
-37
lines changed

docs/reference.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ _Throws a Promise by default_
104104

105105
#### Parameters
106106

107-
| Parameter | Type | Description |
108-
| ----------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
109-
| ref | [`CollectionReference`](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference) | A reference to the collection you want to listen to |
110-
| options _?_ | ReactFireOptions | Options. This hook will not throw a Promise if you provide `startWithValue`. |
107+
| Parameter | Type | Description |
108+
| ----------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
109+
| ref | [`Query`](https://firebase.google.com/docs/reference/js/firebase.firestore.Query) | A query for the collection you want to listen to |
110+
| options _?_ | ReactFireOptions | Options. This hook will not throw a Promise if you provide `startWithValue`. |
111111

112112
#### Returns
113113

reactfire/auth/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function useUser<T = unknown>(
3737

3838
return useObservable(
3939
user(auth),
40-
'user',
40+
'auth: user',
4141
options ? options.startWithValue : undefined
4242
);
4343
}
Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,140 @@
11
import '@testing-library/jest-dom/extend-expect';
2+
import { render, waitForElement, cleanup, act } from '@testing-library/react';
3+
import * as React from 'react';
4+
import '@testing-library/jest-dom/extend-expect';
5+
import * as firebase from '@firebase/testing';
6+
import { useDatabaseObject, useDatabaseList, FirebaseAppProvider } from '..';
7+
import { database } from 'firebase/app';
8+
import { QueryChange } from 'rxfire/database/dist/database';
29

310
describe('Realtime Database (RTDB)', () => {
11+
let app: import('firebase').app.App;
12+
13+
beforeAll(async () => {
14+
app = firebase.initializeTestApp({
15+
projectId: '12345',
16+
databaseName: 'my-database',
17+
auth: { uid: 'alice' }
18+
}) as import('firebase').app.App;
19+
});
20+
21+
afterEach(async () => {
22+
cleanup();
23+
24+
// clear out the database
25+
app
26+
.database()
27+
.ref()
28+
.set(null);
29+
});
30+
31+
test('sanity check - emulator is running', () => {
32+
// IF THIS TEST FAILS, MAKE SURE YOU'RE RUNNING THESE TESTS BY DOING:
33+
// yarn test
34+
35+
return app
36+
.database()
37+
.ref('hello')
38+
.set({ a: 'world' });
39+
});
40+
441
describe('useDatabaseObject', () => {
5-
test.todo("returns the same value as ref.on('value')");
42+
it('can get an object [TEST REQUIRES EMULATOR]', async () => {
43+
const mockData = { a: 'hello' };
44+
45+
const ref = app.database().ref('hello');
46+
47+
await ref.set(mockData);
48+
49+
const ReadObject = () => {
50+
const { snapshot } = useDatabaseObject(ref);
51+
52+
return <h1 data-testid="readSuccess">{snapshot.val().a}</h1>;
53+
};
54+
55+
const { getByTestId } = render(
56+
<FirebaseAppProvider firebase={app}>
57+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
58+
<ReadObject />
59+
</React.Suspense>
60+
</FirebaseAppProvider>
61+
);
62+
63+
await waitForElement(() => getByTestId('readSuccess'));
64+
65+
expect(getByTestId('readSuccess')).toContainHTML(mockData.a);
66+
});
667
});
768

869
describe('useDatabaseList', () => {
9-
test.todo("returns the same value as ref.on('value')");
70+
it('can get a list [TEST REQUIRES EMULATOR]', async () => {
71+
const mockData1 = { a: 'hello' };
72+
const mockData2 = { a: 'goodbye' };
73+
74+
const ref = app.database().ref('myList');
75+
76+
await act(() => ref.push(mockData1));
77+
await act(() => ref.push(mockData2));
78+
79+
const ReadList = () => {
80+
const changes = useDatabaseList(ref) as QueryChange[];
81+
82+
return (
83+
<ul data-testid="readSuccess">
84+
{changes.map(({ snapshot }) => (
85+
<li key={snapshot.key} data-testid="listItem">
86+
{snapshot.val().a}
87+
</li>
88+
))}
89+
</ul>
90+
);
91+
};
92+
93+
const { getAllByTestId } = render(
94+
<FirebaseAppProvider firebase={app}>
95+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
96+
<ReadList />
97+
</React.Suspense>
98+
</FirebaseAppProvider>
99+
);
100+
101+
await waitForElement(() => getAllByTestId('listItem'));
102+
103+
expect(getAllByTestId('listItem').length).toEqual(2);
104+
});
105+
106+
it('Returns different data for different queries on the same path [TEST REQUIRES EMULATOR]', async () => {
107+
const mockData1 = { a: 'hello' };
108+
const mockData2 = { a: 'goodbye' };
109+
110+
const ref = app.database().ref('items');
111+
const filteredRef = ref.orderByChild('a').equalTo('hello');
112+
113+
await act(() => ref.push(mockData1));
114+
await act(() => ref.push(mockData2));
115+
116+
const ReadFirestoreCollection = () => {
117+
const list = useDatabaseList(ref) as QueryChange[];
118+
const filteredList = useDatabaseList(filteredRef) as QueryChange[];
119+
120+
// filteredList's length should be 1 since we only added one value that matches its query
121+
expect(filteredList.length).toEqual(1);
122+
123+
// the full list should be bigger than the filtered list
124+
expect(list.length).toBeGreaterThan(filteredList.length);
125+
126+
return <h1 data-testid="rendered">Hello</h1>;
127+
};
128+
129+
const { getByTestId } = render(
130+
<FirebaseAppProvider firebase={app}>
131+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
132+
<ReadFirestoreCollection />
133+
</React.Suspense>
134+
</FirebaseAppProvider>
135+
);
136+
137+
await waitForElement(() => getByTestId('rendered'));
138+
});
10139
});
11140
});

reactfire/database/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ export function useDatabaseObject<T = unknown>(
1414
): QueryChange | T {
1515
return useObservable(
1616
object(ref),
17-
ref.toString(),
17+
`RTDB: ${ref.toString()}`,
1818
options ? options.startWithValue : undefined
1919
);
2020
}
2121

22+
// Realtime Database has an undocumented method
23+
// that helps us build a unique ID for the query
24+
// https://github.com/firebase/firebase-js-sdk/blob/aca99669dd8ed096f189578c47a56a8644ac62e6/packages/database/src/api/Query.ts#L601
25+
interface _QueryWithId extends database.Query {
26+
queryIdentifier(): string;
27+
}
28+
2229
/**
2330
* Subscribe to a Realtime Database list
2431
*
@@ -29,9 +36,11 @@ export function useDatabaseList<T = { [key: string]: unknown }>(
2936
ref: database.Reference | database.Query,
3037
options?: ReactFireOptions<T[]>
3138
): QueryChange[] | T[] {
39+
const hash = `RTDB: ${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`;
40+
3241
return useObservable(
3342
list(ref),
34-
ref.toString(),
43+
hash,
3544
options ? options.startWithValue : undefined
3645
);
3746
}

reactfire/firestore/firestore.test.tsx

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { renderHook, act } from '@testing-library/react-hooks';
2-
import { render, waitForElement, cleanup } from '@testing-library/react';
1+
import { render, waitForElement, cleanup, act } from '@testing-library/react';
32

43
import * as React from 'react';
54
import '@testing-library/jest-dom/extend-expect';
@@ -32,7 +31,7 @@ describe('Firestore', () => {
3231

3332
test('sanity check - emulator is running', () => {
3433
// IF THIS TEST FAILS, MAKE SURE YOU'RE RUNNING THESE TESTS BY DOING:
35-
//
34+
// yarn test
3635

3736
return app
3837
.firestore()
@@ -105,18 +104,15 @@ describe('Firestore', () => {
105104
});
106105
});
107106

108-
// THIS TEST CAUSES A REACT `act` WARNING
109-
// IT WILL BE FIXED IN REACT 16.9
110-
// More info here: https://github.com/testing-library/react-testing-library/issues/281
111107
describe('useFirestoreCollection', () => {
112108
it('can get a Firestore collection [TEST REQUIRES EMULATOR]', async () => {
113109
const mockData1 = { a: 'hello' };
114110
const mockData2 = { a: 'goodbye' };
115111

116112
const ref = app.firestore().collection('testCollection');
117113

118-
await ref.add(mockData1);
119-
await ref.add(mockData2);
114+
await act(() => ref.add(mockData1));
115+
await act(() => ref.add(mockData2));
120116

121117
const ReadFirestoreCollection = () => {
122118
const collection = useFirestoreCollection(ref);
@@ -143,20 +139,54 @@ describe('Firestore', () => {
143139

144140
expect(getAllByTestId('listItem').length).toEqual(2);
145141
});
142+
143+
it('Returns different data for different queries on the same path [TEST REQUIRES EMULATOR]', async () => {
144+
const mockData1 = { a: 'hello' };
145+
const mockData2 = { a: 'goodbye' };
146+
147+
const ref = app.firestore().collection('testCollection');
148+
const filteredRef = ref.where('a', '==', 'hello');
149+
150+
await act(() => ref.add(mockData1));
151+
await act(() => ref.add(mockData2));
152+
153+
const ReadFirestoreCollection = () => {
154+
const list = (useFirestoreCollection(ref) as firestore.QuerySnapshot)
155+
.docs;
156+
const filteredList = (useFirestoreCollection(
157+
filteredRef
158+
) as firestore.QuerySnapshot).docs;
159+
160+
// filteredList's length should be 1 since we only added one value that matches its query
161+
expect(filteredList.length).toEqual(1);
162+
163+
// the full list should be bigger than the filtered list
164+
expect(list.length).toBeGreaterThan(filteredList.length);
165+
166+
return <h1 data-testid="rendered">Hello</h1>;
167+
};
168+
169+
const { getByTestId } = render(
170+
<FirebaseAppProvider firebase={app}>
171+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
172+
<ReadFirestoreCollection />
173+
</React.Suspense>
174+
</FirebaseAppProvider>
175+
);
176+
177+
await waitForElement(() => getByTestId('rendered'));
178+
});
146179
});
147180

148-
// THIS TEST CAUSES A REACT `act` WARNING
149-
// IT WILL BE FIXED IN REACT 16.9
150-
// More info here: https://github.com/testing-library/react-testing-library/issues/281
151181
describe('useFirestoreCollectionData', () => {
152182
it('can get a Firestore collection [TEST REQUIRES EMULATOR]', async () => {
153183
const mockData1 = { a: 'hello' };
154184
const mockData2 = { a: 'goodbye' };
155185

156186
const ref = app.firestore().collection('testCollection');
157187

158-
await ref.add(mockData1);
159-
await ref.add(mockData2);
188+
await act(() => ref.add(mockData1));
189+
await act(() => ref.add(mockData2));
160190

161191
const ReadFirestoreCollection = () => {
162192
const list = useFirestoreCollectionData<any>(ref, { idField: 'id' });
@@ -183,5 +213,41 @@ describe('Firestore', () => {
183213

184214
expect(getAllByTestId('listItem').length).toEqual(2);
185215
});
216+
217+
it('Returns different data for different queries on the same path [TEST REQUIRES EMULATOR]', async () => {
218+
const mockData1 = { a: 'hello' };
219+
const mockData2 = { a: 'goodbye' };
220+
221+
const ref = app.firestore().collection('testCollection');
222+
const filteredRef = ref.where('a', '==', 'hello');
223+
224+
await act(() => ref.add(mockData1));
225+
await act(() => ref.add(mockData2));
226+
227+
const ReadFirestoreCollection = () => {
228+
const list = useFirestoreCollectionData<any>(ref, { idField: 'id' });
229+
const filteredList = useFirestoreCollectionData<any>(filteredRef, {
230+
idField: 'id'
231+
});
232+
233+
// filteredList's length should be 1 since we only added one value that matches its query
234+
expect(filteredList.length).toEqual(1);
235+
236+
// the full list should be bigger than the filtered list
237+
expect(list.length).toBeGreaterThan(filteredList.length);
238+
239+
return <h1 data-testid="rendered">Hello</h1>;
240+
};
241+
242+
const { getByTestId } = render(
243+
<FirebaseAppProvider firebase={app}>
244+
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
245+
<ReadFirestoreCollection />
246+
</React.Suspense>
247+
</FirebaseAppProvider>
248+
);
249+
250+
await waitForElement(() => getByTestId('rendered'));
251+
});
186252
});
187253
});

0 commit comments

Comments
 (0)