Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
});
});
10 changes: 10 additions & 0 deletions workspaces/frontend/src/app/hooks/__tests__/useMount.spec.tsx
Original file line number Diff line number Diff line change
@@ -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);
});
});
52 changes: 52 additions & 0 deletions workspaces/frontend/src/app/hooks/__tests__/useNamespaces.spec.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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,
},
});
});
});
Original file line number Diff line number Diff line change
@@ -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' });
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading