test: add unit tests for frontend hooks (#527)

Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
This commit is contained in:
Guilherme Caponetto 2025-08-12 09:31:11 -03:00 committed by Bhakti Narvekar
parent a932c0fc4c
commit 1b14efde66
12 changed files with 774 additions and 68 deletions

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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,
},
});
});
});

View File

@ -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' });
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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 });
});
});

View File

@ -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();
});
});
});

View File

@ -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,

View File

@ -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(),