test: add unit tests for frontend hooks (#527)
Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
This commit is contained in:
parent
a932c0fc4c
commit
1b14efde66
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { useCurrentRouteKey } from '~/app/hooks/useCurrentRouteKey';
|
||||
import { AppRouteKey, AppRoutePaths } from '~/app/routes';
|
||||
|
||||
describe('useCurrentRouteKey', () => {
|
||||
const wrapper: React.FC<React.PropsWithChildren<{ initialEntries: string[] }>> = ({
|
||||
children,
|
||||
initialEntries,
|
||||
}) => <MemoryRouter initialEntries={initialEntries}>{children}</MemoryRouter>;
|
||||
|
||||
const fillParams = (pattern: string) => pattern.replace(/:([^/]+)/g, 'test');
|
||||
const cases: ReadonlyArray<readonly [string, AppRouteKey]> = (
|
||||
Object.entries(AppRoutePaths) as [AppRouteKey, string][]
|
||||
).map(([key, pattern]) => [fillParams(pattern), key]);
|
||||
|
||||
it.each(cases)('matches route keys by path: %s', (path, expected) => {
|
||||
const { result } = renderHook(() => useCurrentRouteKey(), {
|
||||
wrapper: (props) => wrapper({ ...props, initialEntries: [path] }),
|
||||
});
|
||||
expect(result.current).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns undefined for unknown paths', () => {
|
||||
const { result } = renderHook(() => useCurrentRouteKey(), {
|
||||
wrapper: (props) => wrapper({ ...props, initialEntries: ['/unknown'] }),
|
||||
});
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import useMount from '~/app/hooks/useMount';
|
||||
|
||||
describe('useMount', () => {
|
||||
it('invokes callback on mount', () => {
|
||||
const cb = jest.fn();
|
||||
renderHook(() => useMount(cb));
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import useNamespaces from '~/app/hooks/useNamespaces';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
import { NotebookApis } from '~/shared/api/notebookApi';
|
||||
import { APIState } from '~/shared/api/types';
|
||||
|
||||
jest.mock('~/app/hooks/useNotebookAPI', () => ({
|
||||
useNotebookAPI: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
|
||||
|
||||
describe('useNamespaces', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rejects when API not available', async () => {
|
||||
const unavailableState: APIState<NotebookApis> = {
|
||||
apiAvailable: false,
|
||||
api: {} as NotebookApis,
|
||||
};
|
||||
mockUseNotebookAPI.mockReturnValue({ ...unavailableState, refreshAllAPI: jest.fn() });
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNamespaces());
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [namespacesData, loaded, loadError] = result.current;
|
||||
expect(namespacesData).toBeNull();
|
||||
expect(loaded).toBe(false);
|
||||
expect(loadError).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns data when API is available', async () => {
|
||||
const listNamespaces = jest.fn().mockResolvedValue({ ok: true, data: [{ name: 'ns1' }] });
|
||||
const api = { namespaces: { listNamespaces } } as unknown as NotebookApis;
|
||||
|
||||
const availableState: APIState<NotebookApis> = {
|
||||
apiAvailable: true,
|
||||
api,
|
||||
};
|
||||
mockUseNotebookAPI.mockReturnValue({ ...availableState, refreshAllAPI: jest.fn() });
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNamespaces());
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [namespacesData, loaded, loadError] = result.current;
|
||||
expect(namespacesData).toEqual([{ name: 'ns1' }]);
|
||||
expect(loaded).toBe(true);
|
||||
expect(loadError).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { NotebookContext } from '~/app/context/NotebookContext';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
|
||||
jest.mock('~/app/EnsureAPIAvailability', () => ({
|
||||
default: ({ children }: { children?: React.ReactNode }) => children as React.ReactElement,
|
||||
}));
|
||||
|
||||
describe('useNotebookAPI', () => {
|
||||
it('returns api state and refresh function from context', () => {
|
||||
const refreshAPIState = jest.fn();
|
||||
const api = {} as ReturnType<typeof useNotebookAPI>['api'];
|
||||
const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||
<NotebookContext.Provider
|
||||
value={{
|
||||
apiState: { apiAvailable: true, api },
|
||||
refreshAPIState,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NotebookContext.Provider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useNotebookAPI(), { wrapper });
|
||||
|
||||
expect(result.current.apiAvailable).toBe(true);
|
||||
expect(result.current.api).toBe(api);
|
||||
|
||||
result.current.refreshAllAPI();
|
||||
expect(refreshAPIState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import useWorkspaceFormData, { EMPTY_FORM_DATA } from '~/app/hooks/useWorkspaceFormData';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
import { NotebookApis } from '~/shared/api/notebookApi';
|
||||
import { buildMockWorkspace, buildMockWorkspaceKind } from '~/shared/mock/mockBuilder';
|
||||
|
||||
jest.mock('~/app/hooks/useNotebookAPI', () => ({
|
||||
useNotebookAPI: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
|
||||
|
||||
describe('useWorkspaceFormData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns empty form data when missing namespace or name', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useWorkspaceFormData({ namespace: undefined, workspaceName: undefined }),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const workspaceFormData = result.current[0];
|
||||
expect(workspaceFormData).toEqual(EMPTY_FORM_DATA);
|
||||
});
|
||||
|
||||
it('maps workspace and kind into form data when API available', async () => {
|
||||
const mockWorkspace = buildMockWorkspace({});
|
||||
const mockWorkspaceKind = buildMockWorkspaceKind({});
|
||||
const getWorkspace = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
data: mockWorkspace,
|
||||
});
|
||||
const getWorkspaceKind = jest.fn().mockResolvedValue({ ok: true, data: mockWorkspaceKind });
|
||||
|
||||
const api = {
|
||||
workspaces: { getWorkspace },
|
||||
workspaceKinds: { getWorkspaceKind },
|
||||
} as unknown as NotebookApis;
|
||||
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useWorkspaceFormData({ namespace: 'ns', workspaceName: 'ws' }),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const workspaceFormData = result.current[0];
|
||||
expect(workspaceFormData).toEqual({
|
||||
kind: mockWorkspaceKind,
|
||||
image: {
|
||||
...mockWorkspace.podTemplate.options.imageConfig.current,
|
||||
hidden: mockWorkspaceKind.hidden,
|
||||
},
|
||||
podConfig: {
|
||||
...mockWorkspace.podTemplate.options.podConfig.current,
|
||||
hidden: mockWorkspaceKind.hidden,
|
||||
},
|
||||
properties: {
|
||||
workspaceName: mockWorkspace.name,
|
||||
deferUpdates: mockWorkspace.deferUpdates,
|
||||
volumes: mockWorkspace.podTemplate.volumes.data,
|
||||
secrets: mockWorkspace.podTemplate.volumes.secrets,
|
||||
homeDirectory: mockWorkspace.podTemplate.volumes.home?.mountPath,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { useWorkspaceFormLocationData } from '~/app/hooks/useWorkspaceFormLocationData';
|
||||
import { NamespaceContextProvider } from '~/app/context/NamespaceContextProvider';
|
||||
|
||||
jest.mock('~/app/context/NamespaceContextProvider', () => {
|
||||
const ReactActual = jest.requireActual('react');
|
||||
const mockNamespaceValue = {
|
||||
namespaces: ['ns1', 'ns2', 'ns3'],
|
||||
selectedNamespace: 'ns1',
|
||||
setSelectedNamespace: jest.fn(),
|
||||
lastUsedNamespace: 'ns1',
|
||||
updateLastUsedNamespace: jest.fn(),
|
||||
};
|
||||
const MockContext = ReactActual.createContext(mockNamespaceValue);
|
||||
return {
|
||||
NamespaceContextProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<MockContext.Provider value={mockNamespaceValue}>{children}</MockContext.Provider>
|
||||
),
|
||||
useNamespaceContext: () => ReactActual.useContext(MockContext),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useWorkspaceFormLocationData', () => {
|
||||
const wrapper: React.FC<
|
||||
React.PropsWithChildren<{ initialEntries: (string | { pathname: string; state?: unknown })[] }>
|
||||
> = ({ children, initialEntries }) => (
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<NamespaceContextProvider>{children}</NamespaceContextProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
it('returns edit mode data', () => {
|
||||
const initialEntries = [
|
||||
{ pathname: '/workspaces/edit', state: { namespace: 'ns2', workspaceName: 'ws' } },
|
||||
];
|
||||
const { result } = renderHook(() => useWorkspaceFormLocationData(), {
|
||||
wrapper: (props) => wrapper({ ...props, initialEntries }),
|
||||
});
|
||||
expect(result.current).toEqual({ mode: 'edit', namespace: 'ns2', workspaceName: 'ws' });
|
||||
});
|
||||
|
||||
it('throws when missing workspaceName in edit mode', () => {
|
||||
const initialEntries = [{ pathname: '/workspaces/edit', state: { namespace: 'ns1' } }];
|
||||
expect(() =>
|
||||
renderHook(() => useWorkspaceFormLocationData(), {
|
||||
wrapper: (props) => wrapper({ ...props, initialEntries }),
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('returns create mode data using selected namespace when state not provided', () => {
|
||||
const initialEntries = [{ pathname: '/workspaces/create' }];
|
||||
const { result } = renderHook(() => useWorkspaceFormLocationData(), {
|
||||
wrapper: (props) => wrapper({ ...props, initialEntries }),
|
||||
});
|
||||
expect(result.current).toEqual({ mode: 'create', namespace: 'ns1' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName';
|
||||
import { NotebookApis } from '~/shared/api/notebookApi';
|
||||
import { buildMockWorkspaceKind } from '~/shared/mock/mockBuilder';
|
||||
|
||||
jest.mock('~/app/hooks/useNotebookAPI', () => ({
|
||||
useNotebookAPI: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
|
||||
|
||||
describe('useWorkspaceKindByName', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rejects when API not available', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: false,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspaceKindByName('jupyter'));
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaceKind, loaded, error] = result.current;
|
||||
expect(workspaceKind).toBeNull();
|
||||
expect(loaded).toBe(false);
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns null when no kind provided', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspaceKindByName(undefined));
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaceKind, loaded, error] = result.current;
|
||||
expect(workspaceKind).toBeNull();
|
||||
expect(loaded).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns kind when API is available', async () => {
|
||||
const mockWorkspaceKind = buildMockWorkspaceKind({});
|
||||
const getWorkspaceKind = jest.fn().mockResolvedValue({ ok: true, data: mockWorkspaceKind });
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: { workspaceKinds: { getWorkspaceKind } } as unknown as NotebookApis,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useWorkspaceKindByName(mockWorkspaceKind.name),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaceKind, loaded, error] = result.current;
|
||||
expect(workspaceKind).toEqual(mockWorkspaceKind);
|
||||
expect(loaded).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
|
||||
import { NotebookApis } from '~/shared/api/notebookApi';
|
||||
import { buildMockWorkspaceKind } from '~/shared/mock/mockBuilder';
|
||||
|
||||
jest.mock('~/app/hooks/useNotebookAPI', () => ({
|
||||
useNotebookAPI: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
|
||||
|
||||
describe('useWorkspaceKinds', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rejects when API not available', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: false,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspaceKinds());
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaceKinds, loaded, error] = result.current;
|
||||
expect(workspaceKinds).toEqual([]);
|
||||
expect(loaded).toBe(false);
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns kinds when API is available', async () => {
|
||||
const mockWorkspaceKind = buildMockWorkspaceKind({});
|
||||
const listWorkspaceKinds = jest.fn().mockResolvedValue({ ok: true, data: [mockWorkspaceKind] });
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: { workspaceKinds: { listWorkspaceKinds } } as unknown as NotebookApis,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspaceKinds());
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaceKinds, loaded, error] = result.current;
|
||||
expect(workspaceKinds).toEqual([mockWorkspaceKind]);
|
||||
expect(loaded).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { WorkspaceActionsContext } from '~/app/context/WorkspaceActionsContext';
|
||||
import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions';
|
||||
import { WorkspacesWorkspace } from '~/generated/data-contracts';
|
||||
import { buildMockWorkspace } from '~/shared/mock/mockBuilder';
|
||||
|
||||
jest.mock('~/app/context/WorkspaceActionsContext', () => {
|
||||
const ReactActual = jest.requireActual('react');
|
||||
const MockContext = ReactActual.createContext(undefined);
|
||||
return {
|
||||
WorkspaceActionsContext: MockContext,
|
||||
useWorkspaceActionsContext: () => ReactActual.useContext(MockContext),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useWorkspaceRowActions', () => {
|
||||
const workspace = buildMockWorkspace({ name: 'ws', namespace: 'ns' });
|
||||
|
||||
type MinimalAction = { title?: string; isSeparator?: boolean; onClick?: () => void };
|
||||
type RequestActionArgs = { workspace: WorkspacesWorkspace; onActionDone?: () => void };
|
||||
type WorkspaceActionsContextLike = {
|
||||
requestViewDetailsAction: (args: RequestActionArgs) => void;
|
||||
requestEditAction: (args: RequestActionArgs) => void;
|
||||
requestDeleteAction: (args: RequestActionArgs) => void;
|
||||
requestStartAction: (args: RequestActionArgs) => void;
|
||||
requestRestartAction: (args: RequestActionArgs) => void;
|
||||
requestStopAction: (args: RequestActionArgs) => void;
|
||||
};
|
||||
|
||||
const contextValue: WorkspaceActionsContextLike = {
|
||||
requestViewDetailsAction: jest.fn(),
|
||||
requestEditAction: jest.fn(),
|
||||
requestDeleteAction: jest.fn(),
|
||||
requestStartAction: jest.fn(),
|
||||
requestRestartAction: jest.fn(),
|
||||
requestStopAction: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||
<WorkspaceActionsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</WorkspaceActionsContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('builds actions respecting visibility and separators', () => {
|
||||
const actionsToInclude = [
|
||||
{ id: 'viewDetails' as const },
|
||||
{ id: 'separator' as const },
|
||||
{ id: 'edit' as const, isVisible: (w: WorkspacesWorkspace) => w.name === 'ws' },
|
||||
{ id: 'delete' as const, isVisible: false },
|
||||
];
|
||||
|
||||
const { result } = renderHook(() => useWorkspaceRowActions(actionsToInclude), { wrapper });
|
||||
const actions = result.current(workspace);
|
||||
|
||||
expect(actions).toHaveLength(3);
|
||||
expect((actions[0] as MinimalAction).title).toBe('View Details');
|
||||
expect((actions[1] as MinimalAction).isSeparator).toBe(true);
|
||||
expect((actions[2] as MinimalAction).title).toBe('Edit');
|
||||
});
|
||||
|
||||
it('triggers context requests on action click', () => {
|
||||
const onActionDone = jest.fn();
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useWorkspaceRowActions([
|
||||
{ id: 'start' },
|
||||
{ id: 'stop' },
|
||||
{ id: 'restart' },
|
||||
{ id: 'delete', onActionDone },
|
||||
]),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const actions = result.current(workspace);
|
||||
act(() => (actions[0] as MinimalAction).onClick?.());
|
||||
act(() => (actions[1] as MinimalAction).onClick?.());
|
||||
act(() => (actions[2] as MinimalAction).onClick?.());
|
||||
act(() => (actions[3] as MinimalAction).onClick?.());
|
||||
|
||||
expect(contextValue.requestStartAction).toHaveBeenCalledWith({ workspace });
|
||||
expect(contextValue.requestStopAction).toHaveBeenCalledWith({ workspace });
|
||||
expect(contextValue.requestRestartAction).toHaveBeenCalledWith({ workspace });
|
||||
expect(contextValue.requestDeleteAction).toHaveBeenCalledWith({ workspace, onActionDone });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,193 @@
|
|||
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
import { useWorkspacesByKind, useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces';
|
||||
import { NotebookApis } from '~/shared/api/notebookApi';
|
||||
import {
|
||||
buildMockImageConfig,
|
||||
buildMockOptionInfo,
|
||||
buildMockPodConfig,
|
||||
buildMockPodTemplate,
|
||||
buildMockWorkspace,
|
||||
buildMockWorkspaceKindInfo,
|
||||
buildMockWorkspaceList,
|
||||
buildPodTemplateOptions,
|
||||
} from '~/shared/mock/mockBuilder';
|
||||
|
||||
jest.mock('~/app/hooks/useNotebookAPI', () => ({
|
||||
useNotebookAPI: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
|
||||
|
||||
describe('useWorkspaces', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useWorkspacesByNamespace', () => {
|
||||
it('returns error when API unavailable', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: false,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspacesByNamespace('ns'));
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaces, loaded, error] = result.current;
|
||||
expect(workspaces).toEqual([]);
|
||||
expect(loaded).toBe(false);
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('fetches workspaces by namespace', async () => {
|
||||
const mockWorkspace = buildMockWorkspace({});
|
||||
const mockWorkspaces = buildMockWorkspaceList({
|
||||
count: 10,
|
||||
namespace: 'ns',
|
||||
kind: mockWorkspace.workspaceKind,
|
||||
});
|
||||
const listWorkspacesByNamespace = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, data: mockWorkspaces });
|
||||
const api = { workspaces: { listWorkspacesByNamespace } } as unknown as NotebookApis;
|
||||
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspacesByNamespace('ns'));
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaces, loaded, error] = result.current;
|
||||
expect(workspaces).toEqual(mockWorkspaces);
|
||||
expect(loaded).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkspacesByKind', () => {
|
||||
it('returns error when API unavailable', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: false,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useWorkspacesByKind({ kind: 'jupyter' }),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaces, loaded, error] = result.current;
|
||||
expect(workspaces).toEqual([]);
|
||||
expect(loaded).toBe(false);
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns default state and error when kind missing', async () => {
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api: {} as NotebookApis,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
const { result, waitForNextUpdate } = renderHook(() => useWorkspacesByKind({ kind: '' }));
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaces, loaded, error] = result.current;
|
||||
expect(workspaces).toEqual([]);
|
||||
expect(loaded).toBe(false);
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('filters workspaces by given criteria', async () => {
|
||||
const mockWorkspace1 = buildMockWorkspace({
|
||||
name: 'workspace1',
|
||||
namespace: 'ns1',
|
||||
workspaceKind: buildMockWorkspaceKindInfo({ name: 'kind1' }),
|
||||
podTemplate: buildMockPodTemplate({
|
||||
options: buildPodTemplateOptions({
|
||||
imageConfig: buildMockImageConfig({
|
||||
current: buildMockOptionInfo({ id: 'img1' }),
|
||||
}),
|
||||
podConfig: buildMockPodConfig({
|
||||
current: buildMockOptionInfo({ id: 'pod1' }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const mockWorkspace2 = buildMockWorkspace({
|
||||
name: 'workspace2',
|
||||
namespace: 'ns2',
|
||||
workspaceKind: buildMockWorkspaceKindInfo({ name: 'kind1' }),
|
||||
podTemplate: buildMockPodTemplate({
|
||||
options: buildPodTemplateOptions({
|
||||
imageConfig: buildMockImageConfig({
|
||||
current: buildMockOptionInfo({ id: 'img2' }),
|
||||
}),
|
||||
podConfig: buildMockPodConfig({
|
||||
current: buildMockOptionInfo({ id: 'pod2' }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const mockWorkspace3 = buildMockWorkspace({
|
||||
name: 'workspace3',
|
||||
namespace: 'ns1',
|
||||
workspaceKind: buildMockWorkspaceKindInfo({ name: 'kind2' }),
|
||||
podTemplate: buildMockPodTemplate({
|
||||
options: buildPodTemplateOptions({
|
||||
imageConfig: buildMockImageConfig({
|
||||
current: buildMockOptionInfo({ id: 'img1' }),
|
||||
}),
|
||||
podConfig: buildMockPodConfig({
|
||||
current: buildMockOptionInfo({ id: 'pod1' }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const mockWorkspaces = [mockWorkspace1, mockWorkspace2, mockWorkspace3];
|
||||
|
||||
const listAllWorkspaces = jest.fn().mockResolvedValue({ ok: true, data: mockWorkspaces });
|
||||
const api = { workspaces: { listAllWorkspaces } } as unknown as NotebookApis;
|
||||
mockUseNotebookAPI.mockReturnValue({
|
||||
api,
|
||||
apiAvailable: true,
|
||||
refreshAllAPI: jest.fn(),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate, rerender } = renderHook(
|
||||
(props) => useWorkspacesByKind(props),
|
||||
{
|
||||
initialProps: {
|
||||
kind: 'kind1',
|
||||
namespace: 'ns1',
|
||||
imageId: 'img1',
|
||||
podConfigId: 'pod1',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const [workspaces, loaded, error] = result.current;
|
||||
expect(workspaces).toEqual([]);
|
||||
expect(loaded).toBe(false);
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaces2, loaded2, error2] = result.current;
|
||||
expect(workspaces2).toEqual([mockWorkspace1]);
|
||||
expect(loaded2).toBe(true);
|
||||
expect(error2).toBeUndefined();
|
||||
|
||||
rerender({ kind: 'kind2', namespace: 'ns1', imageId: 'img1', podConfigId: 'pod1' });
|
||||
await waitForNextUpdate();
|
||||
|
||||
const [workspaces3, loaded3, error3] = result.current;
|
||||
expect(workspaces3).toEqual([mockWorkspace3]);
|
||||
expect(loaded3).toBe(true);
|
||||
expect(error3).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@ import useFetchState, {
|
|||
FetchStateCallbackPromise,
|
||||
} from '~/shared/utilities/useFetchState';
|
||||
|
||||
const EMPTY_FORM_DATA: WorkspaceFormData = {
|
||||
export const EMPTY_FORM_DATA: WorkspaceFormData = {
|
||||
kind: undefined,
|
||||
image: undefined,
|
||||
podConfig: undefined,
|
||||
|
@ -51,14 +51,14 @@ const useWorkspaceFormData = (args: {
|
|||
displayName: imageConfig.displayName,
|
||||
description: imageConfig.description,
|
||||
hidden: false,
|
||||
labels: [],
|
||||
labels: imageConfig.labels,
|
||||
},
|
||||
podConfig: {
|
||||
id: podConfig.id,
|
||||
displayName: podConfig.displayName,
|
||||
description: podConfig.description,
|
||||
hidden: false,
|
||||
labels: [],
|
||||
labels: podConfig.labels,
|
||||
},
|
||||
properties: {
|
||||
workspaceName: workspace.name,
|
||||
|
|
|
@ -5,6 +5,11 @@ import {
|
|||
NamespacesNamespace,
|
||||
WorkspacekindsRedirectMessageLevel,
|
||||
WorkspacekindsWorkspaceKind,
|
||||
WorkspacesImageConfig,
|
||||
WorkspacesOptionInfo,
|
||||
WorkspacesPodConfig,
|
||||
WorkspacesPodTemplate,
|
||||
WorkspacesPodTemplateOptions,
|
||||
WorkspacesWorkspace,
|
||||
WorkspacesWorkspaceKindInfo,
|
||||
WorkspacesWorkspaceState,
|
||||
|
@ -39,6 +44,102 @@ export const buildMockWorkspaceKindInfo = (
|
|||
...workspaceKindInfo,
|
||||
});
|
||||
|
||||
export const buildMockOptionInfo = (
|
||||
optionInfo?: Partial<WorkspacesOptionInfo>,
|
||||
): WorkspacesOptionInfo => ({
|
||||
id: 'jupyterlab_scipy_190',
|
||||
displayName: 'jupyter-scipy:v1.9.0',
|
||||
description: 'JupyterLab, with SciPy Packages',
|
||||
labels: [
|
||||
{
|
||||
key: 'pythonVersion',
|
||||
value: '3.11',
|
||||
},
|
||||
{
|
||||
key: 'jupyterlabVersion',
|
||||
value: '1.9.0',
|
||||
},
|
||||
],
|
||||
...optionInfo,
|
||||
});
|
||||
|
||||
export const buildMockImageConfig = (
|
||||
imageConfig?: Partial<WorkspacesImageConfig>,
|
||||
): WorkspacesImageConfig => ({
|
||||
current: buildMockOptionInfo({}),
|
||||
...imageConfig,
|
||||
});
|
||||
|
||||
export const buildMockPodConfig = (
|
||||
podConfig?: Partial<WorkspacesPodConfig>,
|
||||
): WorkspacesPodConfig => ({
|
||||
current: {
|
||||
id: 'tiny_cpu',
|
||||
displayName: 'Tiny CPU',
|
||||
description: 'Pod with 0.1 CPU, 128 Mb RAM',
|
||||
labels: [
|
||||
{
|
||||
key: 'cpu',
|
||||
value: '100m',
|
||||
},
|
||||
{
|
||||
key: 'memory',
|
||||
value: '128Mi',
|
||||
},
|
||||
{
|
||||
key: 'gpu',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
...podConfig,
|
||||
});
|
||||
|
||||
export const buildPodTemplateOptions = (
|
||||
podTemplateOptions?: Partial<WorkspacesPodTemplateOptions>,
|
||||
): WorkspacesPodTemplateOptions => ({
|
||||
imageConfig: buildMockImageConfig({}),
|
||||
podConfig: buildMockPodConfig({}),
|
||||
...podTemplateOptions,
|
||||
});
|
||||
|
||||
export const buildMockPodTemplate = (
|
||||
podTemplate?: Partial<WorkspacesPodTemplate>,
|
||||
): WorkspacesPodTemplate => ({
|
||||
podMetadata: {
|
||||
labels: { labelKey1: 'labelValue1', labelKey2: 'labelValue2' },
|
||||
annotations: { annotationKey1: 'annotationValue1', annotationKey2: 'annotationValue2' },
|
||||
},
|
||||
volumes: {
|
||||
home: {
|
||||
pvcName: 'Volume-Home',
|
||||
mountPath: '/home',
|
||||
readOnly: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
pvcName: 'Volume-Data1',
|
||||
mountPath: '/data',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
pvcName: 'Volume-Data2',
|
||||
mountPath: '/data',
|
||||
readOnly: false,
|
||||
},
|
||||
],
|
||||
secrets: [
|
||||
{
|
||||
defaultMode: 0o644,
|
||||
mountPath: '/secrets',
|
||||
secretName: 'secret-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: buildPodTemplateOptions({}),
|
||||
...podTemplate,
|
||||
});
|
||||
|
||||
export const buildMockWorkspace = (
|
||||
workspace?: Partial<WorkspacesWorkspace>,
|
||||
): WorkspacesWorkspace => ({
|
||||
|
@ -50,71 +151,7 @@ export const buildMockWorkspace = (
|
|||
pausedTime: new Date(2025, 3, 1).getTime(),
|
||||
state: WorkspacesWorkspaceState.WorkspaceStateRunning,
|
||||
stateMessage: 'Workspace is running',
|
||||
podTemplate: {
|
||||
podMetadata: {
|
||||
labels: { labelKey1: 'labelValue1', labelKey2: 'labelValue2' },
|
||||
annotations: { annotationKey1: 'annotationValue1', annotationKey2: 'annotationValue2' },
|
||||
},
|
||||
volumes: {
|
||||
home: {
|
||||
pvcName: 'Volume-Home',
|
||||
mountPath: '/home',
|
||||
readOnly: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
pvcName: 'Volume-Data1',
|
||||
mountPath: '/data',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
pvcName: 'Volume-Data2',
|
||||
mountPath: '/data',
|
||||
readOnly: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
imageConfig: {
|
||||
current: {
|
||||
id: 'jupyterlab_scipy_190',
|
||||
displayName: 'jupyter-scipy:v1.9.0',
|
||||
description: 'JupyterLab, with SciPy Packages',
|
||||
labels: [
|
||||
{
|
||||
key: 'pythonVersion',
|
||||
value: '3.11',
|
||||
},
|
||||
{
|
||||
key: 'jupyterlabVersion',
|
||||
value: '1.9.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
podConfig: {
|
||||
current: {
|
||||
id: 'tiny_cpu',
|
||||
displayName: 'Tiny CPU',
|
||||
description: 'Pod with 0.1 CPU, 128 Mb RAM',
|
||||
labels: [
|
||||
{
|
||||
key: 'cpu',
|
||||
value: '100m',
|
||||
},
|
||||
{
|
||||
key: 'memory',
|
||||
value: '128Mi',
|
||||
},
|
||||
{
|
||||
key: 'gpu',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
podTemplate: buildMockPodTemplate({}),
|
||||
activity: {
|
||||
lastActivity: new Date(2025, 5, 1).getTime(),
|
||||
lastUpdate: new Date(2025, 4, 1).getTime(),
|
||||
|
|
Loading…
Reference in New Issue