diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useArtifactsFromMlmdContext.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useArtifactsFromMlmdContext.spec.ts new file mode 100644 index 0000000000..514c9cd6b7 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useArtifactsFromMlmdContext.spec.ts @@ -0,0 +1,82 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { Artifact, Context, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useArtifactsFromMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useArtifactsFromMlmdContext'; +import { GetArtifactsByContextResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getArtifactsByContext: jest.fn(), + })), + }; +}); + +describe('useArtifactsFromMlmdContext', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetArtifactsByContext = jest.mocked(mockClient.getArtifactsByContext); + + const mockedContext = new Context(); + mockedContext.setId(1); + + const artifact1 = new Artifact(); + artifact1.setId(1); + const artifact2 = new Artifact(); + artifact2.setId(2); + const mockArtifacts = [artifact1, artifact2]; + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return artifacts', async () => { + mockGetArtifactsByContext.mockResolvedValue({ + getArtifactsList: () => mockArtifacts, + } as GetArtifactsByContextResponse); + + const renderResult = testHook(useArtifactsFromMlmdContext)(mockedContext); + + expect(renderResult).hookToStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockArtifacts, true)); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetArtifactsByContext was called with the correct context ID + const [request] = mockGetArtifactsByContext.mock.calls[0]; + expect(request.getContextId()).toBe(mockedContext.getId()); + }); + + it('should handle errors from getArtifactsByContext', async () => { + const error = new Error('Cannot find artifacts'); + mockGetArtifactsByContext.mockRejectedValue(error); + + const renderResult = testHook(useArtifactsFromMlmdContext)(mockedContext); + + expect(renderResult).hookToStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useExecutionsFromMlmdContext.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useExecutionsFromMlmdContext.spec.ts new file mode 100644 index 0000000000..f02944ad08 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useExecutionsFromMlmdContext.spec.ts @@ -0,0 +1,82 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { Execution, Context, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useExecutionsFromMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useExecutionsFromMlmdContext'; +import { GetExecutionsByContextResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getExecutionsByContext: jest.fn(), + })), + }; +}); + +describe('useExecutionsFromMlmdContext', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetExecutionsByContext = jest.mocked(mockClient.getExecutionsByContext); + + const mockedContext = new Context(); + mockedContext.setId(1); + + const execution1 = new Execution(); + execution1.setId(1); + const execution2 = new Execution(); + execution2.setId(2); + const mockExecutions = [execution1, execution2]; + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return executions', async () => { + mockGetExecutionsByContext.mockResolvedValue({ + getExecutionsList: () => mockExecutions, + } as GetExecutionsByContextResponse); + + const renderResult = testHook(useExecutionsFromMlmdContext)(mockedContext); + + expect(renderResult).hookToStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockExecutions, true)); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetExecutionsByContext was called with the correct context ID + const [request] = mockGetExecutionsByContext.mock.calls[0]; + expect(request.getContextId()).toBe(mockedContext.getId()); + }); + + it('should handle errors from getExecutionsByContext', async () => { + const error = new Error('Cannot find executions'); + mockGetExecutionsByContext.mockRejectedValue(error); + + const renderResult = testHook(useExecutionsFromMlmdContext)(mockedContext); + + expect(renderResult).hookToStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactById.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactById.spec.ts new file mode 100644 index 0000000000..3993ffa482 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactById.spec.ts @@ -0,0 +1,80 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { Artifact, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useGetArtifactById } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactById'; +import { GetArtifactsByIDResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getArtifactsByID: jest.fn(), + })), + GetArtifactsByIDRequest: originalModule.GetArtifactsByIDRequest, + }; +}); + +describe('useGetArtifactById', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetArtifactsByID = jest.mocked(mockClient.getArtifactsByID); + + const mockArtifact = new Artifact(); + mockArtifact.setId(1); + mockArtifact.setName('test-artifact'); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return the artifact', async () => { + mockGetArtifactsByID.mockResolvedValue({ + getArtifactsList: () => [mockArtifact], + } as GetArtifactsByIDResponse); + + const renderResult = testHook(useGetArtifactById)(1); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(undefined)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockArtifact, true)); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetArtifactsByID was called with the correct ID + const [request] = mockGetArtifactsByID.mock.calls[0]; + expect(request.getArtifactIdsList()).toContain(1); + }); + + it('should handle errors from getArtifactsByID', async () => { + const error = new Error('Cannot find specified artifact'); + mockGetArtifactsByID.mockRejectedValue(error); + + const renderResult = testHook(useGetArtifactById)(1); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(undefined)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(undefined, false, error), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactTypes.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactTypes.spec.ts new file mode 100644 index 0000000000..3d5f9e5868 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactTypes.spec.ts @@ -0,0 +1,82 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { ArtifactType, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useGetArtifactTypes } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes'; +import { GetArtifactTypesResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getArtifactTypes: jest.fn(), + })), + GetArtifactTypesRequest: originalModule.GetArtifactTypesRequest, + }; +}); + +describe('useGetArtifactTypes', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetArtifactTypes = jest.mocked(mockClient.getArtifactTypes); + + const mockArtifactType1 = new ArtifactType(); + mockArtifactType1.setId(1); + mockArtifactType1.setName('artifactType1'); + + const mockArtifactType2 = new ArtifactType(); + mockArtifactType2.setId(2); + mockArtifactType2.setName('artifactType2'); + + const mockArtifactTypes = [mockArtifactType1, mockArtifactType2]; + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return the artifact types', async () => { + mockGetArtifactTypes.mockResolvedValue({ + getArtifactTypesList: () => mockArtifactTypes, + } as GetArtifactTypesResponse); + + const renderResult = testHook(useGetArtifactTypes)(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(mockArtifactTypes, true), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getArtifactTypes', async () => { + const error = new Error('Cannot fetch artifact types'); + mockGetArtifactTypes.mockRejectedValue(error); + + const renderResult = testHook(useGetArtifactTypes)(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactsList.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactsList.spec.ts new file mode 100644 index 0000000000..db010dfb25 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetArtifactsList.spec.ts @@ -0,0 +1,107 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { Artifact, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { + usePipelinesAPI, + useMlmdListContext, + MlmdListContextProps, +} from '~/concepts/pipelines/context'; +import { useGetArtifactsList } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactsList'; +import { + GetArtifactsRequest, + GetArtifactsResponse, +} from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; +import { ListOperationOptions } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_pb'; + +// Mock the usePipelinesAPI hook and useMlmdListContext +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), + useMlmdListContext: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getArtifacts: jest.fn(), + })), + GetArtifactsRequest: originalModule.GetArtifactsRequest, + }; +}); + +describe('useGetArtifactsList', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockUseMlmdListContext = jest.mocked(useMlmdListContext); + const mockGetArtifacts = jest.mocked(mockClient.getArtifacts); + + const mockArtifact1 = new Artifact(); + mockArtifact1.setId(1); + mockArtifact1.setName('artifact1'); + + const mockArtifact2 = new Artifact(); + mockArtifact2.setId(2); + mockArtifact2.setName('artifact2'); + + const mockArtifacts = [mockArtifact1, mockArtifact2]; + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + mockUseMlmdListContext.mockReturnValue({ + pageToken: '', + maxResultSize: 100, + filterQuery: '', + } as MlmdListContextProps); + }); + + it('should fetch and return the artifacts list', async () => { + mockGetArtifacts.mockResolvedValue({ + getArtifactsList: () => mockArtifacts, + getNextPageToken: () => 'next-page-token', + } as GetArtifactsResponse); + + const renderResult = testHook(useGetArtifactsList)(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(undefined)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + const request = new GetArtifactsRequest(); + const listOperationOptions = new ListOperationOptions(); + listOperationOptions.setMaxResultSize(100); + request.setOptions(listOperationOptions); + + expect(mockGetArtifacts).toHaveBeenCalledWith(request); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState({ artifacts: mockArtifacts, nextPageToken: 'next-page-token' }, true), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getArtifacts', async () => { + const error = new Error('Cannot fetch artifacts'); + mockGetArtifacts.mockRejectedValue(error); + + const renderResult = testHook(useGetArtifactsList)(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(undefined)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(undefined, false, error), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetEventsByExecutionId.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetEventsByExecutionId.spec.ts new file mode 100644 index 0000000000..c674dcb2dd --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetEventsByExecutionId.spec.ts @@ -0,0 +1,135 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { + MetadataStoreServicePromiseClient, + GetEventsByExecutionIDsResponse, +} from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { + useGetEventsByExecutionId, + useGetEventsByExecutionIds, +} from '~/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getEventsByExecutionIDs: jest.fn(), + })), + GetEventsByExecutionIDsRequest: originalModule.GetEventsByExecutionIDsRequest, + }; +}); + +describe('useGetEventsByExecutionId', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetEventsByExecutionIDs = jest.mocked(mockClient.getEventsByExecutionIDs); + + const mockEventsResponse = new GetEventsByExecutionIDsResponse(); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return events by execution ID', async () => { + mockGetEventsByExecutionIDs.mockResolvedValue(mockEventsResponse); + + const renderResult = testHook(useGetEventsByExecutionId)(1); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(mockEventsResponse, true), + ); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetEventsByExecutionIDs was called with the correct execution ID + const [request] = mockGetEventsByExecutionIDs.mock.calls[0]; + expect(request.getExecutionIdsList()).toContain(1); + }); + + it('should handle errors from getEventsByExecutionIDs', async () => { + const error = new Error('Cannot fetch events'); + mockGetEventsByExecutionIDs.mockRejectedValue(error); + + const renderResult = testHook(useGetEventsByExecutionId)(1); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); + +describe('useGetEventsByExecutionIds', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetEventsByExecutionIDs = jest.mocked(mockClient.getEventsByExecutionIDs); + + const mockEventsResponse = new GetEventsByExecutionIDsResponse(); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return events by execution IDs', async () => { + mockGetEventsByExecutionIDs.mockResolvedValue(mockEventsResponse); + + const renderResult = testHook(useGetEventsByExecutionIds)([1, 2]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(mockEventsResponse, true), + ); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetEventsByExecutionIDs was called with the correct execution IDs + const [request] = mockGetEventsByExecutionIDs.mock.calls[0]; + expect(request.getExecutionIdsList()).toEqual([1, 2]); + }); + + it('should handle errors from getEventsByExecutionIDs', async () => { + const error = new Error('Cannot fetch events'); + mockGetEventsByExecutionIDs.mockRejectedValue(error); + + const renderResult = testHook(useGetEventsByExecutionIds)([1, 2]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetExecutionById.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetExecutionById.spec.ts new file mode 100644 index 0000000000..977b65fc83 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetExecutionById.spec.ts @@ -0,0 +1,96 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { Execution, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useGetExecutionById } from '~/concepts/pipelines/apiHooks/mlmd/useGetExecutionById'; +import { GetExecutionsByIDResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getExecutionsByID: jest.fn(), + })), + GetExecutionsByIDRequest: originalModule.GetExecutionsByIDRequest, + }; +}); + +describe('useGetExecutionById', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetExecutionsByID = jest.mocked(mockClient.getExecutionsByID); + + const mockExecution = new Execution(); + mockExecution.setId(1); + mockExecution.setName('execution1'); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should return null if no executionId is provided', async () => { + const renderResult = testHook(useGetExecutionById)(); + + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, true)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should return null if executionId is NaN', async () => { + const renderResult = testHook(useGetExecutionById)('not-a-number'); + + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, true)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should fetch and return the execution by ID', async () => { + mockGetExecutionsByID.mockResolvedValue({ + getExecutionsList: () => [mockExecution], + } as GetExecutionsByIDResponse); + + const renderResult = testHook(useGetExecutionById)('1'); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockExecution, true)); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetExecutionsByID was called with the correct execution ID + const [request] = mockGetExecutionsByID.mock.calls[0]; + expect(request.getExecutionIdsList()).toContain(1); + }); + + it('should handle errors from getExecutionsByID', async () => { + const error = new Error('Cannot fetch execution'); + mockGetExecutionsByID.mockRejectedValue(error); + + const renderResult = testHook(useGetExecutionById)('1'); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetExecutionsList.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetExecutionsList.spec.ts new file mode 100644 index 0000000000..7232fe10b6 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetExecutionsList.spec.ts @@ -0,0 +1,108 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { Execution, MetadataStoreServicePromiseClient } from '~/third_party/mlmd'; +import { + MlmdListContextProps, + useMlmdListContext, + usePipelinesAPI, +} from '~/concepts/pipelines/context'; +import { useGetExecutionsList } from '~/concepts/pipelines/apiHooks/mlmd/useGetExecutionsList'; +import { + GetExecutionsRequest, + GetExecutionsResponse, +} from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; +import { ListOperationOptions } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_pb'; + +// Mock the usePipelinesAPI and useMlmdListContext hooks +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), + useMlmdListContext: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getExecutions: jest.fn(), + })), + GetExecutionsRequest: originalModule.GetExecutionsRequest, + }; +}); + +describe('useGetExecutionsList', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockUseMlmdListContext = jest.mocked(useMlmdListContext); + const mockGetExecutions = jest.mocked(mockClient.getExecutions); + + const mockExecution1 = new Execution(); + mockExecution1.setId(1); + mockExecution1.setName('execution1'); + + const mockExecution2 = new Execution(); + mockExecution2.setId(2); + mockExecution2.setName('execution2'); + + const mockExecutions = [mockExecution1, mockExecution2]; + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + mockUseMlmdListContext.mockReturnValue({ + pageToken: '', + maxResultSize: 100, + filterQuery: '', + } as MlmdListContextProps); + }); + + it('should fetch and return the executions list', async () => { + mockGetExecutions.mockResolvedValue({ + getExecutionsList: () => mockExecutions, + getNextPageToken: () => 'next-page-token', + } as GetExecutionsResponse); + + const renderResult = testHook(useGetExecutionsList)(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + const request = new GetExecutionsRequest(); + const listOperationOptions = new ListOperationOptions(); + listOperationOptions.setOrderByField( + new ListOperationOptions.OrderByField().setField(ListOperationOptions.OrderByField.Field.ID), + ); + listOperationOptions.setMaxResultSize(100); + request.setOptions(listOperationOptions); + + expect(mockGetExecutions).toHaveBeenCalledWith(request); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState({ executions: mockExecutions, nextPageToken: 'next-page-token' }, true), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getExecutions', async () => { + const error = new Error('Cannot fetch executions'); + mockGetExecutions.mockRejectedValue(error); + + const renderResult = testHook(useGetExecutionsList)(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetLinkedArtifactsByEvents.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetLinkedArtifactsByEvents.spec.ts new file mode 100644 index 0000000000..4dc4700f9a --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetLinkedArtifactsByEvents.spec.ts @@ -0,0 +1,91 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { MetadataStoreServicePromiseClient, Event, Artifact } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useGetLinkedArtifactsByEvents } from '~/concepts/pipelines/apiHooks/mlmd/useGetLinkedArtifactsByEvents'; +import { GetArtifactsByIDResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getArtifactsByID: jest.fn(), + })), + GetArtifactsByIDRequest: originalModule.GetArtifactsByIDRequest, + }; +}); + +describe('useGetLinkedArtifactsByEvents', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetArtifactsByID = jest.mocked(mockClient.getArtifactsByID); + + const mockArtifact = new Artifact(); + mockArtifact.setId(1); + mockArtifact.setName('artifact1'); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should return an empty array if no events have artifact IDs', async () => { + const renderResult = testHook(useGetLinkedArtifactsByEvents)([]); + + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], true)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should fetch and return linked artifacts by events', async () => { + const mockEvent = new Event(); + mockEvent.getArtifactId = jest.fn().mockReturnValue(1); + + mockGetArtifactsByID.mockResolvedValue({ + getArtifactsList: () => [mockArtifact], + } as GetArtifactsByIDResponse); + + const renderResult = testHook(useGetLinkedArtifactsByEvents)([mockEvent]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState([{ event: mockEvent, artifact: mockArtifact }], true), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getArtifactsByID', async () => { + const error = new Error('Cannot fetch artifacts'); + mockGetArtifactsByID.mockRejectedValue(error); + + const mockEvent = new Event(); + mockEvent.getArtifactId = jest.fn().mockReturnValue(1); + + const renderResult = testHook(useGetLinkedArtifactsByEvents)([mockEvent]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetMlmdContextByExecution.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetMlmdContextByExecution.spec.ts new file mode 100644 index 0000000000..e322b5d69a --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetMlmdContextByExecution.spec.ts @@ -0,0 +1,99 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { + MetadataStoreServicePromiseClient, + Execution, + Context, + ContextType, +} from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useGetMlmdContextType } from '~/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType'; +import { useGetPipelineRunContextByExecution } from '~/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextByExecution'; +import { GetContextsByExecutionResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI and useGetMlmdContextType hooks +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +jest.mock('~/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType', () => ({ + useGetMlmdContextType: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getContextsByExecution: jest.fn(), + })), + GetContextsByExecutionRequest: originalModule.GetContextsByExecutionRequest, + }; +}); + +describe('useGetPipelineRunContextByExecution', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockUseGetMlmdContextType = jest.mocked(useGetMlmdContextType); + const mockGetContextsByExecution = jest.mocked(mockClient.getContextsByExecution); + + const mockExecution = new Execution(); + mockExecution.setId(1); + + const mockContext = new Context(); + mockContext.setTypeId(1); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + mockUseGetMlmdContextType.mockReturnValue( + standardUseFetchState( + { + getId: () => 1, + } as ContextType, + false, + ), + ); + }); + + it('should fetch and return the context by execution', async () => { + mockGetContextsByExecution.mockResolvedValue({ + getContextsList: () => [mockContext], + } as GetContextsByExecutionResponse); + + const renderResult = testHook(useGetPipelineRunContextByExecution)(mockExecution); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockContext, true)); + expect(renderResult).hookToHaveUpdateCount(2); + + // Check that mockGetContextsByExecution was called with the correct execution ID + const [request] = mockGetContextsByExecution.mock.calls[0]; + expect(request.getExecutionId()).toBe(1); + }); + + it('should handle errors from getContextsByExecution', async () => { + const error = new Error('Cannot fetch contexts'); + mockGetContextsByExecution.mockRejectedValue(error); + + const renderResult = testHook(useGetPipelineRunContextByExecution)(mockExecution); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetMlmdContextType.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetMlmdContextType.spec.ts new file mode 100644 index 0000000000..ae1d67098e --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useGetMlmdContextType.spec.ts @@ -0,0 +1,83 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { MetadataStoreServicePromiseClient, ContextType } from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { MlmdContextTypes } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { useGetMlmdContextType } from '~/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType'; +import { GetContextTypeResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getContextType: jest.fn(), + })), + GetContextTypeRequest: originalModule.GetContextTypeRequest, + }; +}); + +describe('useGetMlmdContextType', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetContextType = jest.mocked(mockClient.getContextType); + + const mockContextType = new ContextType(); + mockContextType.setId(1); + mockContextType.setName('contextType1'); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should not be loaded when no type is provided', async () => { + const renderResult = testHook(useGetMlmdContextType)(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(null, false, undefined), + ); + }); + + it('should fetch and return the context type', async () => { + mockGetContextType.mockResolvedValue({ + getContextType: () => mockContextType, + } as GetContextTypeResponse); + + const renderResult = testHook(useGetMlmdContextType)(MlmdContextTypes.RUN); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockContextType, true)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getContextType', async () => { + const error = new Error('Cannot fetch context type'); + mockGetContextType.mockRejectedValue(error); + + const renderResult = testHook(useGetMlmdContextType)(MlmdContextTypes.RUN); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useMlmdContext.spec.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useMlmdContext.spec.ts new file mode 100644 index 0000000000..350fdff6ca --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/__tests__/useMlmdContext.spec.ts @@ -0,0 +1,115 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { MetadataStoreServicePromiseClient, Context } from '~/third_party/mlmd'; +import { MlmdContextTypes } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useMlmdContext'; +import { GetContextByTypeAndNameResponse } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI hook +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +// Mock the getMlmdContext function +jest.mock('~/concepts/pipelines/apiHooks/mlmd/useMlmdContext', () => ({ + ...jest.requireActual('~/concepts/pipelines/apiHooks/mlmd/useMlmdContext'), + getMlmdContext: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getContextByTypeAndName: jest.fn(), + })), + GetContextByTypeAndNameRequest: originalModule.GetContextByTypeAndNameRequest, + }; +}); + +describe('useMlmdContext', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetContextByTypeAndName = jest.mocked(mockClient.getContextByTypeAndName); + + const mockContext = new Context(); + mockContext.setName('test-context'); + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should return an error when no type is provided', async () => { + const renderResult = testHook(useMlmdContext)('someName'); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(null, false, undefined), + ); + }); + + it('should return an error when no name is provided', async () => { + const renderResult = testHook(useMlmdContext)(undefined, MlmdContextTypes.RUN); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(null, false, undefined), + ); + }); + + it('should fetch and return the context', async () => { + mockGetContextByTypeAndName.mockResolvedValue({ + getContext: () => mockContext, + } as GetContextByTypeAndNameResponse); + + const renderResult = testHook(useMlmdContext)('testName', MlmdContextTypes.RUN); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(mockContext, true)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle no result from getMlmdContext', async () => { + mockGetContextByTypeAndName.mockResolvedValue({ + getContext: () => undefined, + } as GetContextByTypeAndNameResponse); + + const renderResult = testHook(useMlmdContext)('testName', MlmdContextTypes.RUN); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState(null, false, new Error('Cannot find specified context')), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getMlmdContext', async () => { + const error = new Error('Test error'); + mockGetContextByTypeAndName.mockRejectedValue(error); + + const renderResult = testHook(useMlmdContext)('testName', MlmdContextTypes.RUN); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null)); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState(null, false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts index 810ad059e8..f1bfabd797 100644 --- a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts @@ -7,9 +7,11 @@ import { import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; export const useGetEventsByExecutionId = ( - executionId?: string, -): FetchState => - useGetEventsByExecutionIds([Number(executionId)]); + executionId: number, +): FetchState => { + const ids = React.useMemo(() => [executionId], [executionId]); + return useGetEventsByExecutionIds(ids); +}; export const useGetEventsByExecutionIds = ( executionIds: number[], diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/__tests__/useMlmdPackagesForPipelineRuns.spec.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/__tests__/useMlmdPackagesForPipelineRuns.spec.ts new file mode 100644 index 0000000000..18d492dda0 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/__tests__/useMlmdPackagesForPipelineRuns.spec.ts @@ -0,0 +1,189 @@ +import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks'; +import { + MetadataStoreServicePromiseClient, + Artifact, + Execution, + Event, + Context, +} from '~/third_party/mlmd'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { getMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useMlmdContext'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; +import useMlmdPackagesForPipelineRuns from '~/concepts/pipelines/content/compareRuns/metricsSection/useMlmdPackagesForPipelineRuns'; +import { + GetArtifactsByContextResponse, + GetExecutionsByContextResponse, + GetEventsByExecutionIDsResponse, +} from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; + +// Mock the usePipelinesAPI and getMlmdContext hooks +jest.mock('~/concepts/pipelines/context', () => ({ + usePipelinesAPI: jest.fn(), +})); + +jest.mock('~/concepts/pipelines/apiHooks/mlmd/useMlmdContext', () => ({ + getMlmdContext: jest.fn(), +})); + +// Mock the MetadataStoreServicePromiseClient +jest.mock('~/third_party/mlmd', () => { + const originalModule = jest.requireActual('~/third_party/mlmd'); + return { + ...originalModule, + MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({ + getArtifactsByContext: jest.fn(), + getExecutionsByContext: jest.fn(), + getEventsByExecutionIDs: jest.fn(), + })), + GetArtifactsByContextRequest: originalModule.GetArtifactsByContextRequest, + GetExecutionsByContextRequest: originalModule.GetExecutionsByContextRequest, + GetEventsByExecutionIDsRequest: originalModule.GetEventsByExecutionIDsRequest, + }; +}); + +describe('useMlmdPackagesForPipelineRuns', () => { + const mockClient = new MetadataStoreServicePromiseClient(''); + const mockUsePipelinesAPI = jest.mocked( + usePipelinesAPI as () => Partial>, + ); + const mockGetMlmdContext = jest.mocked(getMlmdContext); + const mockGetArtifactsByContext = jest.mocked(mockClient.getArtifactsByContext); + const mockGetExecutionsByContext = jest.mocked(mockClient.getExecutionsByContext); + const mockGetEventsByExecutionIDs = jest.mocked(mockClient.getEventsByExecutionIDs); + + const mockContext = new Context(); + mockContext.setId(1); + + const mockArtifact = new Artifact(); + mockArtifact.setId(1); + mockArtifact.setName('artifact1'); + + const mockExecution = new Execution(); + mockExecution.setId(1); + + const mockEvent = new Event(); + mockEvent.getArtifactId = jest.fn().mockReturnValue(1); + mockEvent.getExecutionId = jest.fn().mockReturnValue(1); + + // eslint-disable-next-line camelcase + const mockRun = { run_id: 'test-run-id' } as PipelineRunKFv2; + + beforeEach(() => { + jest.clearAllMocks(); + mockUsePipelinesAPI.mockReturnValue({ + metadataStoreServiceClient: mockClient, + }); + }); + + it('should fetch and return MLMD packages for pipeline runs', async () => { + mockGetMlmdContext.mockResolvedValue(mockContext); + mockGetArtifactsByContext.mockResolvedValue({ + getArtifactsList: () => [mockArtifact], + } as GetArtifactsByContextResponse); + mockGetExecutionsByContext.mockResolvedValue({ + getExecutionsList: () => [mockExecution], + } as GetExecutionsByContextResponse); + mockGetEventsByExecutionIDs.mockResolvedValue({ + getEventsList: () => [mockEvent], + } as GetEventsByExecutionIDsResponse); + + const renderResult = testHook(useMlmdPackagesForPipelineRuns)([mockRun]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual( + standardUseFetchState( + [ + { + run: mockRun, + artifacts: [mockArtifact], + events: [mockEvent], + executions: [mockExecution], + }, + ], + true, + ), + ); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getMlmdContext', async () => { + const error = new Error('Cannot fetch context'); + mockGetMlmdContext.mockRejectedValue(error); + + const renderResult = testHook(useMlmdPackagesForPipelineRuns)([mockRun]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getArtifactsByContext', async () => { + const error = new Error('Cannot fetch artifacts'); + mockGetMlmdContext.mockResolvedValue(mockContext); + mockGetArtifactsByContext.mockRejectedValue(error); + + const renderResult = testHook(useMlmdPackagesForPipelineRuns)([mockRun]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getExecutionsByContext', async () => { + const error = new Error('Cannot fetch executions'); + mockGetMlmdContext.mockResolvedValue(mockContext); + mockGetArtifactsByContext.mockResolvedValue({ + getArtifactsList: () => [mockArtifact], + } as GetArtifactsByContextResponse); + mockGetExecutionsByContext.mockRejectedValue(error); + + const renderResult = testHook(useMlmdPackagesForPipelineRuns)([mockRun]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); + + it('should handle errors from getEventsByExecutionIDs', async () => { + const error = new Error('Cannot fetch events'); + mockGetMlmdContext.mockResolvedValue(mockContext); + mockGetArtifactsByContext.mockResolvedValue({ + getArtifactsList: () => [mockArtifact], + } as GetArtifactsByContextResponse); + mockGetExecutionsByContext.mockResolvedValue({ + getExecutionsList: () => [mockExecution], + } as GetExecutionsByContextResponse); + mockGetEventsByExecutionIDs.mockRejectedValue(error); + + const renderResult = testHook(useMlmdPackagesForPipelineRuns)([mockRun]); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([])); + expect(renderResult).hookToHaveUpdateCount(1); + + // wait for update + await renderResult.waitForNextUpdate(); + + expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error)); + expect(renderResult).hookToHaveUpdateCount(2); + }); +}); diff --git a/frontend/src/concepts/pipelines/context/MlmdListContext.tsx b/frontend/src/concepts/pipelines/context/MlmdListContext.tsx index 5535a830ef..159e7ff51a 100644 --- a/frontend/src/concepts/pipelines/context/MlmdListContext.tsx +++ b/frontend/src/concepts/pipelines/context/MlmdListContext.tsx @@ -5,7 +5,7 @@ interface MlmdOrderBy { direction: 'asc' | 'desc'; } -interface MlmdListContextProps { +export interface MlmdListContextProps { filterQuery: string | undefined; pageToken: string | undefined; maxResultSize: number; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx index ee0127b8a9..8937264044 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx @@ -40,7 +40,9 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); const [execution, executionLoaded, executionError] = useGetExecutionById(executionId); - const [eventsResponse, eventsLoaded, eventsError] = useGetEventsByExecutionId(executionId); + const [eventsResponse, eventsLoaded, eventsError] = useGetEventsByExecutionId( + Number(executionId), + ); const [artifactTypes, artifactTypesLoaded] = useGetArtifactTypes(); const allEvents = parseEventsByType(eventsResponse);