Skip to content

Commit

Permalink
useWidget hook and tests
Browse files Browse the repository at this point in the history
- Cleaned up the useObjectFetch tests as well
- Still not using the useWidget hook, will use from deephaven-plugin-ui
  • Loading branch information
mofojed committed May 24, 2024
1 parent ac52ba1 commit 4175c2a
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 27 deletions.
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/jsapi-bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"@deephaven/components": "file:../components",
"@deephaven/jsapi-types": "1.0.0-dev0.34.0",
"@deephaven/log": "file:../log",
"@deephaven/react-hooks": "file:../react-hooks"
"@deephaven/react-hooks": "file:../react-hooks",
"@deephaven/utils": "file:../utils"
},
"devDependencies": {
"react": "^17.x"
Expand Down
1 change: 1 addition & 0 deletions packages/jsapi-bootstrap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './useClient';
export * from './useDeferredApi';
export * from './useObjectFetch';
export * from './useObjectFetcher';
export * from './useWidget';
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useContext } from 'react';
import { TestUtils } from '@deephaven/utils';
import { useObjectFetch } from './useObjectFetch';

const { asMock, flushPromises } = TestUtils;

jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(),
}));

beforeEach(() => {
jest.clearAllMocks();
asMock(useContext).mockName('useContext');
});
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { ObjectFetchManagerContext, useObjectFetch } from './useObjectFetch';

it('should resolve the objectFetch when in the context', async () => {
const objectFetch = jest.fn(async () => undefined);
const unsubscribe = jest.fn();
const descriptor = { type: 'type', name: 'name' };
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
expect(descriptor).toEqual(subscribeDescriptor);
expect(subscribeDescriptor).toEqual(descriptor);
onUpdate({ fetch: objectFetch, error: null });
return unsubscribe;
});
const objectManager = { subscribe };
asMock(useContext).mockReturnValue(objectManager);
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);

const { result } = renderHook(() => useObjectFetch(descriptor));
await act(flushPromises);
const { result } = renderHook(() => useObjectFetch(descriptor), { wrapper });
expect(result.current).toEqual({ fetch: objectFetch, error: null });
expect(result.error).toBeUndefined();
expect(objectFetch).not.toHaveBeenCalled();
});

it('should return an error if objectFetch not available in the context', async () => {
it('should return an error, not throw if objectFetch not available in the context', async () => {
const descriptor = { type: 'type', name: 'name' };
asMock(useContext).mockReturnValue(null);

const { result } = renderHook(() => useObjectFetch(descriptor));
await act(flushPromises);
expect(result.current).toEqual({
fetch: null,
error: expect.any(Error),
Expand Down
68 changes: 68 additions & 0 deletions packages/jsapi-bootstrap/src/useWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { TestUtils } from '@deephaven/utils';
import { useWidget } from './useWidget';
import { ObjectFetchManagerContext } from './useObjectFetch';

describe('useWidget', () => {
it('should return a widget when available', async () => {
const descriptor = { type: 'type', name: 'name' };
const widget = { close: jest.fn() };
const fetch = jest.fn(async () => widget);
const objectFetch = { fetch, error: null };
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
expect(subscribeDescriptor).toEqual(descriptor);
onUpdate(objectFetch);
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
await act(TestUtils.flushPromises);
expect(result.current).toEqual({ widget, error: null });
expect(fetch).toHaveBeenCalledTimes(1);
});

it('should return an error when an error occurs', () => {
const descriptor = { type: 'type', name: 'name' };
const error = new Error('Error fetching widget');
const objectFetch = { fetch: null, error };
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
expect(subscribeDescriptor).toEqual(descriptor);
onUpdate(objectFetch);
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);

const { result } = renderHook(() => useWidget(descriptor), { wrapper });

expect(result.current).toEqual({ widget: null, error });
});

it('should return null when still loading', () => {
const descriptor = { type: 'type', name: 'name' };
const objectFetch = { fetch: null, error: null };
const subscribe = jest.fn((_, onUpdate) => {
onUpdate(objectFetch);
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const { result } = renderHook(() => useWidget(descriptor), { wrapper });

expect(result.current).toEqual({ widget: null, error: null });
});
});
90 changes: 90 additions & 0 deletions packages/jsapi-bootstrap/src/useWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { dh } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import { assertNotNull } from '@deephaven/utils';
import { useEffect, useState } from 'react';
import { useObjectFetch } from './useObjectFetch';

const log = Log.module('useWidget');

/**
* Wrapper object for a widget and error status. Both widget and error will be `null` if it is still loading.
*/
type WidgetWrapper<T extends dh.Widget = dh.Widget> = {
/** Widget object to retrieve */
widget: T | null;

/** Error status if there was an issue fetching the widget */
error: unknown | null;
};

/**
* 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.
* @param descriptor Descriptor to get the widget for
* @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.
*/
export function useWidget<T extends dh.Widget = dh.Widget>(
descriptor: dh.ide.VariableDescriptor
): WidgetWrapper<T> {
const [wrapper, setWrapper] = useState<WidgetWrapper<T>>(() => ({
widget: null,
error: null,
}));

const objectFetch = useObjectFetch<T>(descriptor);

useEffect(
function loadWidget() {
log.debug('loadWidget', descriptor);

const { fetch, error } = objectFetch;

if (error != null) {
// We can't fetch if there's an error getting the fetcher, just return an error
setWrapper({ widget: null, error });
return;
}

if (fetch == null) {
// Still loading
setWrapper({ widget: null, error: null });
return;
}

let isCancelled = false;
async function loadWidgetInternal() {
try {
assertNotNull(fetch);
const newWidget = await fetch();
if (isCancelled) {
log.debug2('loadWidgetInternal cancelled', descriptor, newWidget);
newWidget.close();
newWidget.exportedObjects.forEach(
(exportedObject: dh.WidgetExportedObject) => {
exportedObject.close();
}
);
return;
}
log.debug('loadWidgetInternal done', descriptor, newWidget);

setWrapper({ widget: newWidget, error: null });
} catch (e) {
if (isCancelled) {
return;
}
log.error('loadWidgetInternal error', descriptor, e);
setWrapper({ widget: null, error: e });
}
}
loadWidgetInternal();
return () => {
isCancelled = true;
};
},
[descriptor, objectFetch]
);

return wrapper;
}

export default useWidget;
3 changes: 2 additions & 1 deletion packages/jsapi-bootstrap/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"references": [
{ "path": "../components" },
{ "path": "../log" },
{ "path": "../react-hooks" }
{ "path": "../react-hooks" },
{ "path": "../utils" }
]
}

0 comments on commit 4175c2a

Please sign in to comment.