Skip to content

Commit 4175c2a

Browse files
committed
useWidget hook and tests
- Cleaned up the useObjectFetch tests as well - Still not using the useWidget hook, will use from deephaven-plugin-ui
1 parent ac52ba1 commit 4175c2a

7 files changed

Lines changed: 177 additions & 27 deletions

File tree

package-lock.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/jsapi-bootstrap/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"@deephaven/components": "file:../components",
2626
"@deephaven/jsapi-types": "1.0.0-dev0.34.0",
2727
"@deephaven/log": "file:../log",
28-
"@deephaven/react-hooks": "file:../react-hooks"
28+
"@deephaven/react-hooks": "file:../react-hooks",
29+
"@deephaven/utils": "file:../utils"
2930
},
3031
"devDependencies": {
3132
"react": "^17.x"

packages/jsapi-bootstrap/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './useClient';
66
export * from './useDeferredApi';
77
export * from './useObjectFetch';
88
export * from './useObjectFetcher';
9+
export * from './useWidget';

packages/jsapi-bootstrap/src/useObjectFetch.test.ts renamed to packages/jsapi-bootstrap/src/useObjectFetch.test.tsx

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,32 @@
1-
import { act, renderHook } from '@testing-library/react-hooks';
2-
import { useContext } from 'react';
3-
import { TestUtils } from '@deephaven/utils';
4-
import { useObjectFetch } from './useObjectFetch';
5-
6-
const { asMock, flushPromises } = TestUtils;
7-
8-
jest.mock('react', () => ({
9-
...jest.requireActual('react'),
10-
useContext: jest.fn(),
11-
}));
12-
13-
beforeEach(() => {
14-
jest.clearAllMocks();
15-
asMock(useContext).mockName('useContext');
16-
});
1+
import React from 'react';
2+
import { renderHook } from '@testing-library/react-hooks';
3+
import { ObjectFetchManagerContext, useObjectFetch } from './useObjectFetch';
174

185
it('should resolve the objectFetch when in the context', async () => {
196
const objectFetch = jest.fn(async () => undefined);
207
const unsubscribe = jest.fn();
218
const descriptor = { type: 'type', name: 'name' };
229
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
23-
expect(descriptor).toEqual(subscribeDescriptor);
10+
expect(subscribeDescriptor).toEqual(descriptor);
2411
onUpdate({ fetch: objectFetch, error: null });
2512
return unsubscribe;
2613
});
2714
const objectManager = { subscribe };
28-
asMock(useContext).mockReturnValue(objectManager);
15+
const wrapper = ({ children }) => (
16+
<ObjectFetchManagerContext.Provider value={objectManager}>
17+
{children}
18+
</ObjectFetchManagerContext.Provider>
19+
);
2920

30-
const { result } = renderHook(() => useObjectFetch(descriptor));
31-
await act(flushPromises);
21+
const { result } = renderHook(() => useObjectFetch(descriptor), { wrapper });
3222
expect(result.current).toEqual({ fetch: objectFetch, error: null });
3323
expect(result.error).toBeUndefined();
3424
expect(objectFetch).not.toHaveBeenCalled();
3525
});
3626

37-
it('should return an error if objectFetch not available in the context', async () => {
27+
it('should return an error, not throw if objectFetch not available in the context', async () => {
3828
const descriptor = { type: 'type', name: 'name' };
39-
asMock(useContext).mockReturnValue(null);
40-
4129
const { result } = renderHook(() => useObjectFetch(descriptor));
42-
await act(flushPromises);
4330
expect(result.current).toEqual({
4431
fetch: null,
4532
error: expect.any(Error),
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
import { act, renderHook } from '@testing-library/react-hooks';
3+
import { TestUtils } from '@deephaven/utils';
4+
import { useWidget } from './useWidget';
5+
import { ObjectFetchManagerContext } from './useObjectFetch';
6+
7+
describe('useWidget', () => {
8+
it('should return a widget when available', async () => {
9+
const descriptor = { type: 'type', name: 'name' };
10+
const widget = { close: jest.fn() };
11+
const fetch = jest.fn(async () => widget);
12+
const objectFetch = { fetch, error: null };
13+
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
14+
expect(subscribeDescriptor).toEqual(descriptor);
15+
onUpdate(objectFetch);
16+
return jest.fn();
17+
});
18+
const objectManager = { subscribe };
19+
const wrapper = ({ children }) => (
20+
<ObjectFetchManagerContext.Provider value={objectManager}>
21+
{children}
22+
</ObjectFetchManagerContext.Provider>
23+
);
24+
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
25+
await act(TestUtils.flushPromises);
26+
expect(result.current).toEqual({ widget, error: null });
27+
expect(fetch).toHaveBeenCalledTimes(1);
28+
});
29+
30+
it('should return an error when an error occurs', () => {
31+
const descriptor = { type: 'type', name: 'name' };
32+
const error = new Error('Error fetching widget');
33+
const objectFetch = { fetch: null, error };
34+
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
35+
expect(subscribeDescriptor).toEqual(descriptor);
36+
onUpdate(objectFetch);
37+
return jest.fn();
38+
});
39+
const objectManager = { subscribe };
40+
const wrapper = ({ children }) => (
41+
<ObjectFetchManagerContext.Provider value={objectManager}>
42+
{children}
43+
</ObjectFetchManagerContext.Provider>
44+
);
45+
46+
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
47+
48+
expect(result.current).toEqual({ widget: null, error });
49+
});
50+
51+
it('should return null when still loading', () => {
52+
const descriptor = { type: 'type', name: 'name' };
53+
const objectFetch = { fetch: null, error: null };
54+
const subscribe = jest.fn((_, onUpdate) => {
55+
onUpdate(objectFetch);
56+
return jest.fn();
57+
});
58+
const objectManager = { subscribe };
59+
const wrapper = ({ children }) => (
60+
<ObjectFetchManagerContext.Provider value={objectManager}>
61+
{children}
62+
</ObjectFetchManagerContext.Provider>
63+
);
64+
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
65+
66+
expect(result.current).toEqual({ widget: null, error: null });
67+
});
68+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { dh } from '@deephaven/jsapi-types';
2+
import Log from '@deephaven/log';
3+
import { assertNotNull } from '@deephaven/utils';
4+
import { useEffect, useState } from 'react';
5+
import { useObjectFetch } from './useObjectFetch';
6+
7+
const log = Log.module('useWidget');
8+
9+
/**
10+
* Wrapper object for a widget and error status. Both widget and error will be `null` if it is still loading.
11+
*/
12+
type WidgetWrapper<T extends dh.Widget = dh.Widget> = {
13+
/** Widget object to retrieve */
14+
widget: T | null;
15+
16+
/** Error status if there was an issue fetching the widget */
17+
error: unknown | null;
18+
};
19+
20+
/**
21+
* Retrieve a widget for the given variable descriptor. Note that if the widget is successfully fetched, ownership of the widget is passed to the consumer and will need to close the object as well.
22+
* @param descriptor Descriptor to get the widget for
23+
* @returns A WidgetWrapper object that contains the widget or an error status if there was an issue fetching the widget. Will contain nulls if still loading.
24+
*/
25+
export function useWidget<T extends dh.Widget = dh.Widget>(
26+
descriptor: dh.ide.VariableDescriptor
27+
): WidgetWrapper<T> {
28+
const [wrapper, setWrapper] = useState<WidgetWrapper<T>>(() => ({
29+
widget: null,
30+
error: null,
31+
}));
32+
33+
const objectFetch = useObjectFetch<T>(descriptor);
34+
35+
useEffect(
36+
function loadWidget() {
37+
log.debug('loadWidget', descriptor);
38+
39+
const { fetch, error } = objectFetch;
40+
41+
if (error != null) {
42+
// We can't fetch if there's an error getting the fetcher, just return an error
43+
setWrapper({ widget: null, error });
44+
return;
45+
}
46+
47+
if (fetch == null) {
48+
// Still loading
49+
setWrapper({ widget: null, error: null });
50+
return;
51+
}
52+
53+
let isCancelled = false;
54+
async function loadWidgetInternal() {
55+
try {
56+
assertNotNull(fetch);
57+
const newWidget = await fetch();
58+
if (isCancelled) {
59+
log.debug2('loadWidgetInternal cancelled', descriptor, newWidget);
60+
newWidget.close();
61+
newWidget.exportedObjects.forEach(
62+
(exportedObject: dh.WidgetExportedObject) => {
63+
exportedObject.close();
64+
}
65+
);
66+
return;
67+
}
68+
log.debug('loadWidgetInternal done', descriptor, newWidget);
69+
70+
setWrapper({ widget: newWidget, error: null });
71+
} catch (e) {
72+
if (isCancelled) {
73+
return;
74+
}
75+
log.error('loadWidgetInternal error', descriptor, e);
76+
setWrapper({ widget: null, error: e });
77+
}
78+
}
79+
loadWidgetInternal();
80+
return () => {
81+
isCancelled = true;
82+
};
83+
},
84+
[descriptor, objectFetch]
85+
);
86+
87+
return wrapper;
88+
}
89+
90+
export default useWidget;

packages/jsapi-bootstrap/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"references": [
1010
{ "path": "../components" },
1111
{ "path": "../log" },
12-
{ "path": "../react-hooks" }
12+
{ "path": "../react-hooks" },
13+
{ "path": "../utils" }
1314
]
1415
}

0 commit comments

Comments
 (0)