diff --git a/web_ui/src/core/jobs/hooks/utils.test.tsx b/web_ui/src/core/jobs/hooks/utils.test.tsx
index dcf1de73cb..e3c02e88e6 100644
--- a/web_ui/src/core/jobs/hooks/utils.test.tsx
+++ b/web_ui/src/core/jobs/hooks/utils.test.tsx
@@ -1,9 +1,12 @@
// Copyright (C) 2022-2025 Intel Corporation
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
-import { QueryClient } from '@tanstack/react-query';
+import { ReactNode } from 'react';
+
+import { QueryClientProvider } from '@tanstack/react-query';
import { waitFor } from '@testing-library/react';
+import { createGetiQueryClient } from '../../../providers/query-client-provider/query-client-provider.component';
import { getMockedWorkspaceIdentifier } from '../../../test-utils/mocked-items-factory/mocked-identifiers';
import { getMockedJob } from '../../../test-utils/mocked-items-factory/mocked-jobs';
import { renderHookWithProviders } from '../../../test-utils/render-hook-with-providers';
@@ -29,39 +32,50 @@ const getMockedResponse = (jobs: Job[]) => ({
});
const workspaceIdentifier = getMockedWorkspaceIdentifier({ workspaceId: 'workspaceId' });
+const mockSetInvalidateQueries = jest.fn();
+
+const queryClient = createGetiQueryClient({
+ addNotification: jest.fn(),
+});
+queryClient.invalidateQueries = mockSetInvalidateQueries;
+
+const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+);
describe('Use jobs hook utils', () => {
- beforeAll(() => {
- jest.resetAllMocks();
+ beforeEach(() => {
+ jest.clearAllMocks();
});
it('Should not invalidate balance if feature flag is disabled', async () => {
- const queryClient = new QueryClient();
- queryClient.invalidateQueries = jest.fn();
renderHookWithProviders(
- () =>
- useInvalidateBalanceOnNewJob(
+ () => {
+ return useInvalidateBalanceOnNewJob(
workspaceIdentifier,
getMockedResponse([getMockedJob({ cost: { leaseId: '123', requests: [], consumed: [] } })]),
{ jobState: JobState.SCHEDULED }
- ),
+ );
+ },
{
- providerProps: { queryClient, featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: false } },
+ wrapper,
+ providerProps: { featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: false } },
}
);
await waitFor(() => {
- expect(queryClient.invalidateQueries).not.toHaveBeenCalled();
+ expect(mockSetInvalidateQueries).not.toHaveBeenCalled();
});
});
it('Should invalidate balance if feature flag is enabled', async () => {
- const queryClient = new QueryClient();
- queryClient.invalidateQueries = jest.fn();
const { rerender } = renderHookWithProviders(
- ({ jobs }) => useInvalidateBalanceOnNewJob(workspaceIdentifier, getMockedResponse(jobs), {}),
+ ({ jobs }) => {
+ return useInvalidateBalanceOnNewJob(workspaceIdentifier, getMockedResponse(jobs), {});
+ },
{
- providerProps: { queryClient, featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true } },
+ wrapper,
+ providerProps: { featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true } },
initialProps: {
jobs: [
getMockedJob({
@@ -87,54 +101,54 @@ describe('Use jobs hook utils', () => {
});
await waitFor(() => {
- expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2);
+ expect(mockSetInvalidateQueries).toHaveBeenCalledTimes(2);
});
});
it('Should not invalidate balance if there are no jobs', async () => {
- const queryClient = new QueryClient();
- queryClient.invalidateQueries = jest.fn();
-
renderHookWithProviders(
() =>
useInvalidateBalanceOnNewJob(workspaceIdentifier, getMockedResponse([]), {
jobState: JobState.SCHEDULED,
}),
- { providerProps: { queryClient, featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true } } }
+ {
+ wrapper,
+ providerProps: { featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true } },
+ }
);
await waitFor(() => {
- expect(queryClient.invalidateQueries).not.toHaveBeenCalled();
+ expect(mockSetInvalidateQueries).not.toHaveBeenCalled();
});
});
it('Should not invalidate balance if there are no jobs with cost', async () => {
- const queryClient = new QueryClient();
- queryClient.invalidateQueries = jest.fn();
renderHookWithProviders(
() =>
useInvalidateBalanceOnNewJob(workspaceIdentifier, getMockedResponse([getMockedJob()]), {
jobState: JobState.SCHEDULED,
}),
- { providerProps: { queryClient, featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true } } }
+ {
+ wrapper,
+ providerProps: { featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true } },
+ }
);
await waitFor(() => {
- expect(queryClient.invalidateQueries).not.toHaveBeenCalled();
+ expect(mockSetInvalidateQueries).not.toHaveBeenCalled();
});
});
it('Should invalidate balance if there is a job with new id or a new job', async () => {
- const queryClient = new QueryClient();
- queryClient.invalidateQueries = jest.fn();
const { rerender } = renderHookWithProviders(
- ({ jobs }) =>
- useInvalidateBalanceOnNewJob(workspaceIdentifier, getMockedResponse(jobs), {
+ ({ jobs }) => {
+ return useInvalidateBalanceOnNewJob(workspaceIdentifier, getMockedResponse(jobs), {
jobState: JobState.SCHEDULED,
- }),
+ });
+ },
{
+ wrapper,
providerProps: {
- queryClient,
featureFlags: { FEATURE_FLAG_CREDIT_SYSTEM: true },
},
initialProps: {
@@ -149,7 +163,7 @@ describe('Use jobs hook utils', () => {
);
await waitFor(() => {
- expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1);
+ expect(mockSetInvalidateQueries).toHaveBeenCalledTimes(1);
});
rerender({
@@ -163,7 +177,7 @@ describe('Use jobs hook utils', () => {
});
await waitFor(() => {
- expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2);
+ expect(mockSetInvalidateQueries).toHaveBeenCalledTimes(2);
});
rerender({
@@ -178,7 +192,7 @@ describe('Use jobs hook utils', () => {
});
await waitFor(() => {
- expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(3);
+ expect(mockSetInvalidateQueries).toHaveBeenCalledTimes(3);
});
});
});
diff --git a/web_ui/src/notification/notification.component.tsx b/web_ui/src/notification/notification.component.tsx
index 4e9025c45a..5dfcc13696 100644
--- a/web_ui/src/notification/notification.component.tsx
+++ b/web_ui/src/notification/notification.component.tsx
@@ -24,7 +24,7 @@ type addToastNotificationProps = Omit & {
placement?: NOTIFICATION_CONTAINER;
};
-interface addNotificationProps {
+export interface AddNotificationProps {
message: ReactChild;
type: NOTIFICATION_TYPE;
dismiss?: DismissOptions;
@@ -35,7 +35,7 @@ interface addNotificationProps {
interface NotificationContextProps {
removeNotification: (id: string) => void;
removeNotifications: () => void;
- addNotification: ({ message, type, dismiss, actionButtons }: addNotificationProps) => string;
+ addNotification: ({ message, type, dismiss, actionButtons }: AddNotificationProps) => string;
addToastNotification: (data: addToastNotificationProps) => string;
}
@@ -70,7 +70,7 @@ export const NotificationProvider = ({ children }: NotificationProviderProps): J
hasCloseButton = true,
dismiss = DEFAULT_DISMISS_OPTIONS,
actionButtons,
- }: addNotificationProps): string => {
+ }: AddNotificationProps): string => {
const notificationId = isString(message) || isNumber(message) ? `id-${message}` : `id-${message.key}`;
const NotificationContainer: iNotification = {
diff --git a/web_ui/src/pages/media/hooks/media-delete/media-delete.hook.test.tsx b/web_ui/src/pages/media/hooks/media-delete/media-delete.hook.test.tsx
index 69f30774c0..cbdba997ab 100644
--- a/web_ui/src/pages/media/hooks/media-delete/media-delete.hook.test.tsx
+++ b/web_ui/src/pages/media/hooks/media-delete/media-delete.hook.test.tsx
@@ -1,13 +1,13 @@
// Copyright (C) 2022-2025 Intel Corporation
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
-import { QueryClient } from '@tanstack/react-query';
+import { QueryClientProvider } from '@tanstack/react-query';
import { waitFor } from '@testing-library/react';
import { MEDIA_TYPE } from '../../../../core/media/base-media.interface';
import { createInMemoryMediaService } from '../../../../core/media/services/in-memory-media-service/in-memory-media-service';
import { MediaService } from '../../../../core/media/services/media-service.interface';
-import { NOTIFICATION_TYPE } from '../../../../notification/notification-toast/notification-type.enum';
+import { createGetiQueryClient } from '../../../../providers/query-client-provider/query-client-provider.component';
import { getMockedProjectIdentifier } from '../../../../test-utils/mocked-items-factory/mocked-identifiers';
import { getMockedImageMediaItem } from '../../../../test-utils/mocked-items-factory/mocked-media';
import { renderHookWithProviders } from '../../../../test-utils/render-hook-with-providers';
@@ -16,18 +16,12 @@ import { filterPageMedias } from '../../utils';
import { useDeleteMediaMutation } from './media-delete.hook';
const mockSetQueriesData = jest.fn();
-const mockAddNotification = jest.fn();
jest.mock('../../utils', () => ({
...jest.requireActual('../../utils'),
filterPageMedias: jest.fn(),
}));
-jest.mock('../../../../notification/notification.component', () => ({
- ...jest.requireActual('../../../../notification/notification.component'),
- useNotification: () => ({ addNotification: mockAddNotification }),
-}));
-
const mockedImageMedia = getMockedImageMediaItem({
name: 'image-1',
identifier: { type: MEDIA_TYPE.IMAGE, imageId: '1111' },
@@ -42,14 +36,18 @@ const renderDeleteMediaMutationHook = ({
}: {
mediaService?: MediaService;
} = {}) => {
- const queryClient = new QueryClient();
+ const queryClient = createGetiQueryClient({
+ addNotification: jest.fn(),
+ });
queryClient.setQueriesData = mockSetQueriesData;
return renderHookWithProviders(useDeleteMediaMutation, {
wrapper: ({ children }) => (
- {children}
+
+ {children}
+
),
- providerProps: { mediaService, queryClient },
+ providerProps: { mediaService },
});
};
@@ -85,7 +83,6 @@ describe('useDeleteMediaMutation', () => {
await waitFor(() => {
expect(filterPageMedias).toHaveBeenCalled();
expect(mockSetQueriesData).toHaveBeenCalledTimes(1);
- expect(mockAddNotification).not.toHaveBeenCalled();
});
});
@@ -106,10 +103,6 @@ describe('useDeleteMediaMutation', () => {
await waitFor(() => {
expect(filterPageMedias).toHaveBeenCalled();
expect(mockSetQueriesData).toHaveBeenCalledTimes(2);
- expect(mockAddNotification).toHaveBeenCalledWith({
- message: `Media cannot be deleted. ${errorMessage}`,
- type: NOTIFICATION_TYPE.ERROR,
- });
});
});
});
diff --git a/web_ui/src/providers/query-client-provider/query-client-provider.component.tsx b/web_ui/src/providers/query-client-provider/query-client-provider.component.tsx
index 1383fa6def..591a63c474 100644
--- a/web_ui/src/providers/query-client-provider/query-client-provider.component.tsx
+++ b/web_ui/src/providers/query-client-provider/query-client-provider.component.tsx
@@ -6,6 +6,7 @@ import { ReactNode, useLayoutEffect, useMemo, useRef } from 'react';
import { getErrorMessage } from '@geti/core/src/services/utils';
import {
DefaultOptions,
+ MutationCache,
QueryCache,
QueryClient,
QueryClientProvider as TanstackQueryClientProvider,
@@ -14,7 +15,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { isAxiosError } from 'axios';
import { NOTIFICATION_TYPE } from '../../notification/notification-toast/notification-type.enum';
-import { useNotification } from '../../notification/notification.component';
+import { AddNotificationProps, useNotification } from '../../notification/notification.component';
declare module '@tanstack/react-query' {
interface Register {
@@ -25,10 +26,67 @@ declare module '@tanstack/react-query' {
};
mutationMeta: {
notifyOnError?: boolean;
+ errorMessage?: string;
};
}
}
+export const createGetiQueryClient = ({
+ defaultQueryOptions,
+ addNotification,
+}: {
+ defaultQueryOptions?: DefaultOptions;
+ addNotification: (props: AddNotificationProps) => string;
+}): QueryClient => {
+ const notify = { current: addNotification };
+
+ const queryCache = new QueryCache({
+ onError: (error, query) => {
+ if (isAxiosError(error) && query.meta && 'notifyOnError' in query.meta) {
+ const message = query.meta.errorMessage;
+
+ if (query.meta.notifyOnError === true) {
+ notify.current({
+ message: typeof message === 'string' ? message : getErrorMessage(error),
+ type: NOTIFICATION_TYPE.ERROR,
+ });
+ }
+ }
+
+ if (query.meta && 'disableGlobalErrorHandling' in query.meta) {
+ if (query.meta.disableGlobalErrorHandling === true) {
+ return;
+ }
+ }
+ },
+ });
+
+ const mutationCache = new MutationCache({
+ onError: (error, _variables, _ctx, mutation) => {
+ if (isAxiosError(error) && mutation.meta && 'notifyOnError' in mutation.meta) {
+ const message = mutation.meta.errorMessage;
+
+ notify.current({
+ message: typeof message === 'string' ? message : getErrorMessage(error),
+ type: NOTIFICATION_TYPE.ERROR,
+ });
+ }
+ },
+ });
+
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchIntervalInBackground: false,
+ refetchOnWindowFocus: false,
+ },
+ ...defaultQueryOptions,
+ },
+ queryCache,
+ mutationCache,
+ });
+};
+
export const QueryClientProvider = ({
children,
defaultQueryOptions,
@@ -46,36 +104,7 @@ export const QueryClientProvider = ({
}, [addNotification]);
const queryClient = useMemo(() => {
- const queryCache = new QueryCache({
- onError: (error, query) => {
- if (isAxiosError(error) && query.meta && 'notifyOnError' in query.meta) {
- const message = query.meta.errorMessage;
- if (query.meta.notifyOnError === true) {
- notify.current({
- message: typeof message === 'string' ? message : getErrorMessage(error),
- type: NOTIFICATION_TYPE.ERROR,
- });
- }
- }
-
- if (query.meta && 'disableGlobalErrorHandling' in query.meta) {
- if (query.meta.disableGlobalErrorHandling === true) {
- return;
- }
- }
- },
- });
-
- return new QueryClient({
- defaultOptions: {
- queries: {
- refetchIntervalInBackground: false,
- refetchOnWindowFocus: false,
- },
- ...defaultQueryOptions,
- },
- queryCache,
- });
+ return createGetiQueryClient({ addNotification: notify.current, defaultQueryOptions });
}, [defaultQueryOptions]);
return (
diff --git a/web_ui/src/routes/organizations/organizations-context.test.tsx b/web_ui/src/routes/organizations/organizations-context.test.tsx
index ec316f247e..9b66fc87c5 100644
--- a/web_ui/src/routes/organizations/organizations-context.test.tsx
+++ b/web_ui/src/routes/organizations/organizations-context.test.tsx
@@ -7,7 +7,7 @@ import {
OnboardingService,
OrganizationMetadata,
} from '@geti/core/src/users/services/onboarding-service.interface';
-import { screen, waitForElementToBeRemoved } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import { AxiosError, HttpStatusCode } from 'axios';
import { AccountStatus } from '../../core/organizations/organizations.interface';
@@ -51,10 +51,9 @@ const suspendedOrganization = {
const renderApp = async ({
profile = null,
- onboardingService,
+ onboardingService = createInMemoryOnboardingService(),
}: {
profile?: OnboardingProfile | null;
- isError?: boolean;
onboardingService?: OnboardingService;
organizations?: OrganizationMetadata[];
selectedOrganization?: OrganizationMetadata | null;
@@ -67,22 +66,15 @@ const renderApp = async ({
,
{ profile, services: { onboardingService } }
);
-
- await waitForElementToBeRemoved(screen.getByRole('progressbar'));
};
describe('Organizations context', () => {
it('Displays if the organization has been suspended or deleted.', async () => {
- const onboardingService = createInMemoryOnboardingService();
- onboardingService.getActiveUserProfile = jest.fn(() =>
- Promise.resolve({
+ await renderApp({
+ profile: {
organizations: [suspendedOrganization],
hasAcceptedUserTermsAndConditions: true,
- })
- );
-
- await renderApp({
- onboardingService,
+ },
});
expect(screen.getByText(/Your organization's account has been suspended/)).toBeVisible();
@@ -104,22 +96,21 @@ describe('Organizations context', () => {
await renderApp({
onboardingService,
+ profile: null,
});
- expect(screen.getByText(/You do not have access to any Intel Geti organization/)).toBeVisible();
+ await waitFor(() => {
+ expect(screen.getByText(/You do not have access to any Intel Geti organization/)).toBeVisible();
+ });
});
describe('multiple organizations', () => {
it('Displays the "OrganizationSelectionModal" when no organization has been selected', async () => {
- const onboardingService = createInMemoryOnboardingService();
- onboardingService.getActiveUserProfile = jest.fn(() =>
- Promise.resolve({
+ await renderApp({
+ profile: {
organizations: [mockedOrganization, mockedOrganizationTwo],
hasAcceptedUserTermsAndConditions: true,
- })
- );
- await renderApp({
- onboardingService,
+ },
});
expect(screen.getByText(/^You belong to the following organizations./i)).toBeVisible();
@@ -131,16 +122,11 @@ describe('Organizations context', () => {
});
it('Displays if the organization has been suspended.', async () => {
- const onboardingService = createInMemoryOnboardingService();
- onboardingService.getActiveUserProfile = jest.fn(() =>
- Promise.resolve({
+ await renderApp({
+ profile: {
organizations: [suspendedOrganization, mockedOrganization],
hasAcceptedUserTermsAndConditions: true,
- })
- );
-
- await renderApp({
- onboardingService,
+ },
});
expect(
diff --git a/web_ui/src/test-utils/required-providers-render.tsx b/web_ui/src/test-utils/required-providers-render.tsx
index 3dbac545fc..6d30b391c7 100644
--- a/web_ui/src/test-utils/required-providers-render.tsx
+++ b/web_ui/src/test-utils/required-providers-render.tsx
@@ -1,7 +1,7 @@
// Copyright (C) 2022-2025 Intel Corporation
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
-import { ReactElement, ReactNode, Suspense } from 'react';
+import { ReactElement, ReactNode, Suspense, useMemo } from 'react';
import { CustomFeatureFlags, DEV_FEATURE_FLAGS } from '@geti/core';
import QUERY_KEYS from '@geti/core/src/requests/query-keys';
@@ -11,13 +11,14 @@ import {
} from '@geti/core/src/services/application-services-provider.component';
import { OnboardingProfile } from '@geti/core/src/users/services/onboarding-service.interface';
import { defaultTheme, IntelBrandedLoading, Provider as ThemeProvider } from '@geti/ui';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { QueryClientProvider } from '@tanstack/react-query';
import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { AuthProvider } from 'react-oidc-context';
import { MemoryRouter as Router } from 'react-router-dom';
import { AccountStatusDTO } from '../core/organizations/dtos/organizations.interface';
-import { NotificationProvider, Notifications } from '../notification/notification.component';
+import { NotificationProvider, Notifications, useNotification } from '../notification/notification.component';
+import { createGetiQueryClient } from '../providers/query-client-provider/query-client-provider.component';
import { TusUploadProvider } from '../providers/tus-upload-provider/tus-upload-provider.component';
import { getMockedWorkspace } from './mocked-items-factory/mocked-workspace';
@@ -26,43 +27,57 @@ interface RequiredProvidersProps extends Partial {
- const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- gcTime: 1000,
+const PrefilledQueryClientProvider = ({
+ children,
+ featureFlags,
+ profile,
+}: {
+ children: ReactNode;
+ featureFlags?: CustomFeatureFlags;
+ profile?: OnboardingProfile | null;
+}) => {
+ const { addNotification } = useNotification();
+
+ const prefilledQueryClient = useMemo(() => {
+ const client = createGetiQueryClient({
+ addNotification,
+ defaultQueryOptions: {
+ queries: {
+ gcTime: 1000,
+ },
},
- },
- });
-
- queryClient.setQueryData(QUERY_KEYS.FEATURE_FLAGS, {
- ...DEV_FEATURE_FLAGS,
- ...featureFlags,
- });
-
- if (profile !== null) {
- queryClient.setQueryData(QUERY_KEYS.USER_ONBOARDING_PROFILE, {
- organizations: [{ id: prefilledOrgId, status: AccountStatusDTO.ACTIVE }],
- hasAcceptedUserTermsAndConditions: true,
- ...profile,
});
- }
- ['123', 'org-id', 'organization-id', prefilledOrgId].forEach((organizationId) => {
- const workspaceKey = QUERY_KEYS.WORKSPACES(organizationId);
+ client.setQueryData(QUERY_KEYS.FEATURE_FLAGS, {
+ ...DEV_FEATURE_FLAGS,
+ ...featureFlags,
+ });
+
+ if (profile !== null) {
+ client.setQueryData(QUERY_KEYS.USER_ONBOARDING_PROFILE, {
+ organizations: [{ id: prefilledOrgId, status: AccountStatusDTO.ACTIVE }],
+ hasAcceptedUserTermsAndConditions: true,
+ ...profile,
+ });
+ }
+
+ ['123', 'org-id', 'organization-id', prefilledOrgId].forEach((organizationId) => {
+ const workspaceKey = QUERY_KEYS.WORKSPACES(organizationId);
+
+ client.setQueryData(workspaceKey, [
+ getMockedWorkspace({ id: 'workspace-1', name: 'Workspace 1' }),
+ getMockedWorkspace({ id: 'workspace-2', name: 'Workspace 2' }),
+ ]);
+ });
- queryClient.setQueryData(workspaceKey, [
- getMockedWorkspace({ id: 'workspace-1', name: 'Workspace 1' }),
- getMockedWorkspace({ id: 'workspace-2', name: 'Workspace 2' }),
- ]);
- });
+ return client;
+ }, [addNotification, featureFlags, profile]);
- return queryClient;
+ return {children};
};
export const RequiredProviders = ({
@@ -70,26 +85,25 @@ export const RequiredProviders = ({
featureFlags,
initialEntries,
profile,
- queryClient,
...services
}: RequiredProvidersProps): JSX.Element => {
- const prefilledQueryClient = usePrefilledQueryClient(featureFlags, profile);
-
return (
}>
-
-
-
-
-
+
+
+
+
+ }>
- {children}
+
+ {children}
+
-
-
-
-
+
+
+
+
);