Skip to content

Commit 72b1c2d

Browse files
authored
test and fix for error swallowing (#171)
1 parent 459d524 commit 72b1c2d

File tree

3 files changed

+59
-2
lines changed

3 files changed

+59
-2
lines changed

reactfire/useObservable/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function suspendUntilFirst(observable$, observableId) {
1515
})
1616
.catch(err => {
1717
request.isComplete = true;
18-
throw err;
18+
request.setError(err);
1919
});
2020
}
2121

reactfire/useObservable/requestCache.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class ActiveRequest {
1616
this.value = value;
1717
this.isComplete = true;
1818
}
19+
20+
setError(err) {
21+
this.error = err;
22+
this.isComplete = true;
23+
}
1924
}
2025

2126
/*

reactfire/useObservable/useObservable.test.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom/extend-expect';
22
import { act, cleanup, render, waitForElement } from '@testing-library/react';
33
import { act as actOnHook, renderHook } from '@testing-library/react-hooks';
44
import * as React from 'react';
5-
import { of, Subject } from 'rxjs';
5+
import { of, Subject, throwError } from 'rxjs';
66
import { useObservable } from '.';
77

88
describe('useObservable', () => {
@@ -44,6 +44,58 @@ describe('useObservable', () => {
4444
expect(result.current).toEqual(observableVal);
4545
});
4646

47+
it('throws an error if there is an error on initial fetch', async () => {
48+
const error = new Error('I am an error');
49+
const observable$ = throwError(error);
50+
51+
// stop a nasty-looking console error
52+
// https://github.com/facebook/react/issues/11098#issuecomment-523977830
53+
const spy = jest.spyOn(console, 'error');
54+
spy.mockImplementation(() => {});
55+
56+
class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
57+
constructor(props) {
58+
super(props);
59+
this.state = { hasError: false };
60+
}
61+
62+
static getDerivedStateFromError(error) {
63+
// Update state so the next render will show the fallback UI.
64+
return { hasError: true };
65+
}
66+
67+
componentDidCatch(newError, errorInfo) {
68+
expect(newError).toEqual(error);
69+
}
70+
71+
render() {
72+
if (this.state.hasError) {
73+
return <h1 data-testid="error-component">Error</h1>;
74+
} else {
75+
return this.props.children;
76+
}
77+
}
78+
}
79+
80+
const Component = () => {
81+
const val = useObservable(observable$, 'test-error');
82+
return <h1 data-testid="thing">{val}</h1>;
83+
};
84+
85+
const { queryByTestId, getByTestId } = render(
86+
<ErrorBoundary>
87+
<React.Suspense fallback={null}>
88+
<Component />
89+
</React.Suspense>
90+
</ErrorBoundary>
91+
);
92+
93+
await waitForElement(() => getByTestId('error-component'));
94+
expect(queryByTestId('error-component')).toBeInTheDocument();
95+
96+
spy.mockRestore();
97+
});
98+
4799
it('returns the provided startWithValue first even if the observable is ready right away', () => {
48100
// This behavior is a consequense of how observables work. There is
49101
// not a synchronous way to ask an observable if it has a value to emit.

0 commit comments

Comments
 (0)