diff --git a/package-lock.json b/package-lock.json index 37436a4f9f..a5d7ab424c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20199,11 +20199,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, - "object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" - }, "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", diff --git a/package.json b/package.json index 3222bd240b..002b4d129d 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "next-with-less": "^2.0.5", "nprogress": "^0.2.0", "null-loader": "^3.0.0", - "object-hash": "^2.0.3", "prop-types": "^15.7.2", "rc-util": "^5.28.0", "react": "^18.2.0", diff --git a/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx b/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx index af526278c2..858da2cc16 100644 --- a/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx +++ b/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx @@ -17,15 +17,14 @@ import { makeStore } from 'redux/store'; import CellSetsTool from 'components/data-exploration/cell-sets-tool/CellSetsTool'; import { createCellSet } from 'redux/actions/cellSets'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import mockAPI, { generateDefaultMockAPIResponses, promiseResponse } from '__test__/test-utils/mockAPI'; import { loadBackendStatus } from 'redux/actions/backendStatus'; enableFetchMocks(); -jest.mock('utils/work/seekWorkResponse', () => ({ - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); jest.mock('utils/socketConnection', () => { const mockEmit = jest.fn(); @@ -806,18 +805,7 @@ describe('AnnotateClustersTool', () => { // It dispatches a work request await waitFor(() => { - expect(dispatchWorkRequest).toHaveBeenCalledWith( - experimentId, - { - name: 'ScTypeAnnotate', - species: 'mouse', - tissue: 'Liver', - }, - expect.anything(), - expect.anything(), - { PipelineRunETag: '2021-10-20T12:51:44.755Z', broadcast: true }, - expect.anything(), - ); + expect(fetchWork).toMatchSnapshot(); }); }); }); diff --git a/src/__test__/components/data-exploration/cell-sets-tool/__snapshots__/CellSetsTool.test.jsx.snap b/src/__test__/components/data-exploration/cell-sets-tool/__snapshots__/CellSetsTool.test.jsx.snap index d82081b060..024c6b0fcc 100644 --- a/src/__test__/components/data-exploration/cell-sets-tool/__snapshots__/CellSetsTool.test.jsx.snap +++ b/src/__test__/components/data-exploration/cell-sets-tool/__snapshots__/CellSetsTool.test.jsx.snap @@ -1,5 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`AnnotateClustersTool Can dispatch work request 1`] = ` +[MockFunction] { + "calls": [ + [ + "1234", + { + "name": "ScTypeAnnotate", + "species": "mouse", + "tissue": "Liver", + }, + [Function], + [Function], + { + "broadcast": true, + "timeout": 180.172, + }, + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], +} +`; + exports[`CellSetsTool cell set operations should work appropriately for complement 1`] = ` Set { 31, diff --git a/src/__test__/components/data-exploration/generic-gene-table/ExpressionCellSetModal.test.jsx b/src/__test__/components/data-exploration/generic-gene-table/ExpressionCellSetModal.test.jsx index 834eff6585..a30947af6b 100644 --- a/src/__test__/components/data-exploration/generic-gene-table/ExpressionCellSetModal.test.jsx +++ b/src/__test__/components/data-exploration/generic-gene-table/ExpressionCellSetModal.test.jsx @@ -9,7 +9,8 @@ import { makeStore } from 'redux/store'; import ExpressionCellSetModal from 'components/data-exploration/generic-gene-table/ExpressionCellSetModal'; import { GENES_SELECT } from 'redux/actionTypes/genes'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import { loadBackendStatus } from 'redux/actions/backendStatus'; import { updateExperimentInfo } from 'redux/actions/experimentSettings'; @@ -24,9 +25,7 @@ const mockOnCancel = jest.fn(); jest.mock('utils/pushNotificationMessage'); -jest.mock('utils/work/seekWorkResponse', () => ({ - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const renderExpressionCellSetModal = async (storeState) => { await act(async () => { @@ -96,12 +95,12 @@ describe('ExpressionCellSetModal', () => { userEvent.click(screen.getByText(createButtonText)); }); - const requestParams = dispatchWorkRequest.mock.calls[0]; + const requestParams = fetchWork.mock.calls[0]; expect(requestParams).toMatchSnapshot(); }); it('Modal closes on success', async () => { - dispatchWorkRequest.mockImplementationOnce(() => new Promise((resolve) => { + fetchWork.mockImplementationOnce(() => new Promise((resolve) => { setTimeout(resolve({ data: 'mockData' }), 2000); })); @@ -118,7 +117,7 @@ describe('ExpressionCellSetModal', () => { }); it('Modal shows error notification and does not close on error', async () => { - dispatchWorkRequest.mockImplementationOnce(() => Promise.reject(new Error('Some error'))); + fetchWork.mockImplementationOnce(() => Promise.reject(new Error('Some error'))); await renderExpressionCellSetModal(storeState); diff --git a/src/__test__/components/data-exploration/generic-gene-table/__snapshots__/ExpressionCellSetModal.test.jsx.snap b/src/__test__/components/data-exploration/generic-gene-table/__snapshots__/ExpressionCellSetModal.test.jsx.snap index c033b7794b..805b5315e8 100644 --- a/src/__test__/components/data-exploration/generic-gene-table/__snapshots__/ExpressionCellSetModal.test.jsx.snap +++ b/src/__test__/components/data-exploration/generic-gene-table/__snapshots__/ExpressionCellSetModal.test.jsx.snap @@ -18,12 +18,10 @@ exports[`ExpressionCellSetModal Sends the correct genes list with params 1`] = ` ], "name": "GetExpressionCellSets", }, - 180, - "0db78a1886bd55d2346875d2c695b84e", + [Function], + [Function], { - "PipelineRunETag": "2021-10-20T12:51:44.755Z", "broadcast": true, }, - [Function], ] `; diff --git a/src/__test__/components/data-exploration/heatmap/HeatmapPlot.test.jsx b/src/__test__/components/data-exploration/heatmap/HeatmapPlot.test.jsx index cab02ad5d5..268785df9a 100644 --- a/src/__test__/components/data-exploration/heatmap/HeatmapPlot.test.jsx +++ b/src/__test__/components/data-exploration/heatmap/HeatmapPlot.test.jsx @@ -4,12 +4,11 @@ import preloadAll from 'jest-next-dynamic'; import { render, screen, waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; import { Provider } from 'react-redux'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import markerGenesData2 from '__test__/data/marker_genes_2.json'; import markerGenesData5 from '__test__/data/marker_genes_5.json'; import noCellsGeneExpression from '__test__/data/no_cells_genes_expression.json'; import cellSetsData from '__test__/data/cell_sets.json'; @@ -19,7 +18,6 @@ import { makeStore } from 'redux/store'; import mockAPI, { generateDefaultMockAPIResponses, delayedResponse, - dispatchWorkRequestMock, } from '__test__/test-utils/mockAPI'; import HeatmapPlot from 'components/data-exploration/heatmap/HeatmapPlot'; @@ -36,21 +34,7 @@ import { updatePlotConfig } from 'redux/actions/componentConfig'; const experimentId = fake.EXPERIMENT_ID; -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - const mockWorkRequestETag = (ETagParams) => `${ETagParams.body.nGenes}-marker-genes`; - const mockGeneExpressionETag = (ETagParams) => `${ETagParams.missingGenesBody.genes.join('-')}-expression`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); let vitesscePropsSpy = null; jest.mock('next/dynamic', () => () => (props) => { @@ -66,12 +50,9 @@ jest.mock('lodash/sampleSize', () => ({ enableFetchMocks(); const mockWorkerResponses = { - '5-marker-genes': markerGenesData5, - '2-marker-genes': markerGenesData2, + MarkerHeatmap: markerGenesData5, }; -const newGeneLoadETag = 'Ms4a4b-Smc4-Ccr7-Ifi27l2a-Gm8369-S100a4-S100a6-Tmem176a-Tmem176b-Cxcr6-5830411N06Rik-Lmo4-Il18r1-Atp2b1-Pde5a-Ccl5-Nkg7-Klrd1-AW112010-Klrc1-Gzma-Stmn1-Hmgn2-Pclaf-Tuba1b-Lyz2-Ifitm3-Fcer1g-Tyrobp-Cst3-Cd74-Igkc-Cd79a-H2-Ab1-H2-Eb1-loading_gene_id-expression'; - const loadAndRenderDefaultHeatmap = async (storeState) => { await act(async () => { render( @@ -88,8 +69,6 @@ const loadAndRenderDefaultHeatmap = async (storeState) => { const mockAPIResponses = generateDefaultMockAPIResponses(experimentId); -const errorResponse = () => Promise.reject(new Error('Some error idk')); - let storeState = null; describe('HeatmapPlot', () => { @@ -111,9 +90,9 @@ describe('HeatmapPlot', () => { fetchMock.mockIf(/.*/, mockAPI(mockAPIResponses)); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); vitesscePropsSpy = null; @@ -124,11 +103,6 @@ describe('HeatmapPlot', () => { }); it('Renders the heatmap component by default if everything loads', async () => { - fetchMock.mockIf(/.*/, mockAPI({ - [`/v2/workRequest/${experimentId}/5-marker-genes$`]: () => Promise.resolve(JSON.stringify(true)), - ...mockAPIResponses, - })); - await loadAndRenderDefaultHeatmap(storeState); expect(screen.getByText(/Sup Im a heatmap/i)).toBeInTheDocument(); @@ -153,30 +127,29 @@ describe('HeatmapPlot', () => { it('Shows loader message if the marker genes are loading', async () => { const customWorkerResponses = { - [`/v2/workRequest/${experimentId}/5-marker-genes`]: () => delayedResponse({ body: 'Not found', status: 404 }, 10000), + [`/v2/workRequest/${experimentId}`]: () => delayedResponse({ body: 'Not found', status: 404 }, 10000), ...mockWorkerResponses, }; fetchMock.mockIf(/.*/, mockAPI(customWorkerResponses)); - dispatchWorkRequest - .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); - await loadAndRenderDefaultHeatmap(storeState); expect(screen.getByText(/Assigning a worker to your analysis/i)).toBeInTheDocument(); }); it('Shows loader message if the marker genes are loaded but there\'s other selected genes still loading', async () => { - const customWorkerResponses = { - ...mockWorkerResponses, - [newGeneLoadETag]: () => delayedResponse({ body: 'Not found', status: 404 }, 4000), - }; + let onEtagGeneratedCallback; - dispatchWorkRequest + // we need to manually call the onEtagGenerated callback + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(customWorkerResponses)); + .mockImplementationOnce(() => markerGenesData5) + .mockImplementationOnce((_experimentId, _body, _getState, _dispatch, + { onETagGenerated }) => { + onEtagGeneratedCallback = onETagGenerated; + return new Promise(() => { }); + }); await loadAndRenderDefaultHeatmap(storeState); @@ -186,9 +159,10 @@ describe('HeatmapPlot', () => { // A new gene is being loaded await act(async () => { storeState.dispatch(loadDownsampledGeneExpression(experimentId, [...markerGenesData5.orderedGeneNames, 'loading_gene_id'], 'interactiveHeatmap')); - jest.runAllTimers(); }); + onEtagGeneratedCallback(); + // Loading screen shows up await waitFor(() => { expect(screen.getByText(/Assigning a worker to your analysis/i)).toBeInTheDocument(); @@ -196,15 +170,9 @@ describe('HeatmapPlot', () => { }); it('Handles marker genes loading error correctly', async () => { - const customWorkerResponses = { - ...mockWorkerResponses, - '5-marker-genes': errorResponse, - }; - - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(() => Promise.resolve(null)) - .mockImplementationOnce(dispatchWorkRequestMock(customWorkerResponses)); + .mockImplementationOnce(() => Promise.reject(new Error('Some error idk'))); await loadAndRenderDefaultHeatmap(storeState); @@ -213,14 +181,10 @@ describe('HeatmapPlot', () => { }); it('Handles expression data loading error correctly', async () => { - const customWorkerResponses = { - ...mockWorkerResponses, - [newGeneLoadETag]: errorResponse, - }; - - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(customWorkerResponses)); + .mockImplementationOnce(() => markerGenesData5) + .mockImplementationOnce(() => Promise.reject(new Error('Some error idk'))); await act(async () => { await loadAndRenderDefaultHeatmap(storeState); @@ -232,7 +196,6 @@ describe('HeatmapPlot', () => { // A new gene is being loaded await act(async () => { await storeState.dispatch(loadDownsampledGeneExpression(experimentId, [...markerGenesData5.orderedGeneNames, 'loading_gene_id'], 'interactiveHeatmap')); - jest.runAllTimers(); }); // Error screen shows up @@ -256,8 +219,8 @@ describe('HeatmapPlot', () => { .cellIds.map((cellId) => cellId.toString()); // It loaded once the marker genes - expect(dispatchWorkRequest).toHaveBeenCalledTimes(1); - expect(dispatchWorkRequest.mock.calls[0][1].name === 'MarkerHeatmap').toBe(true); + expect(fetchWork).toHaveBeenCalledTimes(1); + expect(fetchWork.mock.calls[0][1].name === 'MarkerHeatmap').toBe(true); // It shows cells in louvain-3 expect(isSubset(cellsInLouvain3, vitesscePropsSpy.obsIndex)).toEqual(true); @@ -272,17 +235,13 @@ describe('HeatmapPlot', () => { }); // It performs the request with the new hidden cell sets array - expect(dispatchWorkRequest).toHaveBeenCalledTimes(2); - expect(dispatchWorkRequest.mock.calls[1]).toMatchSnapshot(); + expect(fetchWork).toHaveBeenCalledTimes(2); }); it('Shows an empty message when all cell sets are hidden ', async () => { - dispatchWorkRequest - .mockReset() - // Mock each of the loadMarkerGenes calls caused by hiding a cell set - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); - - await loadAndRenderDefaultHeatmap(storeState); + await act(async () => { + await loadAndRenderDefaultHeatmap(storeState); + }); // Renders correctly expect(screen.getByText(/Sup Im a heatmap/i)).toBeInTheDocument(); @@ -292,16 +251,15 @@ describe('HeatmapPlot', () => { .cellSets.find(({ key: parentKey }) => parentKey === 'louvain') .children.map(({ key: cellSetKey }) => cellSetKey); - dispatchWorkRequest + fetchWork .mockReset() // Last call (all the cellSets are hidden) return empty - .mockImplementationOnce(() => Promise.resolve({ data: noCellsGeneExpression })); - - const hideAllCellsPromise = louvainClusterKeys.map(async (cellSetKey) => { - storeState.dispatch(setCellSetHiddenStatus(cellSetKey)); - }); + .mockImplementationOnce(() => noCellsGeneExpression); await act(async () => { + const hideAllCellsPromise = louvainClusterKeys.map(async (cellSetKey) => { + storeState.dispatch(setCellSetHiddenStatus(cellSetKey)); + }); await Promise.all(hideAllCellsPromise); }); diff --git a/src/__test__/components/data-exploration/heatmap/__snapshots__/HeatmapPlot.test.jsx.snap b/src/__test__/components/data-exploration/heatmap/__snapshots__/HeatmapPlot.test.jsx.snap index 6d91689045..39768fbd75 100644 --- a/src/__test__/components/data-exploration/heatmap/__snapshots__/HeatmapPlot.test.jsx.snap +++ b/src/__test__/components/data-exploration/heatmap/__snapshots__/HeatmapPlot.test.jsx.snap @@ -1,70 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`HeatmapPlot Does not display hidden cell sets 1`] = ` -[ - "testae48e318dab9a1bd0bexperiment", - { - "downsampleSettings": { - "groupedTracks": [ - "louvain", - "sample", - ], - "hiddenCellSets": [ - "louvain-3", - ], - "selectedCellSet": "louvain", - "selectedPoints": "All", - }, - "downsampled": true, - "genes": [ - "Ms4a4b", - "Smc4", - "Ccr7", - "Ifi27l2a", - "Gm8369", - "S100a4", - "S100a6", - "Tmem176a", - "Tmem176b", - "Cxcr6", - "5830411N06Rik", - "Lmo4", - "Il18r1", - "Atp2b1", - "Pde5a", - "Ccl5", - "Nkg7", - "Klrd1", - "AW112010", - "Klrc1", - "Gzma", - "Stmn1", - "Hmgn2", - "Pclaf", - "Tuba1b", - "Lyz2", - "Ifitm3", - "Fcer1g", - "Tyrobp", - "Cst3", - "Cd74", - "Igkc", - "Cd79a", - "H2-Ab1", - "H2-Eb1", - ], - "name": "GeneExpression", - }, - 180, - "Ms4a4b-Smc4-Ccr7-Ifi27l2a-Gm8369-S100a4-S100a6-Tmem176a-Tmem176b-Cxcr6-5830411N06Rik-Lmo4-Il18r1-Atp2b1-Pde5a-Ccl5-Nkg7-Klrd1-AW112010-Klrc1-Gzma-Stmn1-Hmgn2-Pclaf-Tuba1b-Lyz2-Ifitm3-Fcer1g-Tyrobp-Cst3-Cd74-Igkc-Cd79a-H2-Ab1-H2-Eb1-expression", - { - "ETagPipelineRun": "2021-10-20T12:51:44.755Z", - "broadcast": false, - }, - [Function], -] -`; - exports[`HeatmapPlot Reacts to cellClass groupby being changed 1`] = ` Uint8Array [ 90, diff --git a/src/__test__/components/data-management/__snapshots__/DownloadDataButton.test.jsx.snap b/src/__test__/components/data-management/__snapshots__/DownloadDataButton.test.jsx.snap index 512e30f7b7..7a6bbc51d6 100644 --- a/src/__test__/components/data-management/__snapshots__/DownloadDataButton.test.jsx.snap +++ b/src/__test__/components/data-management/__snapshots__/DownloadDataButton.test.jsx.snap @@ -22,7 +22,6 @@ exports[`DownloadDataButton Downloads processed matrix properly 1`] = ` [ "testae48e318dab9a1bd0bexperiment-0", { - "embeddingETag": "40425699abc7c16fbf3430b3d4ff3f3e", "name": "DownloadAnnotSeuratObject", }, [Function], diff --git a/src/__test__/components/data-processing/configure-embedding/ConfigureEmbedding.test.jsx b/src/__test__/components/data-processing/configure-embedding/ConfigureEmbedding.test.jsx index b3379a0ac2..dba241dbab 100644 --- a/src/__test__/components/data-processing/configure-embedding/ConfigureEmbedding.test.jsx +++ b/src/__test__/components/data-processing/configure-embedding/ConfigureEmbedding.test.jsx @@ -9,13 +9,13 @@ import mockAPI, { statusResponse, promiseResponse, generateDefaultMockAPIResponses, - dispatchWorkRequestMock, } from '__test__/test-utils/mockAPI'; import cellSetsData from '__test__/data/cell_sets.json'; import { MAX_LEGEND_ITEMS } from 'components/plots/helpers/PlotLegendAlert'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import mockEmbedding from '__test__/data/embedding.json'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; @@ -36,19 +36,7 @@ const embeddingPreviewNumOfUmisPlotUuid = generateDataProcessingPlotUuid(null, f enableFetchMocks(); -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => `${ETagParams.body.name}`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { GetEmbedding: mockEmbedding, @@ -97,18 +85,9 @@ describe('Configure Embedding', () => { fetchMock.resetMocks(); fetchMock.doMock(); - dispatchWorkRequest + fetchWork .mockReset() - // Call for GetEmbedding - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // Call for GetMitochondrialContent - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // Call for GetDoubletScore - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // Call for GetNGenes - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // Call for GetNUmis - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); fetchMock.mockIf(/.*/, mockAPI(mockApiResponses)); storeState = makeStore(); diff --git a/src/__test__/components/data-processing/data-integration/DataIntegration.test.jsx b/src/__test__/components/data-processing/data-integration/DataIntegration.test.jsx index ba14cc0bd4..adecc739bd 100644 --- a/src/__test__/components/data-processing/data-integration/DataIntegration.test.jsx +++ b/src/__test__/components/data-processing/data-integration/DataIntegration.test.jsx @@ -9,13 +9,13 @@ import mockAPI, { statusResponse, promiseResponse, generateDefaultMockAPIResponses, - dispatchWorkRequestMock, } from '__test__/test-utils/mockAPI'; import cellSetsData from '__test__/data/cell_sets.json'; import { MAX_LEGEND_ITEMS } from 'components/plots/helpers/PlotLegendAlert'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import mockEmbedding from '__test__/data/embedding.json'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; @@ -34,19 +34,7 @@ const frequencyPlotTitle = 'Frequency plot coloured by sample'; enableFetchMocks(); -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => `${ETagParams.body.name}`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { GetEmbedding: mockEmbedding, @@ -86,9 +74,9 @@ describe('DataIntegration', () => { fetchMock.resetMocks(); fetchMock.doMock(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); fetchMock.mockIf(/.*/, mockAPI(mockApiResponses)); storeState = makeStore(); diff --git a/src/__test__/components/plots/CategoricalEmbeddingPlot.test.jsx b/src/__test__/components/plots/CategoricalEmbeddingPlot.test.jsx index 9c30bfe7b6..f82af62704 100644 --- a/src/__test__/components/plots/CategoricalEmbeddingPlot.test.jsx +++ b/src/__test__/components/plots/CategoricalEmbeddingPlot.test.jsx @@ -10,12 +10,13 @@ import CategoricalEmbeddingPlot from 'components/plots/CategoricalEmbeddingPlot' import { initialPlotConfigStates } from 'redux/reducers/componentConfig/initialState'; import mockAPI, { - generateDefaultMockAPIResponses, statusResponse, delayedResponse, dispatchWorkRequestMock, + generateDefaultMockAPIResponses, statusResponse, delayedResponse, } from '__test__/test-utils/mockAPI'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import fake from '__test__/test-utils/constants'; import mockEmbedding from '__test__/data/embedding.json'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import { loadBackendStatus } from 'redux/actions/backendStatus'; import WorkResponseError from 'utils/errors/http/WorkResponseError'; @@ -23,21 +24,7 @@ enableFetchMocks(); const experimentId = fake.EXPERIMENT_ID; -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = () => 'embedding'; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { embedding: mockEmbedding, @@ -74,9 +61,9 @@ describe('Categorical embedding plot', () => { fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(defaultAPIResponse)); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementationOnce(() => mockWorkerResponses.embedding); storeState = makeStore(); await storeState.dispatch(loadBackendStatus(experimentId)); @@ -130,7 +117,7 @@ describe('Categorical embedding plot', () => { }); it('Shows a loader if embedding data is loading', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => delayedResponse({ body: 'Not found', status: 404 }, 4000)); @@ -141,7 +128,7 @@ describe('Categorical embedding plot', () => { }); it('Shows an error if there is an error fetching embedding', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => Promise.reject(new WorkResponseError('some random error'))); diff --git a/src/__test__/components/plots/ContinuousEmbeddingPlot.test.jsx b/src/__test__/components/plots/ContinuousEmbeddingPlot.test.jsx index f02ecef6e0..2e567a2344 100644 --- a/src/__test__/components/plots/ContinuousEmbeddingPlot.test.jsx +++ b/src/__test__/components/plots/ContinuousEmbeddingPlot.test.jsx @@ -6,7 +6,8 @@ import { makeStore } from 'redux/store'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import * as PlotSpecGenerators from 'utils/plotSpecs/generateEmbeddingContinuousSpec'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import { loadBackendStatus } from 'redux/actions/backendStatus'; import ContinuousEmbeddingPlot from 'components/plots/ContinuousEmbeddingPlot'; @@ -14,7 +15,7 @@ import ContinuousEmbeddingPlot from 'components/plots/ContinuousEmbeddingPlot'; import { initialPlotConfigStates } from 'redux/reducers/componentConfig/initialState'; import mockAPI, { - generateDefaultMockAPIResponses, statusResponse, delayedResponse, dispatchWorkRequestMock, + generateDefaultMockAPIResponses, statusResponse, delayedResponse, } from '__test__/test-utils/mockAPI'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import fake from '__test__/test-utils/constants'; @@ -28,21 +29,7 @@ enableFetchMocks(); const experimentId = fake.EXPERIMENT_ID; -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = () => 'embedding'; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { embedding: mockEmbedding, @@ -88,9 +75,9 @@ describe('Continuous embedding plot', () => { fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(defaultAPIResponse)); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementationOnce(() => mockWorkerResponses.embedding); storeState = makeStore(); await storeState.dispatch(loadBackendStatus(experimentId)); @@ -185,7 +172,7 @@ describe('Continuous embedding plot', () => { }); it('Shows a loader if embedding data is loading', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => delayedResponse({ body: 'Not found', status: 404 }, 4000)); @@ -196,7 +183,7 @@ describe('Continuous embedding plot', () => { }); it('Shows an error if there is an error fetching embedding', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => Promise.reject(new WorkResponseError('some random error'))); diff --git a/src/__test__/components/plots/TrajectoryAnalysisPlot.test.jsx b/src/__test__/components/plots/TrajectoryAnalysisPlot.test.jsx index bb5e5d28aa..f928ec611f 100644 --- a/src/__test__/components/plots/TrajectoryAnalysisPlot.test.jsx +++ b/src/__test__/components/plots/TrajectoryAnalysisPlot.test.jsx @@ -8,7 +8,7 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import TrajectoryAnalysisPlot from 'components/plots/TrajectoryAnalysisPlot'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, + generateDefaultMockAPIResponses, promiseResponse, statusResponse, } from '__test__/test-utils/mockAPI'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import fake from '__test__/test-utils/constants'; @@ -16,7 +16,8 @@ import mockEmbedding from '__test__/data/embedding.json'; import mockStartingNodes from '__test__/data/starting_nodes.json'; import mockProcessingConfig from '__test__/data/processing_config.json'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import { loadBackendStatus } from 'redux/actions/backendStatus'; import { loadPlotConfig } from 'redux/actions/componentConfig'; import getTrajectoryPlotStartingNodes from 'redux/actions/componentConfig/getTrajectoryPlotStartingNodes'; @@ -31,21 +32,7 @@ enableFetchMocks(); const experimentId = fake.EXPERIMENT_ID; -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => `${ETagParams.body.name}`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { GetTrajectoryAnalysisStartingNodes: mockStartingNodes, @@ -116,9 +103,9 @@ describe('Trajectory analysis plot', () => { fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(defaultAPIResponse)); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); storeState = makeStore(); await storeState.dispatch(loadBackendStatus(experimentId)); diff --git a/src/__test__/components/plots/ViolinPlot.test.jsx b/src/__test__/components/plots/ViolinPlot.test.jsx index 764f390390..c174e64683 100644 --- a/src/__test__/components/plots/ViolinPlot.test.jsx +++ b/src/__test__/components/plots/ViolinPlot.test.jsx @@ -7,7 +7,8 @@ import { act } from 'react-dom/test-utils'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import '__test__/test-utils/setupTests'; import { loadBackendStatus } from 'redux/actions/backendStatus'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import expressionDataFAKEGENE from '__test__/data/gene_expression_FAKEGENE.json'; import ViolinPlot from 'components/plots/ViolinPlotMain'; @@ -22,7 +23,6 @@ import endUserMessages from 'utils/endUserMessages'; import _ from 'lodash'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, statusResponse, } from '__test__/test-utils/mockAPI'; @@ -30,26 +30,11 @@ import mockAPI, { import fake from '__test__/test-utils/constants'; import { plotTypes } from 'utils/constants'; -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = () => 'list-genes'; - const mockGeneExpressionETag = () => 'gene-expression'; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { - 'list-genes': null, - 'gene-expression': expressionDataFAKEGENE, + ListGenes: null, + GeneExpression: expressionDataFAKEGENE, }; const plotType = plotTypes.VIOLIN_PLOT; @@ -88,9 +73,9 @@ describe('ViolinPlot', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); enableFetchMocks(); fetchMock.resetMocks(); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap index 7b12555ec8..08eb46c0ce 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap @@ -476,13 +476,11 @@ exports[`Dot plot page Should show a no data error if user is using marker gene "dispersions", ], }, - 900, - "paginated-gene-expression", + [Function], + [Function], { - "PipelineRunETag": "2021-10-20T12:51:44.755Z", - "broadcast": false, + "timeout": 900, }, - [Function], ], [ "testae48e318dab9a1bd0bexperiment", @@ -513,13 +511,11 @@ exports[`Dot plot page Should show a no data error if user is using marker gene "numberOfMarkers": 3, "useMarkerGenes": false, }, - 180, - "dot-plot-data", + [Function], + [Function], { - "PipelineRunETag": "2021-10-20T12:51:44.755Z", - "broadcast": false, + "timeout": 180, }, - [Function], ], [ "testae48e318dab9a1bd0bexperiment", @@ -550,13 +546,11 @@ exports[`Dot plot page Should show a no data error if user is using marker gene "numberOfMarkers": 3, "useMarkerGenes": true, }, - 180, - "dot-plot-data", + [Function], + [Function], { - "PipelineRunETag": "2021-10-20T12:51:44.755Z", - "broadcast": false, + "timeout": 180, }, - [Function], ], [ "testae48e318dab9a1bd0bexperiment", @@ -587,13 +581,11 @@ exports[`Dot plot page Should show a no data error if user is using marker gene "numberOfMarkers": 3, "useMarkerGenes": true, }, - 180, - "dot-plot-data", + [Function], + [Function], { - "PipelineRunETag": "2021-10-20T12:51:44.755Z", - "broadcast": false, + "timeout": 180, }, - [Function], ], ] -`; \ No newline at end of file +`; diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx index 7aefed4be6..2eaa0e485a 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx @@ -2,7 +2,9 @@ import React from 'react'; import _ from 'lodash'; import { act } from 'react-dom/test-utils'; -import { render, screen, fireEvent, within, waitFor } from '@testing-library/react'; +import { + render, screen, fireEvent, within, waitFor, +} from '@testing-library/react'; import { mount } from 'enzyme'; import '@testing-library/jest-dom'; @@ -12,13 +14,13 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import fake from '__test__/test-utils/constants'; import mockAPI, { - dispatchWorkRequestMock, + // dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, } from '__test__/test-utils/mockAPI'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import { makeStore } from 'redux/store'; @@ -46,27 +48,11 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => { - if (ETagParams.body.name === 'ListGenes') return 'paginated-gene-expression'; - if (ETagParams.body.name === 'DotPlot') return 'dot-plot-data'; - }; - const mockGeneExpressionETag = (ETagParams) => `${ETagParams.missingGenesBody.genes.join('-')}-expression`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { - 'paginated-gene-expression': paginatedGeneExpressionData, - 'dot-plot-data': dotPlotData, + ListGenes: paginatedGeneExpressionData, + DotPlot: dotPlotData, }; const experimentId = fake.EXPERIMENT_ID; @@ -133,9 +119,9 @@ describe('Dot plot page', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(mockAPIResponse)); @@ -156,7 +142,7 @@ describe('Dot plot page', () => { it('Renders the plot page correctly', async () => { await renderDotPlot(storeState); - // There is the text Dot plot show in the breadcrumbs + // screen.debug(null, Infinity); // There is the text Dot plot show in the breadcrumbs expect(screen.getByText(new RegExp(plotNames.DOT_PLOT, 'i'))).toBeInTheDocument(); // It has the required dropdown options @@ -205,13 +191,12 @@ describe('Dot plot page', () => { it('Shows platform error if there are errors fetching the work', async () => { const errorResponse = { ...mockWorkerResponses, - 'dot-plot-data': () => { throw new Error('error'); }, + DotPlot: () => { throw new Error('error'); }, }; - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(errorResponse)) - .mockImplementationOnce(dispatchWorkRequestMock(errorResponse)); + .mockImplementation((_experimentId, body) => errorResponse[body.name]); await renderDotPlot(storeState); @@ -222,7 +207,7 @@ describe('Dot plot page', () => { it('Shows an empty message if there is no data to show in the plot', async () => { const emptyResponse = { ...mockWorkerResponses, - 'dot-plot-data': { + DotPlot: { cellSetsIdx: [], cellSetsNames: [], cellsPercentage: [], @@ -232,9 +217,9 @@ describe('Dot plot page', () => { }, }; - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(emptyResponse)); + .mockImplementation((_experimentId, body) => emptyResponse[body.name]); await renderDotPlot(storeState); @@ -243,15 +228,11 @@ describe('Dot plot page', () => { }); it('Should show a no data error if user is using marker gene and selected filter sets are not represented in more than 1 group in the base cell set', async () => { - dispatchWorkRequest - .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); - await renderDotPlot(storeState); // Call to list genes await waitFor(() => { - expect(dispatchWorkRequest).toHaveBeenCalledTimes(2); + expect(fetchWork).toHaveBeenCalledTimes(2); }); // Use marker genes @@ -260,7 +241,7 @@ describe('Dot plot page', () => { }); // Call to load dot plot - expect(dispatchWorkRequest).toHaveBeenCalledTimes(3); + expect(fetchWork).toHaveBeenCalledTimes(3); // Select data userEvent.click(screen.getByText(/Select data/i)); @@ -274,7 +255,7 @@ describe('Dot plot page', () => { // Call to load dot plot await waitFor(() => { - expect(dispatchWorkRequest).toHaveBeenCalledTimes(4); + expect(fetchWork).toHaveBeenCalledTimes(4); }); // Select the filter sets @@ -295,10 +276,10 @@ describe('Dot plot page', () => { }); // No new calls to load dot plot - expect(dispatchWorkRequest).toHaveBeenCalledTimes(4); + expect(fetchWork).toHaveBeenCalledTimes(4); // Calls are correct - expect(dispatchWorkRequest.mock.calls).toMatchSnapshot(); + expect(fetchWork.mock.calls).toMatchSnapshot(); }); it('removing a gene keeps the order', async () => { @@ -396,15 +377,6 @@ describe('Dot plot page', () => { }); it('resets the data', async () => { - dispatchWorkRequest - .mockReset() - // 1st call to list genes - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // 2nd call to load dot plot - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // 3rd call to load dot plot - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); - await renderDotPlot(storeState); // add a gene to prepare for reset @@ -438,13 +410,6 @@ describe('Drag and drop enzyme tests', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest - .mockReset() - // 1st call to list genes - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // 2nd call to paginated gene expression - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); - fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(mockAPIResponse)); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-categorical/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-categorical/index.test.jsx index 8019dd1bfa..b37cb6f0cc 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-categorical/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-categorical/index.test.jsx @@ -7,18 +7,19 @@ import { act } from 'react-dom/test-utils'; import { Provider } from 'react-redux'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import mockEmbedding from '__test__/data/embedding.json'; import preloadAll from 'jest-next-dynamic'; import fake from '__test__/test-utils/constants'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, } from '__test__/test-utils/mockAPI'; + import cellSetsData from '__test__/data/cell_sets.json'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import { MAX_LEGEND_ITEMS } from 'components/plots/helpers/PlotLegendAlert'; @@ -30,21 +31,7 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => `${ETagParams.body.name}`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { GetEmbedding: mockEmbedding, @@ -91,9 +78,9 @@ describe('Categorical embedding plot', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); enableFetchMocks(); fetchMock.resetMocks(); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-continuous/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-continuous/index.test.jsx index 2c94b27904..758c6adc0b 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-continuous/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/embedding-continuous/index.test.jsx @@ -7,7 +7,8 @@ import { act } from 'react-dom/test-utils'; import { Provider } from 'react-redux'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import markerGenes1 from '__test__/data/marker_genes_1.json'; import mockEmbedding from '__test__/data/embedding.json'; @@ -17,7 +18,6 @@ import preloadAll from 'jest-next-dynamic'; import fake from '__test__/test-utils/constants'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, @@ -31,25 +31,10 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (EtagParams) => `${EtagParams.body.name}`; - const mockGeneExpressionETag = () => '1-marker-genes'; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { - '1-marker-genes': markerGenes1, + GeneExpression: markerGenes1, ListGenes: paginatedGeneExpressionData, GetEmbedding: mockEmbedding, }; @@ -94,9 +79,9 @@ describe('Continuous embedding plot', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); enableFetchMocks(); fetchMock.resetMocks(); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx index 6d8a415fd8..e97c9eaabb 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx @@ -11,7 +11,8 @@ import { Provider } from 'react-redux'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { loadDownsampledGeneExpression } from 'redux/actions/genes'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import markerGenesData2 from '__test__/data/marker_genes_2.json'; import markerGenesData5 from '__test__/data/marker_genes_5.json'; import markerGenesData5AndFakeGene from '__test__/data/marker_genes_5_and_FAKE_gene.json'; @@ -21,7 +22,6 @@ import preloadAll from 'jest-next-dynamic'; import fake from '__test__/test-utils/constants'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, @@ -38,22 +38,6 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => { - if (ETagParams.body.name === 'ListGenes') return 'ListGenes'; - return `${ETagParams.body.nGenes}-marker-genes`; - }; - - const mockGeneExpressionETag = (ETagParams) => `${ETagParams.missingGenesBody.genes.join('-')}-expression`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - // Disable local cache jest.mock('localforage', () => ({ getItem: () => Promise.resolve(undefined), @@ -63,21 +47,11 @@ jest.mock('localforage', () => ({ length: () => 0, })); -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); - -const fakeGenesETag = 'Ms4a4b-Smc4-Ccr7-Ifi27l2a-Gm8369-S100a4-S100a6-Tmem176a-Tmem176b-Cxcr6-5830411N06Rik-Lmo4-Il18r1-Atp2b1-Pde5a-Ccl5-Nkg7-Klrd1-AW112010-Klrc1-Gzma-Stmn1-Hmgn2-Pclaf-Tuba1b-Lyz2-Ifitm3-Fcer1g-Tyrobp-Cst3-Cd74-Igkc-Cd79a-H2-Ab1-H2-Eb1-FAKEGENE-expression'; -const fakeGenesETag1 = 'Ms4a4b-Smc4-Ccr7-Ifi27l2a-Gm8369-S100a4-S100a6-Tmem176b-Cxcr6-5830411N06Rik-Lmo4-Il18r1-Atp2b1-Pde5a-Ccl5-Nkg7-Klrd1-AW112010-Klrc1-Gzma-Stmn1-Hmgn2-Pclaf-Tuba1b-Lyz2-Ifitm3-Fcer1g-Tyrobp-Cst3-Cd74-Igkc-Cd79a-H2-Ab1-H2-Eb1-expression'; -const fakeGenesETag2 = 'Ms4a4b-Smc4-Ccr7-Ifi27l2a-Gm8369-S100a4-S100a6-Tmem176b-Cxcr6-5830411N06Rik-Lmo4-Il18r1-Atp2b1-Pde5a-Ccl5-Nkg7-Klrd1-AW112010-Klrc1-Gzma-Stmn1-Hmgn2-Pclaf-Tuba1b-Lyz2-Ifitm3-Fcer1g-Tyrobp-Cst3-Cd74-Igkc-Cd79a-H2-Ab1-H2-Eb1-Tmem176a-expression'; +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { - '5-marker-genes': markerGenesData5, - '2-marker-genes': markerGenesData2, - [fakeGenesETag]: markerGenesData5AndFakeGene, - [fakeGenesETag1]: markerGenesData5AndFakeGene, - [fakeGenesETag2]: markerGenesData5AndFakeGene, + 'MarkerHeatmap-5': markerGenesData5, + GeneExpression: markerGenesData5AndFakeGene, ListGenes: geneList, }; @@ -125,9 +99,12 @@ describe('Marker heatmap plot', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => { + const reqType = body.nGenes ? `${body.name}-${body.nGenes}` : body.name; + return mockWorkerResponses[reqType]; + }); fetchMock.resetMocks(); fetchMock.doMock(); @@ -160,13 +137,14 @@ describe('Marker heatmap plot', () => { }); it('Shows an error message if marker genes failed to load', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementation( - (_experimentId, _body, _timeout, ETag) => { - if (ETag === '5-marker-genes') return Promise.reject(new Error('Not found')); + (_experimentId, body) => { + const reqType = body.nGenes ? `${body.name}-${body.nGenes}` : body.name; + if (reqType === 'MarkerHeatmap-5') return Promise.reject(new Error('Not found')); - return workerDataResult(mockWorkerResponses[ETag]); + return workerDataResult(mockWorkerResponses[reqType]); }, ); @@ -213,12 +191,7 @@ describe('Marker heatmap plot', () => { }); it('adds genes correctly into the plot', async () => { - dispatchWorkRequest - .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); - await renderHeatmapPage(storeState); - // Add in a new gene const genesToLoad = [...markerGenesData5.orderedGeneNames, 'FAKEGENE']; @@ -258,13 +231,15 @@ describe('Marker heatmap plot', () => { }); it('Shows an error message if gene expression fails to load', async () => { - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation((_experimentId, _body, _timeout, ETag) => { - if (ETag === '5-marker-genes' || ETag === 'ListGenes') return workerDataResult(mockWorkerResponses[ETag]); - - if (ETag === 'FAKEGENE-expression') { return Promise.reject(new Error('Not found')); } - }); + .mockImplementation( + (_experimentId, body) => { + const reqType = body.nGenes ? `${body.name}-${body.nGenes}` : body.name; + if (reqType === 'GeneExpression') return Promise.reject(new Error('Not found')); + return mockWorkerResponses[reqType]; + }, + ); await renderHeatmapPage(storeState); @@ -377,17 +352,13 @@ describe('Marker heatmap plot', () => { // check the selected gene is being loaded await waitFor(() => { - expect(dispatchWorkRequest).toHaveBeenCalledWith( - experimentId, - expect.objectContaining({ - name: 'GeneExpression', - genes: expect.arrayContaining(['Tmem176a']), - }), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), + // Check if there is a call with 'GeneExpression' and containing 'Tmem176a' in the genes array + const hasGeneExpressionWithTmem176a = fetchWork.mock.calls.some( + (call) => call[1].name === 'GeneExpression' && call[1].genes.includes('Tmem176a'), ); + + // Assert that such a call exists + expect(hasGeneExpressionWithTmem176a).toBeTruthy(); }); }); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx index 46b0ca6c5d..712901ad1d 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx @@ -7,9 +7,9 @@ import { act } from 'react-dom/test-utils'; import { Provider } from 'react-redux'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import expressionDataFAKEGENE from '__test__/data/gene_expression_FAKEGENE.json'; -import markerGenesData2 from '__test__/data/marker_genes_2.json'; import markerGenesData5 from '__test__/data/marker_genes_5.json'; import geneList from '__test__/data/paginated_gene_expression.json'; @@ -17,7 +17,6 @@ import preloadAll from 'jest-next-dynamic'; import fake from '__test__/test-utils/constants'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, @@ -33,22 +32,6 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => { - if (ETagParams.body.name === 'ListGenes') return 'ListGenes'; - return `${ETagParams.body.nGenes}-marker-genes`; - }; - - const mockGeneExpressionETag = (ETagParams) => `${ETagParams.missingGenesBody.genes.join('-')}-expression`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - // Disable local cache jest.mock('localforage', () => ({ getItem: () => Promise.resolve(undefined), @@ -58,14 +41,10 @@ jest.mock('localforage', () => ({ length: () => 0, })); -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { - '5-marker-genes': markerGenesData5, - '2-marker-genes': markerGenesData2, + MarkerHeatmap: markerGenesData5, 'FAKEGENE-expression': expressionDataFAKEGENE, ListGenes: geneList, }; @@ -79,10 +58,7 @@ const customAPIResponses = { if (req.method === 'PUT') return promiseResponse(JSON.stringify('OK')); return statusResponse(404, 'Not Found'); }, - [`/v2/workRequest/${experimentId}/5-marker-genes$`]: () => statusResponse(200, 'OK'), - [`/v2/workRequest/${experimentId}/2-marker-genes$`]: () => statusResponse(200, 'OK'), - [`/v2/workRequest/${experimentId}/FAKEGENE-expression$`]: () => statusResponse(200, 'OK'), - [`/v2/workRequest/${experimentId}/ListGenes$`]: () => statusResponse(200, 'OK'), + [`/v2/workRequest/${experimentId}`]: () => statusResponse(200, 'OK'), }; const defaultResponses = _.merge( @@ -127,9 +103,9 @@ describe('Drag and drop enzyme tests', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); enableFetchMocks(); fetchMock.resetMocks(); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/trajectory-analysis/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/trajectory-analysis/index.test.jsx index 15619445da..b614c3f319 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/trajectory-analysis/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/trajectory-analysis/index.test.jsx @@ -9,7 +9,8 @@ import { Vega } from 'react-vega'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import mockEmbedding from '__test__/data/embedding.json'; import mockStartingNodes from '__test__/data/starting_nodes.json'; import mockPseudoTime from '__test__/data/pseudotime.json'; @@ -25,7 +26,6 @@ import mockAPI, { promiseResponse, statusResponse, delayedResponse, - dispatchWorkRequestMock, } from '__test__/test-utils/mockAPI'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import userEvent from '@testing-library/user-event'; @@ -38,21 +38,7 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = (ETagParams) => `${ETagParams.body.name}`; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); jest.mock('react-vega', () => { const originalModule = jest.requireActual('react-vega'); @@ -145,11 +131,9 @@ describe('Trajectory analysis plot', () => { beforeEach(async () => { jest.clearAllMocks(); - - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementation(dispatchWorkRequestMock(mockWorkerResponses)); - + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); enableFetchMocks(); fetchMock.resetMocks(); fetchMock.doMock(); @@ -337,7 +321,7 @@ describe('Trajectory analysis plot', () => { }); it('Shows a loader if embedding data is loading', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => delayedResponse({ body: 'Not found', status: 404 }, 4000)); @@ -350,7 +334,7 @@ describe('Trajectory analysis plot', () => { }); it('Shows an error if there is an error fetching embedding', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => Promise.reject(new WorkResponseError('some random error'))); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/violin/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/violin/index.test.jsx index 60121f9dc3..db519128b3 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/violin/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/violin/index.test.jsx @@ -8,17 +8,16 @@ import { act } from 'react-dom/test-utils'; import { Provider } from 'react-redux'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import markerGenes1 from '__test__/data/marker_genes_1.json'; import paginatedGeneExpressionData from '__test__/data/paginated_gene_expression.json'; import fake from '__test__/test-utils/constants'; import mockAPI, { - dispatchWorkRequestMock, generateDefaultMockAPIResponses, promiseResponse, statusResponse, - workerDataResult, } from '__test__/test-utils/mockAPI'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; @@ -29,26 +28,11 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -// Mock hash so we can control the ETag that is produced by hash.MD5 when fetching work requests -// EtagParams is the object that's passed to the function which generates ETag in fetchWork -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = () => 'list-genes'; - const mockGeneExpressionETag = () => '1-marker-genes'; - - return mockWorkResultETag(objectHash, mockWorkRequestETag, mockGeneExpressionETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); const mockWorkerResponses = { - 'list-genes': paginatedGeneExpressionData, - '1-marker-genes': markerGenes1, + ListGenes: paginatedGeneExpressionData, + GeneExpression: markerGenes1, }; const experimentId = fake.EXPERIMENT_ID; @@ -91,12 +75,9 @@ describe('ViolinIndex', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() - // 1st call to list genes - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)) - // 2nd call to load gene expression - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(defaultResponses)); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/volcano/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/volcano/index.test.jsx index 2e2acf3dea..a2265ea27f 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/volcano/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/volcano/index.test.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import _ from 'lodash'; @@ -12,7 +12,6 @@ import mockAPI, { promiseResponse, statusResponse, delayedResponse, - dispatchWorkRequestMock, } from '__test__/test-utils/mockAPI'; import volcanoPlotPage from 'pages/experiments/[experimentId]/plots-and-tables/volcano'; @@ -25,7 +24,8 @@ import mockDiffExprResult from '__test__/data/differential_expression_0_All_WT1. import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import fake from '__test__/test-utils/constants'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; + import { plotNames } from 'utils/constants'; enableFetchMocks(); @@ -41,24 +41,12 @@ jest.mock('react-resize-detector', () => (props) => { return children({ width: 800, height: 800 }); }); -jest.mock('object-hash', () => { - const objectHash = jest.requireActual('object-hash'); - const mockWorkResultETag = jest.requireActual('__test__/test-utils/mockWorkResultETag'); - - const mockWorkRequestETag = () => 'differential-expression'; - - return mockWorkResultETag(objectHash, mockWorkRequestETag); -}); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, - dispatchWorkRequest: jest.fn(() => true), -})); +jest.mock('utils/work/fetchWork'); jest.mock('@aws-amplify/auth', () => ({})); const mockWorkerResponses = { - 'differential-expression': mockDiffExprResult, + DifferentialExpression: mockDiffExprResult, }; const customAPIResponses = { @@ -110,14 +98,14 @@ const runComparison = async () => { describe('Volcano plot page', () => { beforeEach(async () => { - dispatchWorkRequest.mockClear(); + fetchWork.mockClear(); fetchMock.resetMocks(); fetchMock.mockIf(/.*/, mockAPI(defaultResponses)); - dispatchWorkRequest + fetchWork .mockReset() - .mockImplementationOnce(dispatchWorkRequestMock(mockWorkerResponses)); + .mockImplementation((_experimentId, body) => mockWorkerResponses[body.name]); storeState = makeStore(); await storeState.dispatch(loadBackendStatus(experimentId)); @@ -160,7 +148,7 @@ describe('Volcano plot page', () => { }); it('Shows loader if diff expression is still loading', async () => { - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => delayedResponse({ body: 'Not found', status: 404 })); @@ -177,7 +165,7 @@ describe('Volcano plot page', () => { }); it('Shows platform error if loading diff expression result failed ', async () => { - dispatchWorkRequest + fetchWork .mockReset() // TODO this is weird, take a look later .mockImplementationOnce(() => promiseResponse('Not Found', 404)); diff --git a/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap b/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap index 09cf2f7e86..b5216f6030 100644 --- a/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap +++ b/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap @@ -14,9 +14,7 @@ exports[`runCellSetsClustering action Dispatches all required actions to update "type": "louvain", }, 60, - "mock-hash", { - "PipelineRunETag": "2021-01-01T00:00:00", "broadcast": true, }, [Function], @@ -38,9 +36,7 @@ exports[`runCellSetsClustering action Dispatches error action when dispatchWorkR "type": "louvain", }, 60, - "mock-hash", { - "PipelineRunETag": "2021-01-01T00:00:00", "broadcast": true, }, [Function], diff --git a/src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js b/src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js index 027e95fdaa..35c01516d2 100644 --- a/src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js +++ b/src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js @@ -3,27 +3,20 @@ import thunk from 'redux-thunk'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import runCellSetsClustering from 'redux/actions/cellSets/runCellSetsClustering'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import dispatchWorkRequest from 'utils/work/dispatchWorkRequest'; import initialState from 'redux/reducers/cellSets/initialState'; enableFetchMocks(); const mockStore = configureStore([thunk]); -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, // this property makes it work - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/dispatchWorkRequest'); jest.mock('utils/getTimeoutForWorkerTask', () => ({ __esModule: true, // this property makes it work default: () => 60, })); -jest.mock('object-hash', () => ({ - MD5: () => 'mock-hash', -})); - const startDate = '2021-01-01T00:00:00'; describe('runCellSetsClustering action', () => { diff --git a/src/__test__/redux/actions/embeddings/__snapshots__/loadEmbedding.defaultParams.test.js.snap b/src/__test__/redux/actions/embeddings/__snapshots__/loadEmbedding.defaultParams.test.js.snap deleted file mode 100644 index b4d6871d9d..0000000000 --- a/src/__test__/redux/actions/embeddings/__snapshots__/loadEmbedding.defaultParams.test.js.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`loadEmbedding action loadEmbedding generates correct hash params body for ETag: embeddings - hash call params 1`] = ` -[ - [ - { - "body": { - "config": { - "distanceMetric": "cosine", - "minimumDistance": 0.3, - }, - "name": "GetEmbedding", - "type": "umap", - "useSaved": undefined, - }, - "cacheUniquenessKey": null, - "experimentId": "6463cb35-3e08-4e94-a181-6d155a5ca570", - "extraDependencies": [], - "extras": undefined, - "qcPipelineStartDate": "2016-03-22T04:00:00.000Z", - "workerVersion": 4, - }, - ], -] -`; diff --git a/src/__test__/redux/actions/embeddings/loadEmbedding.defaultParams.test.js b/src/__test__/redux/actions/embeddings/loadEmbedding.defaultParams.test.js deleted file mode 100644 index 3038216a8c..0000000000 --- a/src/__test__/redux/actions/embeddings/loadEmbedding.defaultParams.test.js +++ /dev/null @@ -1,111 +0,0 @@ -import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import mockAPI, { - generateDefaultMockAPIResponses, - promiseResponse, -} from '__test__/test-utils/mockAPI'; - -import { MD5 } from 'object-hash'; -import _ from 'lodash'; -import backendStatusData from '__test__/data/backend_status.json'; -import createObjectHash from 'utils/work/createObjectHash'; -import generateExperimentSettingsMock from '__test__/test-utils/experimentSettings.mock'; -import { loadBackendStatus } from 'redux/actions/backendStatus'; -import { loadEmbedding } from 'redux/actions/embedding'; -import loadProcessingSettings from 'redux/actions/experimentSettings/processingConfig/loadProcessingSettings'; -import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; - -jest.mock('utils/getTimeoutForWorkerTask', () => ({ - __esModule: true, // this property makes it work - default: () => 60, -})); -enableFetchMocks(); -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, // this property makes it work - dispatchWorkRequest: jest.fn(() => true), -})); - -jest.mock('utils/work/createObjectHash'); - -const experimentId = '6463cb35-3e08-4e94-a181-6d155a5ca570'; -const sampleIds = ['sample-WT', 'sample-WT1', 'sample-KO']; -const initialExperimentSettingsState = generateExperimentSettingsMock(sampleIds); - -const experimentSettings = { - ...initialExperimentSettingsState, - processing: { - ...initialExperimentSettingsState.processing, - configureEmbedding: { - clusteringSettings: { - method: 'louvain', - methodSettings: { - louvain: { - resolution: 0.8, - }, - }, - }, - embeddingSettings: { - method: 'umap', - methodSettings: { - tsne: { perplexity: 30, learningRate: 200 }, - umap: { distanceMetric: 'cosine', minimumDistance: 0.3 }, - }, - }, - }, - }, -}; - -// this is the same Date used in the API to make sure the default ETag generated -// by the UI is identical to the API -const date = new Date(1458619200000); -backendStatusData.pipeline.startDate = date.toISOString(); - -const customAPIResponses = { - [`experiments/${experimentId}/processingConfig$`]: () => promiseResponse(JSON.stringify(experimentSettings.processing)), - [`experiments/${experimentId}/backendStatus$`]: () => promiseResponse( - JSON.stringify(backendStatusData), - ), -}; - -const mockApiResponses = _.merge( - generateDefaultMockAPIResponses(experimentId), customAPIResponses, -); - -describe('loadEmbedding action', () => { - let store; - - beforeEach(async () => { - jest.clearAllMocks(); - fetchMock.resetMocks(); - fetchMock.doMock(); - fetchMock.mockIf(/.*/, mockAPI(mockApiResponses)); - - dispatchWorkRequest - .mockReset() - .mockImplementationOnce(() => Promise.resolve([[1, 2], [3, 4]])); - - createObjectHash.mockImplementation((object) => MD5(object)); - - store = makeStore(); - await store.dispatch(loadProcessingSettings(experimentId)); - await store.dispatch(loadBackendStatus(experimentId)); - }); - - it('loadEmbedding generates correct hash params body for ETag', async () => { - const hashMock = jest.fn((object) => MD5(object)); - createObjectHash.mockImplementation(hashMock); - - await store.dispatch(loadEmbedding( - experimentId, - experimentSettings.processing.configureEmbedding.embeddingSettings.method, - )); - expect(hashMock).toHaveBeenCalled(); - // this snapshot should match exactly API snapshot: - // submitMarkerHeatmapWork.test.js.snap - expect(hashMock.mock.calls).toMatchSnapshot('embeddings - hash call params'); - // this ETag should match exactly the one in - // submitMarkerHeatmap.test.js - const ETag = hashMock.mock.results[0].value; - expect(ETag).toEqual('5c144d6e44aa4e09497a4bc5b12a285c'); // pragma: allowlist secret` - }); -}); diff --git a/src/__test__/redux/actions/embeddings/loadEmbedding.test.js b/src/__test__/redux/actions/embeddings/loadEmbedding.test.js index b32a196eda..f98e1427cb 100644 --- a/src/__test__/redux/actions/embeddings/loadEmbedding.test.js +++ b/src/__test__/redux/actions/embeddings/loadEmbedding.test.js @@ -4,24 +4,16 @@ import { loadEmbedding } from 'redux/actions/embedding'; import { initialEmbeddingState } from 'redux/reducers/embeddings/initialState'; import generateExperimentSettingsMock from '__test__/test-utils/experimentSettings.mock'; -import { - EMBEDDINGS_ERROR, - EMBEDDINGS_LOADING, -} from 'redux/actionTypes/embeddings'; - -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; import { workerDataResult } from '__test__/test-utils/mockAPI'; +jest.mock('utils/work/fetchWork'); + jest.mock('utils/getTimeoutForWorkerTask', () => ({ __esModule: true, // this property makes it work default: () => 60, })); -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, // this property makes it work - dispatchWorkRequest: jest.fn(), -})); - const mockStore = configureStore([thunk]); const embeddingType = 'umap'; @@ -55,7 +47,7 @@ describe('loadEmbedding action', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => workerDataResult([[1, 2], [3, 4]])); }); @@ -92,7 +84,7 @@ describe('loadEmbedding action', () => { }, ); - store.dispatch(loadEmbedding(experimentId, embeddingType)); + await store.dispatch(loadEmbedding(experimentId, embeddingType)); expect(store.getActions().length).toEqual(0); }); @@ -106,7 +98,7 @@ describe('loadEmbedding action', () => { }, ); - store.dispatch(loadEmbedding(experimentId, embeddingType)); + await store.dispatch(loadEmbedding(experimentId, embeddingType)); expect(store.getActions().length).toEqual(0); }); @@ -175,7 +167,7 @@ describe('loadEmbedding action', () => { }, ); - dispatchWorkRequest + fetchWork .mockReset() .mockImplementationOnce(() => { throw new Error('random error!'); }); @@ -214,33 +206,6 @@ describe('loadEmbedding action', () => { expect(store.getActions().length).toEqual(0); }); - it('Dispatches error if pipeline has not been run', async () => { - const store = mockStore( - { - backendStatus: { - ...backendStatus, - 1234: { - ...backendStatus['1234'], - status: {}, - }, - }, - networkResources: { - environment: 'testing', - }, - embeddings: {}, - experimentSettings: { - ...experimentSettings, - }, - }, - ); - - await store.dispatch(loadEmbedding(experimentId, embeddingType)); - - const [first, second] = store.getActions(); - expect(first.type).toBe(EMBEDDINGS_LOADING); - expect(second.type).toBe(EMBEDDINGS_ERROR); - }); - it('Dispatches on if forceReload is set to true', async () => { const store = mockStore( { diff --git a/src/__test__/redux/actions/genes/__snapshots__/loadMarkerGenes.defaultParams.test.js.snap b/src/__test__/redux/actions/genes/__snapshots__/loadMarkerGenes.defaultParams.test.js.snap deleted file mode 100644 index 204f45d591..0000000000 --- a/src/__test__/redux/actions/genes/__snapshots__/loadMarkerGenes.defaultParams.test.js.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`loadEmbedding action loadEmbedding generates correct hash params body for ETag: marker heatmap - hash call params 1`] = ` -[ - [ - { - "body": { - "downsampleSettings": { - "groupedTracks": [ - "sample", - "louvain", - ], - "hiddenCellSets": [], - "selectedCellSet": "louvain", - "selectedPoints": "All", - }, - "nGenes": 5, - "name": "MarkerHeatmap", - }, - "cacheUniquenessKey": null, - "experimentId": "6463cb35-3e08-4e94-a181-6d155a5ca570", - "extraDependencies": [ - { - "method": "louvain", - "methodSettings": { - "louvain": { - "resolution": 0.8, - }, - }, - }, - [ - [ - "louvain-0", - "louvain-1", - "louvain-2", - "louvain-3", - "louvain-4", - "louvain-5", - "louvain-6", - "louvain-7", - "louvain-8", - "louvain-9", - "louvain-10", - "louvain-11", - "louvain-12", - "louvain-13", - ], - [ - "louvain-0", - "louvain-1", - "louvain-2", - "louvain-3", - "louvain-4", - "louvain-5", - "louvain-6", - "louvain-7", - "louvain-8", - "louvain-9", - "louvain-10", - "louvain-11", - "louvain-12", - "louvain-13", - "b62028a1-ffa0-4f10-823d-93c9ddb88898", - "ab568662-27fa-462c-9435-625594341314", - "98c7a0ed-d086-4df8-bf94-a9ee6edb793f", - "Track_1-KMeta", - "Track_1-WMetaT", - ], - ], - 1, - ], - "extras": undefined, - "qcPipelineStartDate": "2016-03-22T04:00:00.000Z", - "workerVersion": 4, - }, - ], -] -`; diff --git a/src/__test__/redux/actions/genes/__snapshots__/loadPaginatedGeneProperties.test.js.snap b/src/__test__/redux/actions/genes/__snapshots__/loadPaginatedGeneProperties.test.js.snap index 434e973f30..3dab3b6379 100644 --- a/src/__test__/redux/actions/genes/__snapshots__/loadPaginatedGeneProperties.test.js.snap +++ b/src/__test__/redux/actions/genes/__snapshots__/loadPaginatedGeneProperties.test.js.snap @@ -84,12 +84,10 @@ exports[`loadPaginatedGeneProperties action Dispatches appropriately on success "c", ], }, - 60, - "1529f02c2d88b6e0592c21c9c3840397", + [Function], + [Function], { - "PipelineRunETag": "2021-01-01T01:01:01.000Z", - "broadcast": false, + "timeout": 60, }, - [Function], ] `; diff --git a/src/__test__/redux/actions/genes/loadMarkerGenes.defaultParams.test.js b/src/__test__/redux/actions/genes/loadMarkerGenes.defaultParams.test.js deleted file mode 100644 index 3021165267..0000000000 --- a/src/__test__/redux/actions/genes/loadMarkerGenes.defaultParams.test.js +++ /dev/null @@ -1,113 +0,0 @@ -import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import mockAPI, { - generateDefaultMockAPIResponses, - promiseResponse, - workerDataResult, -} from '__test__/test-utils/mockAPI'; - -import { MD5 } from 'object-hash'; -import _ from 'lodash'; -import backendStatusData from '__test__/data/backend_status.json'; -import createObjectHash from 'utils/work/createObjectHash'; -import generateExperimentSettingsMock from '__test__/test-utils/experimentSettings.mock'; -import { loadBackendStatus } from 'redux/actions/backendStatus'; -import { loadMarkerGenes } from 'redux/actions/genes'; -import loadProcessingSettings from 'redux/actions/experimentSettings/processingConfig/loadProcessingSettings'; -import { makeStore } from 'redux/store'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; -import { loadCellSets } from 'redux/actions/cellSets'; - -jest.mock('utils/getTimeoutForWorkerTask', () => ({ - __esModule: true, // this property makes it work - default: () => 60, -})); - -enableFetchMocks(); - -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, // this property makes it work - dispatchWorkRequest: jest.fn(), -})); - -jest.mock('utils/work/createObjectHash'); - -const experimentId = '6463cb35-3e08-4e94-a181-6d155a5ca570'; -const sampleIds = ['sample-WT', 'sample-WT1', 'sample-KO']; -const initialExperimentSettingsState = generateExperimentSettingsMock(sampleIds); - -const experimentSettings = { - ...initialExperimentSettingsState, - processing: { - ...initialExperimentSettingsState.processing, - configureEmbedding: { - clusteringSettings: { - method: 'louvain', - methodSettings: { - louvain: { - resolution: 0.8, - }, - }, - }, - embeddingSettings: { - method: 'umap', - methodSettings: { - tsne: { perplexity: 30, learningRate: 200 }, - umap: { distanceMetric: 'cosine', minimumDistance: 0.3 }, - }, - }, - }, - }, -}; - -// this is the same Date used in the API to make sure the default ETag generated -// by the UI is identical to the API -const date = new Date(1458619200000); -backendStatusData.pipeline.startDate = date.toISOString(); - -const customAPIResponses = { - [`experiments/${experimentId}/processingConfig$`]: () => promiseResponse(JSON.stringify(experimentSettings.processing)), - [`experiments/${experimentId}/backendStatus$`]: () => promiseResponse( - JSON.stringify(backendStatusData), - ), -}; - -const mockApiResponses = _.merge( - generateDefaultMockAPIResponses(experimentId), customAPIResponses, -); - -describe('loadEmbedding action', () => { - let store; - - beforeEach(async () => { - jest.clearAllMocks(); - fetchMock.resetMocks(); - fetchMock.doMock(); - fetchMock.mockIf(/.*/, mockAPI(mockApiResponses)); - - dispatchWorkRequest - .mockReset() - .mockImplementationOnce(() => workerDataResult([[1, 2], [3, 4]])); - createObjectHash.mockImplementation((object) => MD5(object)); - - store = makeStore(); - await store.dispatch(loadProcessingSettings(experimentId)); - await store.dispatch(loadBackendStatus(experimentId)); - await store.dispatch(loadCellSets(experimentId)); - }); - - it('loadEmbedding generates correct hash params body for ETag', async () => { - const hashMock = jest.fn((object) => MD5(object)); - createObjectHash.mockImplementation(hashMock); - - await store.dispatch(loadMarkerGenes(experimentId, 'interactiveHeatmap')); - - expect(hashMock).toHaveBeenCalled(); - // this snapshot should max exactly API snapshot: - // submitMarkerHeatmapWork.test.js.snap - expect(hashMock.mock.calls).toMatchSnapshot('marker heatmap - hash call params'); - // this ETag should match exactly the one in - // submitMarkerHeatmap.test.js - const ETag = hashMock.mock.results[0].value; - expect(ETag).toEqual('5a0e619279cd36c96c0ffd89d0d86d46'); // pragma: allowlist secret` - }); -}); diff --git a/src/__test__/redux/actions/genes/loadMarkerGenes.test.js b/src/__test__/redux/actions/genes/loadMarkerGenes.test.js index 06817c9d77..5d10a58b2c 100644 --- a/src/__test__/redux/actions/genes/loadMarkerGenes.test.js +++ b/src/__test__/redux/actions/genes/loadMarkerGenes.test.js @@ -10,7 +10,7 @@ import '__test__/test-utils/setupTests'; import { getOneGeneMatrix } from '__test__/utils/ExpressionMatrix/testMatrixes'; jest.mock('utils/work/fetchWork'); - +jest.mock('utils/work/getCellSetsThatAffectDownsampling'); jest.mock('utils/getTimeoutForWorkerTask', () => ({ __esModule: true, // this property makes it work default: () => 60, diff --git a/src/__test__/redux/actions/genes/loadPaginatedGeneProperties.test.js b/src/__test__/redux/actions/genes/loadPaginatedGeneProperties.test.js index 00edbd1271..847ac1dac1 100644 --- a/src/__test__/redux/actions/genes/loadPaginatedGeneProperties.test.js +++ b/src/__test__/redux/actions/genes/loadPaginatedGeneProperties.test.js @@ -11,15 +11,12 @@ import { GENES_PROPERTIES_ERROR, } from 'redux/actionTypes/genes'; -import { dispatchWorkRequest } from 'utils/work/seekWorkResponse'; +import fetchWork from 'utils/work/fetchWork'; import mockAPI, { generateDefaultMockAPIResponses, workerDataResult } from '__test__/test-utils/mockAPI'; import processingConfigData from '__test__/data/processing_config.json'; -jest.mock('utils/work/seekWorkResponse', () => ({ - __esModule: true, // this property makes it work - dispatchWorkRequest: jest.fn(), -})); +jest.mock('utils/work/fetchWork'); jest.mock('utils/getTimeoutForWorkerTask', () => ({ __esModule: true, // this property makes it work @@ -47,7 +44,7 @@ describe('loadPaginatedGeneProperties action', () => { beforeEach(async () => { jest.clearAllMocks(); - dispatchWorkRequest + fetchWork .mockReset() .mockImplementation(() => null); @@ -90,7 +87,7 @@ describe('loadPaginatedGeneProperties action', () => { }, }); - dispatchWorkRequest + fetchWork .mockReset() .mockImplementation(() => workerDataResult({ total: 2, @@ -123,7 +120,7 @@ describe('loadPaginatedGeneProperties action', () => { expect(loadedAction).toMatchSnapshot(); expect(loadedAction.type).toEqual(GENES_PROPERTIES_LOADED_PAGINATED); - const dispatchParams = dispatchWorkRequest.mock.calls[0]; + const dispatchParams = fetchWork.mock.calls[0]; expect(dispatchParams).toMatchSnapshot(); }); @@ -140,7 +137,7 @@ describe('loadPaginatedGeneProperties action', () => { }, }); - dispatchWorkRequest + fetchWork .mockReset() .mockImplementation(() => Promise.reject(new Error('random error!'))); diff --git a/src/__test__/test-utils/constants.js b/src/__test__/test-utils/constants.js index a6454f47c9..bdc75a642c 100644 --- a/src/__test__/test-utils/constants.js +++ b/src/__test__/test-utils/constants.js @@ -11,4 +11,5 @@ export default { // pragma: allowlist secret JWT_TOKEN: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6Ikpfake.eyJzdWIiOiIxMjM0NTY3ODkwIiwifakeZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTfakeM5MDIyfQ.fakexwRfakeKKF2QT4fwpMeJf36POk6yJV_adQsfake', MOCK_DATETIME: '0000-00-00T00:00:00.000Z', + ETAG: '380c53b4-mock-etag-8091-4c82336d6d49', }; diff --git a/src/__test__/test-utils/mockAPI.js b/src/__test__/test-utils/mockAPI.js index 3087448285..ebce71623e 100644 --- a/src/__test__/test-utils/mockAPI.js +++ b/src/__test__/test-utils/mockAPI.js @@ -30,17 +30,17 @@ const delayedResponse = (response, delay = 10000, options = {}) => new Promise(( }, delay); }); -const workerDataResult = (data) => Promise.resolve(_.cloneDeep({ data })); +const workerDataResult = (data) => Promise.resolve(_.cloneDeep(data)); -const dispatchWorkRequestMock = ( +const fetchWorkMock = ( mockedResults, -) => (_experimentId, _body, _timeout, ETag) => { - if (typeof mockedResults[ETag] === 'function') { - return mockedResults[ETag](); +) => ((experimentId, body) => { + if (typeof mockedResults[body.name] === 'function') { + return mockedResults[body.name](); } - return workerDataResult(mockedResults[ETag]); -}; + return workerDataResult(mockedResults[experimentId]); +}); const generateDefaultMockAPIResponses = (experimentId) => ({ [`experiments/${experimentId}$`]: () => promiseResponse( @@ -67,7 +67,7 @@ const generateDefaultMockAPIResponses = (experimentId) => ({ 'experiments/clone$': () => promiseResponse( JSON.stringify(fake.CLONED_EXPERIMENT_ID), ), - [`/v2/workRequest/${experimentId}/[^/]+$`]: () => statusResponse(200, 'OK'), + [`/v2/workRequest/${experimentId}`]: () => statusResponse(200, 'OK'), }); const mockAPI = (apiMapping) => (req) => { @@ -92,5 +92,5 @@ export { statusResponse, delayedResponse, workerDataResult, - dispatchWorkRequestMock, + fetchWorkMock, }; diff --git a/src/__test__/test-utils/mockWorkResultETag.js b/src/__test__/test-utils/mockWorkResultETag.js deleted file mode 100644 index c704ca7f51..0000000000 --- a/src/__test__/test-utils/mockWorkResultETag.js +++ /dev/null @@ -1,18 +0,0 @@ -const mockWorkResultETag = ( - objectHash, - workRequestETagGenerator, - geneExpressionETagGenerator, -) => ({ - ...objectHash, - MD5: (ETagParams) => { - if ('body' in ETagParams) { - return workRequestETagGenerator(ETagParams); - } - - if ('missingGenesBody' in ETagParams) { - return geneExpressionETagGenerator(ETagParams); - } - }, -}); - -export default mockWorkResultETag; diff --git a/src/__test__/test-utils/mockWorkerBackend.js b/src/__test__/test-utils/mockWorkerBackend.js index fafecc6ba2..e36a3aa112 100644 --- a/src/__test__/test-utils/mockWorkerBackend.js +++ b/src/__test__/test-utils/mockWorkerBackend.js @@ -1,18 +1,18 @@ import * as socketConnectionMocks from 'utils/socketConnection'; -import * as seekWorkResponseMocks from 'utils/work/seekWorkResponse'; +import * as fetchWorkResponseMocks from 'utils/work/fetchWork'; import SocketMock from 'socket.io-mock'; const socketMock = new SocketMock(); -jest.mock('utils/work/seekWorkResponse', () => { +jest.mock('utils/work/fetchWork', () => { const mockSeekFromS3 = jest.fn(); - const originalModule = jest.requireActual('utils/work/seekWorkResponse'); + const originalModule = jest.requireActual('utils/work/fetchWork'); return { __esModule: true, // Use it when dealing with esModules ...originalModule, - seekFromS3: mockSeekFromS3, + fetchWork: mockSeekFromS3, }; }); @@ -63,7 +63,7 @@ jest.mock('utils/socketConnection', () => { // To mock worker response, modify the response returned from the API // Set to null to force fetch from API -seekWorkResponseMocks.seekFromS3.mockImplementation(() => null); +fetchWorkResponseMocks.fetchWork.mockImplementation(() => null); // Set up socket emitter mock socketConnectionMocks.mockEmit.mockImplementation((workRequestType, requestBody) => { diff --git a/src/__test__/utils/work/__snapshots__/fetchWork.test.js.snap b/src/__test__/utils/work/__snapshots__/fetchWork.test.js.snap index 1115da3c93..aaab2cc543 100644 --- a/src/__test__/utils/work/__snapshots__/fetchWork.test.js.snap +++ b/src/__test__/utils/work/__snapshots__/fetchWork.test.js.snap @@ -1,122 +1,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`fetchWork runs correctly for gene expression work request 1`] = ` +exports[`fetchWork does not use cache for gene expression request 1`] = `undefined`; + +exports[`fetchWork does not use cache for gene expression request 2`] = ` { - "orderedGeneNames": [ - "A", - "B", - "C", - "D", + "S3Data": true, +} +`; + +exports[`fetchWork returns data from S3 directly if available 1`] = ` +[ + [ + "ListGenes", + "fakeSignedUrl", ], - "rawExpression": { - "datatype": undefined, - "index": [ - 0, - 0, - 2, - 0, - 2, - ], - "mathjs": "SparseMatrix", - "ptr": [ - 0, - 1, - 3, - 5, - ], - "size": [ - 3, - 3, - ], - "values": [ - 1, - 2, - 5, - 1, - 6, - ], - }, - "stats": { - "rawMean": [ - 0.3, - 2.3, - 1.3, - 14.2, - ], - "rawStdev": [ - 0.4, - 2, - 1, - 1.2, - ], - "truncatedMax": [ - 1, - 3, - 5, - 0.5, - ], - "truncatedMin": [ - 0, - 2, - 1, - 0.1, - ], - }, - "truncatedExpression": { - "datatype": undefined, - "index": [ - 0, - 0, - 2, - 0, - 2, - ], - "mathjs": "SparseMatrix", - "ptr": [ - 0, - 1, - 3, - 5, - ], - "size": [ - 3, - 3, - ], - "values": [ - 1, - 2, - 3, - 0.5, - 1, - ], - }, - "zScore": { - "datatype": undefined, - "index": [ - 0, - 0, - 2, - 0, - 2, - ], - "mathjs": "SparseMatrix", - "ptr": [ - 0, - 1, - 3, - 5, - ], - "size": [ - 3, - 3, - ], - "values": [ - 1, - 1, - 3, - 3, - 1, - ], - }, +] +`; + +exports[`fetchWork returns data from S3 directly if available 2`] = ` +{ + "S3Data": true, +} +`; + +exports[`fetchWork returns data from cache if available 1`] = ` +{ + "cacheData": true, } `; + +exports[`fetchWork waits and returns data from the worker 1`] = `[]`; + +exports[`fetchWork waits and returns data from the worker 2`] = ` +[ + [ + "fakeETag", + "testae48e318dab9a1bd0bexperiment", + null, + 10, + [Function], + ], +] +`; + +exports[`fetchWork waits and returns data from the worker 3`] = `true`; diff --git a/src/__test__/utils/work/__snapshots__/seekWorkResponse.test.js.snap b/src/__test__/utils/work/__snapshots__/seekWorkResponse.test.js.snap deleted file mode 100644 index 24d51f07db..0000000000 --- a/src/__test__/utils/work/__snapshots__/seekWorkResponse.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dispatchWorkRequest unit tests Sends work to the worker when called: EventNames 1`] = ` -[ - "WorkerInfo-testae48e318dab9a1bd0bexperiment", - "Heartbeat-testae48e318dab9a1bd0bexperiment", - "WorkResponse-facefeed", -] -`; diff --git a/src/__test__/utils/work/fetchWork.mock.js b/src/__test__/utils/work/fetchWork.mock.js index e1a016ae02..4bc6f1df3e 100644 --- a/src/__test__/utils/work/fetchWork.mock.js +++ b/src/__test__/utils/work/fetchWork.mock.js @@ -83,11 +83,11 @@ const mockGenesListData = ['A', 'B', 'C', 'D']; // return null; // }); -const mockCacheGet = jest.fn(() => null); +const mockCacheGet = jest.fn(); const mockCacheSet = jest.fn(); const mockCacheRemove = jest.fn(); -const mockDispatchWorkRequest = jest.fn(() => true); +const mockFetchWorkRequest = jest.fn(() => true); const mockSeekFromS3 = jest.fn(); @@ -97,16 +97,15 @@ const mockCacheModule = { _remove: jest.fn((key) => mockCacheRemove(key)), }; -const mockSeekWorkResponseModule = { +const mockFetchWorkModule = { __esModule: true, - seekFromS3: mockSeekFromS3, - dispatchWorkRequest: mockDispatchWorkRequest, + fetchWork: mockFetchWorkRequest, }; const mockQcPipelineStartDate = '2021-01-01T01:01:01.000Z'; export { mockGenesListData, mockCacheGet, mockCacheSet, - mockDispatchWorkRequest, mockSeekFromS3, - mockQcPipelineStartDate, mockCacheModule, mockSeekWorkResponseModule, + mockFetchWorkRequest, mockSeekFromS3, + mockQcPipelineStartDate, mockCacheModule, mockFetchWorkModule, }; diff --git a/src/__test__/utils/work/fetchWork.test.js b/src/__test__/utils/work/fetchWork.test.js index fe554c0356..2c0a79d87b 100644 --- a/src/__test__/utils/work/fetchWork.test.js +++ b/src/__test__/utils/work/fetchWork.test.js @@ -1,17 +1,18 @@ /* eslint-disable global-require */ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import mockAPI, { generateDefaultMockAPIResponses, workerDataResult } from '__test__/test-utils/mockAPI'; +import mockAPI, { generateDefaultMockAPIResponses } from '__test__/test-utils/mockAPI'; +import fake from '__test__/test-utils/constants'; import fetchWork from 'utils/work/fetchWork'; -import { getFourGenesMatrix } from '__test__/utils/ExpressionMatrix/testMatrixes'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { makeStore } from 'redux/store'; +import downloadFromS3 from 'utils/work/downloadFromS3'; +import waitForWorkRequest from 'utils/work/waitForWorkRequest'; +import dispatchWorkRequest from 'utils/work/dispatchWorkRequest'; const { - mockGenesListData, mockCacheGet, mockCacheSet, - mockDispatchWorkRequest, mockSeekFromS3, } = require('__test__/utils/work/fetchWork.mock'); @@ -19,16 +20,18 @@ jest.mock( 'utils/cache', () => require('__test__/utils/work/fetchWork.mock').mockCacheModule, ); -jest.mock( - 'utils/work/seekWorkResponse', - () => require('__test__/utils/work/fetchWork.mock').mockSeekWorkResponseModule, -); -const experimentId = '1234'; -const NON_GENE_EXPRESSION_ETAG = 'bac72df9fc53b884b7ae1dfeb5356e01'; // pragma: allowlist secret -const GENE_EXPRESSION_ABCD_ETAG = '1732d6bcb5134d736e9f1ec36ec81c0d'; // pragma: allowlist secret -const timeout = 10; +jest.mock('utils/work/dispatchWorkRequest', () => jest.fn().mockReturnValue({ + ETag, + signedUrl: 'fakeSignedUrl', + request: null, +})); +jest.mock('utils/work/downloadFromS3'); +jest.mock('utils/work/waitForWorkRequest'); + +const timeout = 10; +const ETag = 'fakeETag'; const nonGeneExpressionWorkRequest = { name: 'ListGenes', }; @@ -48,180 +51,115 @@ describe('fetchWork', () => { jest.clearAllMocks(); - mockDispatchWorkRequest - .mockReset() - .mockImplementationOnce(() => workerDataResult(mockGenesListData)); - fetchMock.resetMocks(); - fetchMock.mockIf(/.*/, mockAPI(generateDefaultMockAPIResponses(experimentId))); + fetchMock.mockIf(/.*/, mockAPI(generateDefaultMockAPIResponses(fake.EXPERIMENT_ID))); store = makeStore(); - await store.dispatch(loadBackendStatus(experimentId)); + await store.dispatch(loadBackendStatus(fake.EXPERIMENT_ID)); }); - it('runs correctly for gene expression work request', async () => { - mockDispatchWorkRequest - .mockReset() - .mockImplementation(() => workerDataResult(getFourGenesMatrix())); + it('returns data from cache if available', async () => { + mockCacheGet.mockImplementationOnce(() => ({ cacheData: true })); const res = await fetchWork( - experimentId, - geneExpressionWorkRequest, + fake.EXPERIMENT_ID, + nonGeneExpressionWorkRequest, store.getState, store.dispatch, { timeout }, ); - // Temporarily disabled the cache for gene expression - expect(mockDispatchWorkRequest).toHaveBeenCalledWith( - experimentId, - { name: 'GeneExpression', genes: ['A', 'B', 'C', 'D'] }, - timeout, - GENE_EXPRESSION_ABCD_ETAG, - expect.anything(), - expect.anything(), - ); - - // The expected response should be fine - - // Disabled gene expression cache, so the whole thing is being loaded - // expect(mockCacheGet).toHaveBeenCalledTimes(4); - // expect(mockCacheSet).toHaveBeenCalledTimes(1); - // expect(mockCacheSet).toHaveBeenCalledWith( - // mockCacheKeyMappings.D, - // expectedResponse.D, - // ); + expect(mockCacheGet).toHaveBeenCalledWith(ETag); + expect(downloadFromS3).not.toHaveBeenCalled(); + expect(waitForWorkRequest).not.toHaveBeenCalled(); expect(res).toMatchSnapshot(); - expect(mockSeekFromS3).toHaveBeenCalledTimes(0); }); - it('runs correctly for non gene expression work request', async () => { + it('returns data from S3 directly if available', async () => { + downloadFromS3.mockReturnValueOnce({ + S3Data: true, + }); + const res = await fetchWork( - experimentId, + fake.EXPERIMENT_ID, nonGeneExpressionWorkRequest, store.getState, store.dispatch, { timeout }, ); - expect(mockDispatchWorkRequest).toHaveBeenCalledWith( - experimentId, - nonGeneExpressionWorkRequest, - timeout, - NON_GENE_EXPRESSION_ETAG, - expect.anything(), - expect.anything(), - ); - expect(mockCacheGet).toHaveBeenCalledTimes(1); - expect(mockCacheSet).toHaveBeenCalledTimes(1); - expect(mockCacheSet).toHaveBeenCalledWith( - NON_GENE_EXPRESSION_ETAG, - mockGenesListData, - ); - expect(mockSeekFromS3).toHaveBeenCalledTimes(0); - expect(res).toEqual(mockGenesListData); + expect(mockCacheGet).toHaveBeenCalledWith(ETag); + expect(downloadFromS3.mock.calls).toMatchSnapshot(); + expect(waitForWorkRequest).not.toHaveBeenCalled(); + expect(res).toMatchSnapshot(); }); - it('Throws an error if the dispatched work request throws an error', async () => { - mockDispatchWorkRequest - .mockReset() - .mockImplementationOnce(() => Promise.reject(new Error('Worker timeout'))); - - await expect( - fetchWork( - experimentId, - nonGeneExpressionWorkRequest, - store.getState, - store.dispatch, - { timeout: 10 }, - ), - ).rejects.toThrow(); - - expect(mockCacheGet).toHaveBeenCalledTimes(1); - expect(mockCacheSet).not.toHaveBeenCalled(); - - // Not called ever because result is received straight from dispatchWorkRequest - expect(mockSeekFromS3).toHaveBeenCalledTimes(0); - }); + it('waits and returns data from the worker', async () => { + dispatchWorkRequest.mockReturnValueOnce({ + ETag, + signedUrl: null, + request: null, + }); - it('does not change ETag if caching is enabled', async () => { - Storage.prototype.getItem = jest.fn((key) => (key === 'disableCache' ? 'false' : null)); + waitForWorkRequest.mockReturnValueOnce({ + workerSignedUrl: 'fakeWorkerSignedUrl', + data: true, + }); - await fetchWork( - experimentId, + const res = await fetchWork( + fake.EXPERIMENT_ID, nonGeneExpressionWorkRequest, store.getState, store.dispatch, { timeout }, ); - expect(mockDispatchWorkRequest).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - NON_GENE_EXPRESSION_ETAG, - expect.anything(), - expect.anything(), - ); + expect(mockCacheGet).toHaveBeenCalledWith(ETag); + expect(downloadFromS3.mock.calls).toMatchSnapshot(); + expect(waitForWorkRequest.mock.calls).toMatchSnapshot(); + expect(res).toMatchSnapshot(); }); - it('changes ETag if caching is disabled', async () => { - Storage.prototype.getItem = jest.fn((key) => (key === 'disableCache' ? 'true' : null)); + it('does not use cache for gene expression request', async () => { + downloadFromS3.mockReturnValueOnce({ + S3Data: true, + }); - await fetchWork( - experimentId, - nonGeneExpressionWorkRequest, + const res = await fetchWork( + fake.EXPERIMENT_ID, + geneExpressionWorkRequest, store.getState, store.dispatch, { timeout }, ); - expect(mockDispatchWorkRequest).not.toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - NON_GENE_EXPRESSION_ETAG, - expect.anything(), - ); + expect(mockCacheGet).not.toHaveBeenCalled(); + expect(mockCacheSet).not.toHaveBeenCalled(); + expect(downloadFromS3.calls).toMatchSnapshot(); + expect(waitForWorkRequest).not.toHaveBeenCalled(); + expect(res).toMatchSnapshot(); }); - it('Caching is disabled by default if environment is dev', async () => { - await fetchWork( - experimentId, - nonGeneExpressionWorkRequest, - store.getState, - store.dispatch, - { timeout }, - ); - - expect(mockDispatchWorkRequest).not.toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - NON_GENE_EXPRESSION_ETAG, - expect.anything(), - ); - }); + it('Throws an error if the dispatched work request throws an error', async () => { + dispatchWorkRequest + .mockReset() + .mockImplementationOnce(() => Promise.reject(new Error('Worker timeout'))); - it('Setting cache to false in development enables cache', async () => { - Storage.prototype.getItem = jest.fn((key) => (key === 'disableCache' ? 'false' : null)); + await expect( + fetchWork( + fake.EXPERIMENT_ID, + nonGeneExpressionWorkRequest, + store.getState, + store.dispatch, + { timeout: 10 }, + ), + ).rejects.toThrow(); - await fetchWork( - experimentId, - nonGeneExpressionWorkRequest, - store.getState, - store.dispatch, - { timeout }, - ); + expect(mockCacheGet).not.toHaveBeenCalled(); + expect(mockCacheSet).not.toHaveBeenCalled(); - expect(mockDispatchWorkRequest).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - NON_GENE_EXPRESSION_ETAG, - expect.anything(), - expect.anything(), - ); + // Not called ever because result is received straight from dispatchWorkRequest + expect(mockSeekFromS3).toHaveBeenCalledTimes(0); }); }); diff --git a/src/__test__/utils/work/generateETag.test.js b/src/__test__/utils/work/generateETag.test.js deleted file mode 100644 index 6031bc350b..0000000000 --- a/src/__test__/utils/work/generateETag.test.js +++ /dev/null @@ -1,94 +0,0 @@ -import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import mockAPI, { generateDefaultMockAPIResponses } from '__test__/test-utils/mockAPI'; - -import { Environment } from 'utils/deploymentInfo'; -import generateETag from 'utils/work/generateETag'; -import { makeStore } from 'redux/store'; -import { mockQcPipelineStartDate } from '__test__/utils/work/fetchWork.mock'; - -const NON_GENE_EXPRESSION_ETAG = '8203e3ba0d492753400d09168c0f0b03'; // pragma: allowlist secret -const GENE_EXPRESSION_D_ETAG = '8993e43f446701ebe837fdec97429fce'; // pragma: allowlist secret - -const experimentId = '1234'; -const mockExtras = undefined; - -const nonGeneExpressionWorkRequest = { - name: 'ListGenes', -}; - -enableFetchMocks(); - -describe('generateEtag', () => { - let store; - - beforeEach(() => { - jest.resetAllMocks(); - - global.Math.random = jest.fn(() => 1); - - fetchMock.resetMocks(); - fetchMock.mockIf(/.*/, mockAPI(generateDefaultMockAPIResponses(experimentId))); - - store = makeStore(); - }); - - it('Generates the correct ETag', async () => { - const ETag = await generateETag( - experimentId, - nonGeneExpressionWorkRequest, - mockExtras, - mockQcPipelineStartDate, - Environment.PRODUCTION, - store.dispatch, - store.getState, - ); - - expect(ETag).toEqual(NON_GENE_EXPRESSION_ETAG); - }); - - it('Generates the correct geneExpression ETag', async () => { - const ETag = await generateETag( - experimentId, - { name: 'GeneExpression', genes: ['D'] }, - mockExtras, - mockQcPipelineStartDate, - Environment.PRODUCTION, - store.dispatch, - store.getState, - ); - - expect(ETag).toEqual(GENE_EXPRESSION_D_ETAG); - }); - - it('Generates unique key for dev environment', async () => { - Storage.prototype.getItem = jest.fn(() => 'true'); - - await generateETag( - experimentId, - nonGeneExpressionWorkRequest, - mockExtras, - mockQcPipelineStartDate, - Environment.DEVELOPMENT, - store.dispatch, - store.getState, - ); - - expect(global.Math.random).toHaveBeenCalledTimes(1); - }); - - it('Does not generate a unique key for GetEmbedding in dev environment', async () => { - Storage.prototype.getItem = jest.fn(() => 'true'); - - await generateETag( - experimentId, - { name: 'GetEmbedding' }, - mockExtras, - mockQcPipelineStartDate, - Environment.DEVELOPMENT, - store.dispatch, - store.getState, - ); - - expect(global.Math.random).not.toHaveBeenCalled(); - }); -}); diff --git a/src/__test__/utils/work/seekWorkResponse.test.js b/src/__test__/utils/work/seekWorkResponse.test.js deleted file mode 100644 index 8536c9e281..0000000000 --- a/src/__test__/utils/work/seekWorkResponse.test.js +++ /dev/null @@ -1,210 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import SocketMock from 'socket.io-mock'; -import { dispatchWorkRequest, seekFromS3 } from 'utils/work/seekWorkResponse'; -import fake from '__test__/test-utils/constants'; -import mockAPI, { generateDefaultMockAPIResponses } from '__test__/test-utils/mockAPI'; - -import unpackResult from 'utils/work/unpackResult'; -import parseResult from 'utils/work/parseResult'; - -/** - * jest.mock calls are automatically hoisted to the top of the javascript - * during compilation. Accordingly, `mockEmit` and `mockOn` as exported - * from jest.mock will be accessible under `socketConnectionMocks`, even - * if they do not appear in the original file. - */ -import * as socketConnectionMocks from 'utils/socketConnection'; -import { waitFor } from '@testing-library/react'; - -enableFetchMocks(); -uuidv4.mockImplementation(() => 'my-random-uuid'); - -jest.mock('uuid'); -jest.mock('dayjs', () => () => jest.requireActual('dayjs')('4022-01-01T00:00:00.000Z')); -jest.mock('utils/socketConnection', () => { - const mockEmit = jest.fn(); - const mockOn = jest.fn(); - - return { - __esModule: true, - default: new Promise((resolve) => { - resolve({ emit: mockEmit, on: mockOn, id: '5678' }); - }), - mockEmit, - mockOn, - }; -}); - -jest.mock('utils/work/parseResult'); - -const mockResponseDataCompressed = 'mockResCompressed'; -const mockResponseDataDecompressed = 'mockResDecompressed'; - -jest.mock('utils/work/unpackResult', () => ({ - __esModule: true, - default: jest.fn(), - decompressUint8Array: jest.fn().mockImplementation(() => mockResponseDataDecompressed), -})); - -const taskName = 'GetEmbedding'; - -describe('dispatchWorkRequest unit tests', () => { - const experimentId = fake.EXPERIMENT_ID; - const timeout = 30; - const body = { - name: taskName, - type: 'fake task', - }; - - const dispatchMock = jest.fn(); - - beforeEach(async () => { - jest.clearAllMocks(); - - fetchMock.resetMocks(); - fetchMock.mockIf(/.*/, mockAPI(generateDefaultMockAPIResponses(fake.EXPERIMENT_ID))); - - const socketMock = new SocketMock(); - - parseResult.mockImplementationOnce(() => 'mockParsedResult'); - - socketConnectionMocks.mockEmit.mockImplementation((workRequestType, requestBody) => { - const podInfo = { - response: { - podInfo: { - name: 'worker-pod', - creationTimestamp: '2022-04-29T07:48:47.000Z', - phase: 'Pending', - }, - }, - }; - - // mockDecompressUint8Array.mockReturnValue(Promise.resolve(mockResponseDataDecompressed)); - - // This is a mocked response emit response from server - socketMock.socketClient.emit(`WorkResponse-${requestBody.ETag}`, mockResponseDataCompressed); - socketMock.socketClient.emit(`WorkerInfo-${fake.EXPERIMENT_ID}`, podInfo); - }); - - socketConnectionMocks.mockOn.mockImplementation((channel, socketCallback) => { - // This is a listener for the response from the server - socketMock.on(channel, (responseBody) => { - socketCallback(responseBody); - }); - }); - }); - - it('Sends work to the worker when called', async () => { - fetchMock.mockResponse(JSON.stringify({ signedUrl: 'http://www.apiUrl:portNum/path/blabla' })); - - const resultPromise = dispatchWorkRequest( - experimentId, body, timeout, 'facefeed', null, dispatchMock, - ); - - // Wait for subscription to WorkResponse-facefeed - await waitFor(() => { - expect(socketConnectionMocks.mockOn.mock.calls.length).toBeGreaterThan(0); - - const [, workResponseHandler] = socketConnectionMocks.mockOn.mock.calls.find(([channel]) => channel === 'WorkResponse-facefeed'); - - workResponseHandler(mockResponseDataCompressed); - }); - - const result = await resultPromise; - - expect(result).toEqual({ data: 'mockParsedResult' }); - - expect(parseResult).toHaveBeenCalledWith(mockResponseDataDecompressed); - - expect(socketConnectionMocks.mockOn).toHaveBeenCalledTimes(3); - expect(socketConnectionMocks.mockOn.mock.calls.map(([eventName]) => eventName)).toMatchSnapshot('EventNames'); - }); - - it('Returns an error if there is error in the response.', async () => { - socketConnectionMocks.mockOn.mockImplementation(async (channelName, cb) => { - let response = null; - - if (channelName.match('WorkerInfo')) { - response = { - podInfo: { - name: 'worker-pod', - creationTimestamp: '2022-04-29T07:48:47.000Z', - phase: 'Pending', - }, - }; - } - - if (channelName.match('WorkResponse')) { - response = { - error: true, - errorCode: 'MOCK_ERROR_CODE', - userMessage: 'Mock worker error message', - }; - } - - cb({ response }); - }); - - expect(async () => { - await dispatchWorkRequest(experimentId, body, timeout, 'facefeed', null, dispatchMock); - }).rejects.toEqual(new Error('MOCK_ERROR_CODE: Mock worker error message')); - }); -}); - -describe('seekFromS3 unit tests', () => { - const result = 'someResult'; - - const validSignedUrl = 'https://s3.mock/validSignedUrl'; - const invalidSignedUrl = 'https://s3.mock/invalidSignedUrl'; - const notFoundSignedUrl = 'https://s3.mock/notFoundSignedUrl'; - - const s3ErrorResponse = new Response('Forbidden', { status: 403 }); - const notFoundErrorResponse = new Response('NotFound', { status: 404 }); - - beforeAll(async () => { - fetchMock.mockIf(/.*/, (req) => { - const path = req.url; - - if (path.endsWith(validSignedUrl)) return Promise.resolve(result); - if (path.endsWith(invalidSignedUrl)) return Promise.resolve(s3ErrorResponse); - if (path.endsWith(notFoundSignedUrl)) return Promise.resolve(notFoundErrorResponse); - - return { - status: 500, - body: 'Something eror with test', - }; - }); - }); - - beforeEach(async () => { - jest.clearAllMocks(); - }); - - it('Should return null when response status is not found or forbidden', async () => { - let finalResult = await seekFromS3(taskName, notFoundSignedUrl); - - expect(finalResult).toBeNull(); - - // returns 403 - finalResult = await seekFromS3(taskName, invalidSignedUrl); - - expect(finalResult).toBeNull(); - }); - - it('Should return results correctly', async () => { - unpackResult.mockReturnValueOnce('mockUnpackedResult'); - parseResult.mockReturnValueOnce('mockParsedResult'); - - const finalResult = await seekFromS3(taskName, validSignedUrl); - - expect(finalResult).toEqual('mockParsedResult'); - - expect(unpackResult).toHaveBeenCalledTimes(1); - const response = unpackResult.mock.calls[0][0]; - const mockResponsePayload = await response.text(); - expect(mockResponsePayload).toEqual(result); - - expect(parseResult).toHaveBeenCalledWith('mockUnpackedResult', taskName); - }); -}); diff --git a/src/components/data-exploration/embedding/Embedding.jsx b/src/components/data-exploration/embedding/Embedding.jsx index 71c6bc05f0..af7c1adc7e 100644 --- a/src/components/data-exploration/embedding/Embedding.jsx +++ b/src/components/data-exploration/embedding/Embedding.jsx @@ -316,7 +316,7 @@ const Embedding = (props) => { { data ? ( { selectedGenes, cellSets, ); + setHeatmapData(data); }, [ selectedGenes, @@ -291,7 +292,7 @@ const HeatmapPlot = (props) => { ); } - if (selectedGenes.length === 0) { + if (selectedGenes?.length === 0) { return (
diff --git a/src/redux/actions/componentConfig/getTrajectoryPlotPseudoTime.js b/src/redux/actions/componentConfig/getTrajectoryPlotPseudoTime.js index 9a8d04134a..00ff893ea3 100644 --- a/src/redux/actions/componentConfig/getTrajectoryPlotPseudoTime.js +++ b/src/redux/actions/componentConfig/getTrajectoryPlotPseudoTime.js @@ -4,7 +4,6 @@ import handleError from 'utils/http/handleError'; import endUserMessages from 'utils/endUserMessages'; import fetchWork from 'utils/work/fetchWork'; import getTimeoutForWorkerTask from 'utils/getTimeoutForWorkerTask'; -import getEmbeddingETag from 'utils/work/getEmbeddingETag'; const getTrajectoryPlotPseudoTime = ( rootNodes, @@ -15,7 +14,6 @@ const getTrajectoryPlotPseudoTime = ( // Currenty monocle3 only trajectory analysis only supports // UMAP embedding. Therefore, this embedding is specifically fetched. const embeddingMethod = 'umap'; - const embeddingETag = await getEmbeddingETag(experimentId, getState, dispatch, embeddingMethod); const timeout = getTimeoutForWorkerTask(getState(), 'TrajectoryAnalysisPseudotime'); @@ -24,12 +22,13 @@ const getTrajectoryPlotPseudoTime = ( embeddingSettings, } = getState().experimentSettings.processing.configureEmbedding; + // the request needs the embeddingETag to merge that data with the rds + // the embeddingETag is added by the API to this body const body = { name: 'GetTrajectoryAnalysisPseudoTime', embedding: { method: embeddingMethod, methodSettings: embeddingSettings.methodSettings[embeddingMethod], - ETag: embeddingETag, }, clustering: { method: clusteringSettings.method, diff --git a/src/redux/actions/componentConfig/getTrajectoryPlotStartingNodes.js b/src/redux/actions/componentConfig/getTrajectoryPlotStartingNodes.js index 0d40c98dd5..252b26247b 100644 --- a/src/redux/actions/componentConfig/getTrajectoryPlotStartingNodes.js +++ b/src/redux/actions/componentConfig/getTrajectoryPlotStartingNodes.js @@ -1,10 +1,8 @@ import { PLOT_DATA_LOADED, PLOT_DATA_LOADING, PLOT_DATA_ERROR } from 'redux/actionTypes/componentConfig'; -import { getBackendStatus } from 'redux/selectors'; import handleError from 'utils/http/handleError'; import endUserMessages from 'utils/endUserMessages'; import fetchWork from 'utils/work/fetchWork'; -import generateETag from 'utils/work/generateETag'; import getTimeoutForWorkerTask from 'utils/getTimeoutForWorkerTask'; const getTrajectoryPlotStartingNodes = ( @@ -18,41 +16,18 @@ const getTrajectoryPlotStartingNodes = ( const { clusteringSettings, - embeddingSettings: { methodSettings, useSaved }, + embeddingSettings: { methodSettings }, } = getState().experimentSettings.processing.configureEmbedding; - const { environment } = getState().networkResources; - - const { - pipeline: - { startDate: qcPipelineStartDate }, - } = getBackendStatus(experimentId)(getState()).status; - - const embeddingBody = { - name: 'GetEmbedding', - type: embeddingMethod, - config: methodSettings[embeddingMethod], - useSaved, - }; - - const embeddingETag = await generateETag( - experimentId, - embeddingBody, - undefined, - qcPipelineStartDate, - environment, - dispatch, - getState, - ); - const timeout = getTimeoutForWorkerTask(getState(), 'TrajectoryAnalysisStartingNodes'); + // the request needs the embeddingETag to merge that data with the rds + // the embeddingETag is added by the API to this body const body = { name: 'GetTrajectoryAnalysisStartingNodes', embedding: { method: embeddingMethod, methodSettings: methodSettings[embeddingMethod], - ETag: embeddingETag, }, clustering: { method: clusteringSettings.method, diff --git a/src/redux/actions/embedding/loadEmbedding.js b/src/redux/actions/embedding/loadEmbedding.js index 0d4e0a44b5..6cf4f4ceb0 100644 --- a/src/redux/actions/embedding/loadEmbedding.js +++ b/src/redux/actions/embedding/loadEmbedding.js @@ -41,7 +41,7 @@ const loadEmbedding = ( config: methodSettings[embeddingType], }; - const timeout = getTimeoutForWorkerTask(getState(), 'GetEmbedding', { type: embeddingType, useSaved }); + const timeout = getTimeoutForWorkerTask(getState(), 'GetEmbedding'); try { const data = await fetchWork( diff --git a/src/redux/actions/genes/loadDownsampledGeneExpression.js b/src/redux/actions/genes/loadDownsampledGeneExpression.js index d6ef59e320..f6daa3b243 100644 --- a/src/redux/actions/genes/loadDownsampledGeneExpression.js +++ b/src/redux/actions/genes/loadDownsampledGeneExpression.js @@ -8,6 +8,7 @@ import { import fetchWork from 'utils/work/fetchWork'; import getTimeoutForWorkerTask from 'utils/getTimeoutForWorkerTask'; +import getCellSetsThatAffectDownsampling from 'utils/work/getCellSetsThatAffectDownsampling'; // Debounce so that we only fetch once the settings are done being set up const loadDownsampledGeneExpression = ( @@ -16,35 +17,40 @@ const loadDownsampledGeneExpression = ( componentUuid, withHiddenCellSets = false, ) => async (dispatch, getState) => { + if (genes.length === 0) { + dispatch({ + type: DOWNSAMPLED_GENES_EXPRESSION_LOADED, + payload: { + componentUuid, + genes, + }, + }); + + return; + } + const state = getState(); const { groupedTracks, - selectedCellSet, + selectedCellSet: selectedCellSetKey, selectedPoints, } = state.componentConfig[componentUuid]?.config; const hiddenCellSets = withHiddenCellSets ? Array.from(state.cellSets.hidden) : []; + const cellSets = await getCellSetsThatAffectDownsampling( + experimentId, selectedCellSetKey, groupedTracks, dispatch, getState, + ); + const downsampleSettings = { - selectedCellSet, + selectedCellSet: selectedCellSetKey, groupedTracks, + cellSets, selectedPoints, hiddenCellSets, }; - if (genes.length === 0) { - dispatch({ - type: DOWNSAMPLED_GENES_EXPRESSION_LOADED, - payload: { - componentUuid, - genes, - }, - }); - - return; - } - const body = { name: 'GeneExpression', genes, @@ -55,7 +61,7 @@ const loadDownsampledGeneExpression = ( const timeout = getTimeoutForWorkerTask(getState(), 'GeneExpression'); try { - let requestETag; + let requestETag = null; const { orderedGeneNames, diff --git a/src/redux/actions/genes/loadMarkerGenes.js b/src/redux/actions/genes/loadMarkerGenes.js index 5dd6ef6354..d10a4e1dd3 100644 --- a/src/redux/actions/genes/loadMarkerGenes.js +++ b/src/redux/actions/genes/loadMarkerGenes.js @@ -5,6 +5,7 @@ import { } from 'redux/actionTypes/genes'; import fetchWork from 'utils/work/fetchWork'; +import getCellSetsThatAffectDownsampling from 'utils/work/getCellSetsThatAffectDownsampling'; import getTimeoutForWorkerTask from 'utils/getTimeoutForWorkerTask'; import handleError from 'utils/http/handleError'; import endUserMessages from 'utils/endUserMessages'; @@ -15,14 +16,19 @@ const loadMarkerGenes = ( const { numGenes = 5, groupedTracks = ['sample', 'louvain'], - selectedCellSet = 'louvain', + selectedCellSetKey = 'louvain', selectedPoints = 'All', hiddenCellSets = [], } = options; + const cellSets = await getCellSetsThatAffectDownsampling( + experimentId, selectedCellSetKey, groupedTracks, dispatch, getState, + ); + const downsampleSettings = { - selectedCellSet, + selectedCellSet: selectedCellSetKey, groupedTracks, + cellSets, selectedPoints, hiddenCellSets: Array.from(hiddenCellSets), }; @@ -36,7 +42,8 @@ const loadMarkerGenes = ( try { const timeout = getTimeoutForWorkerTask(getState(), 'MarkerHeatmap'); - let requestETag; + // TODO ask martin if it's fine to use null as default + let requestETag = null; const { orderedGeneNames, diff --git a/src/utils/extraActionCreators/downloadProcessedMatrix.js b/src/utils/extraActionCreators/downloadProcessedMatrix.js index 3160c850df..051c126340 100644 --- a/src/utils/extraActionCreators/downloadProcessedMatrix.js +++ b/src/utils/extraActionCreators/downloadProcessedMatrix.js @@ -5,7 +5,6 @@ import fetchWork from 'utils/work/fetchWork'; import getTimeoutForWorkerTask from 'utils/getTimeoutForWorkerTask'; import writeToFileURL from 'utils/upload/writeToFileURL'; import downloadFromUrl from 'utils/downloadFromUrl'; -import getEmbeddingETag from 'utils/work/getEmbeddingETag'; import handleError from 'utils/http/handleError'; import endUserMessages from 'utils/endUserMessages'; @@ -21,9 +20,10 @@ const downloadProcessedMatrix = (experimentId) => async (dispatch, getState) => const taskName = 'DownloadAnnotSeuratObject'; + // the request needs the embeddingETag to merge that data with the rds + // the embeddingETag is added by the API to this body const body = { name: taskName, - embeddingETag: await getEmbeddingETag(experimentId, getState, dispatch), }; const timeout = getTimeoutForWorkerTask(getState(), taskName); diff --git a/src/utils/work/createObjectHash.js b/src/utils/work/createObjectHash.js deleted file mode 100644 index 2e6853265e..0000000000 --- a/src/utils/work/createObjectHash.js +++ /dev/null @@ -1,5 +0,0 @@ -import { MD5 } from 'object-hash'; - -const createObjectHash = (object) => MD5(object); - -export default createObjectHash; diff --git a/src/utils/work/dispatchWorkRequest.js b/src/utils/work/dispatchWorkRequest.js new file mode 100644 index 0000000000..6f48d7214c --- /dev/null +++ b/src/utils/work/dispatchWorkRequest.js @@ -0,0 +1,54 @@ +import dayjs from 'dayjs'; + +import fetchAPI from 'utils/http/fetchAPI'; + +const getWorkerTimeout = (taskName, defaultTimeout) => { + switch (taskName) { + case 'GetEmbedding': + case 'ListGenes': + case 'MarkerHeatmap': { + return dayjs().add(1800, 's').toISOString(); + } + + default: { + return dayjs().add(defaultTimeout, 's').toISOString(); + } + } +}; + +const dispatchWorkRequest = async ( + experimentId, + body, + timeout, + requestProps, +) => { + const { name: taskName } = body; + + // for listGenes, markerHeatmap, & getEmbedding we set a long timeout for the worker + // after that timeout the worker will skip those requests + // meanwhile in the UI we set a shorter timeout. The UI will be prolonging this timeout + // as long as it receives "heartbeats" from the worker because that means the worker is alive + // and progresing. + // this should be removed if we make each request run in a different worker + const workerTimeoutDate = getWorkerTimeout(taskName, timeout); + + const request = { + experimentId, + timeout: workerTimeoutDate, + body, + ...requestProps, + }; + + const { data: { ETag, signedUrl } } = await fetchAPI( + `/v2/workRequest/${experimentId}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }, + ); + + return { ETag, signedUrl, request }; +}; + +export default dispatchWorkRequest; diff --git a/src/utils/work/downloadFromS3.js b/src/utils/work/downloadFromS3.js new file mode 100644 index 0000000000..4bd1046e34 --- /dev/null +++ b/src/utils/work/downloadFromS3.js @@ -0,0 +1,29 @@ +import unpackResult from 'utils/work/unpackResult'; +import httpStatusCodes from 'utils/http/httpStatusCodes'; +import parseResult from 'utils/work/parseResult'; + +const downloadFromS3 = async (taskName, signedUrl) => { + const response = await fetch(signedUrl); + + // some WorkRequests like scType and runClustering do not upload data to S3 + // (nor return it via socket) instead they patch the cellsets through the API. + // In those cases the workResults will not exist, and it's fine because data will + // be updated through the cellsets. + // the forbidden (in addition to the not found) is required because of how signed URLs work + // when you try to download a file from a signedUrl that doesn't exist you get a 403 forbidden + // because the user is not authorized to access a file that does not exist + if (response.status === httpStatusCodes.NOT_FOUND + || response.status === httpStatusCodes.FORBIDDEN) { + return null; + } + if (!response.ok) { + throw new Error(`Error ${response.status}: ${response.text}`, { cause: response }); + } + + const unpackedResult = await unpackResult(response, taskName); + const parsedResult = parseResult(unpackedResult, taskName); + + return parsedResult; +}; + +export default downloadFromS3; diff --git a/src/utils/work/fetchWork.js b/src/utils/work/fetchWork.js index 74eb3cee04..db561462ec 100644 --- a/src/utils/work/fetchWork.js +++ b/src/utils/work/fetchWork.js @@ -1,59 +1,53 @@ import { Environment, isBrowser } from 'utils/deploymentInfo'; -import { getBackendStatus } from 'redux/selectors'; - import cache from 'utils/cache'; -import generateETag from 'utils/work/generateETag'; -import { dispatchWorkRequest, seekFromS3 } from 'utils/work/seekWorkResponse'; - -// Temporarily using gene expression without local cache -const fetchGeneExpressionWorkWithoutLocalCache = async ( - experimentId, +import dispatchWorkRequest from 'utils/work/dispatchWorkRequest'; +import downloadFromS3 from 'utils/work/downloadFromS3'; +import waitForWorkRequest from 'utils/work/waitForWorkRequest'; + +// retrieveData will try to get the data for the given experimentId and ETag from +// the fastest source possible. It will try to get the data in order from: +// 1. Browser cache +// 2. S3 (cache) via signedURL +// 3. Worker +// 3.1. Via socket (small data) +// 3.2. Via S3 (large data) via signedURL +const retrieveData = async (experimentId, + ETag, + signedUrl, + request, timeout, body, - backendStatus, - environment, - broadcast, - extras, - onETagGenerated, dispatch, - getState, -) => { - // If new genes are needed, construct payload, try S3 for results, - // and send out to worker if there's a miss. - const { pipeline: { startDate: qcPipelineStartDate } } = backendStatus; + useCache) => { + // 1. Check if we have the ETag in the browser cache (no worker) + const cachedData = useCache ? await cache.get(ETag) : null; + if (cachedData) { + return cachedData; + } - const ETag = await generateETag( + // 2. Check if data is cached in S3 so we can download from the signed URL (no worker) + if (signedUrl) { + return await downloadFromS3(body.name, signedUrl); + } + + // 3. If we don't have signedURL, wait for the worker to send us the data via + // - the data via socket + // - the signedURL to download the data from S3 + const { signedUrl: workerSignedUrl, data } = await waitForWorkRequest( + ETag, experimentId, - body, - extras, - qcPipelineStartDate, - environment, + request, + timeout, dispatch, - getState, ); - onETagGenerated(ETag); - - try { - const { signedUrl, data } = await dispatchWorkRequest( - experimentId, - body, - timeout, - ETag, - { - ETagPipelineRun: qcPipelineStartDate, - broadcast, - ...extras, - }, - dispatch, - ); - - return data ?? await seekFromS3(body.name, signedUrl); - } catch (error) { - console.error('Error dispatching work request: ', error); - throw error; + // 3.1. The worker send the data via socket because it's small enough + if (data) { + return data; } + // 3.2. The worker send a signedUrl to download the data + return await downloadFromS3(body.name, workerSignedUrl); }; const fetchWork = async ( @@ -70,78 +64,42 @@ const fetchWork = async ( onETagGenerated = () => { }, } = optionals; - const backendStatus = getBackendStatus(experimentId)(getState()).status; - - const { environment } = getState().networkResources; - if (!isBrowser) { throw new Error('Disabling network interaction on server'); } + const { environment } = getState().networkResources; if (environment === Environment.DEVELOPMENT && !localStorage.getItem('disableCache')) { localStorage.setItem('disableCache', 'true'); } - const { pipeline: { startDate: qcPipelineStartDate } } = backendStatus; - - if (body.name === 'GeneExpression') { - return fetchGeneExpressionWorkWithoutLocalCache( - experimentId, - timeout, - body, - backendStatus, - environment, - broadcast, - extras, - onETagGenerated, - dispatch, - getState, - ); - } - - const ETag = await generateETag( + // 1. Contact the API to get ETag and possible S3 signedURL + const { ETag, signedUrl, request } = await dispatchWorkRequest( experimentId, body, - extras, - qcPipelineStartDate, - environment, + timeout, + { + broadcast, + ...extras, + }, dispatch, - getState, ); onETagGenerated(ETag); - // First, let's try to fetch this information from the local cache. - const cachedData = await cache.get(ETag); + const useCache = body.name !== 'GeneExpression'; - if (cachedData) { - return cachedData; - } - - // If there is no response in S3, dispatch workRequest via the worker - try { - const { signedUrl, data } = await dispatchWorkRequest( - experimentId, - body, - timeout, - ETag, - { - PipelineRunETag: qcPipelineStartDate, - broadcast, - ...extras, - }, - dispatch, - ); - - const response = data ?? await seekFromS3(body.name, signedUrl); - - await cache.set(ETag, response); + // 2. Try to get the data from the fastest source possible + const data = await retrieveData( + experimentId, ETag, signedUrl, request, timeout, body, dispatch, useCache, + ); - return response; - } catch (error) { - console.error('Error dispatching work request', error); - throw error; + // 3. Cache the data in the browser + if (useCache) { + await cache.set(ETag, data); } + + return data; }; export default fetchWork; diff --git a/src/utils/work/generateETag.js b/src/utils/work/generateETag.js deleted file mode 100644 index c508690763..0000000000 --- a/src/utils/work/generateETag.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Environment } from 'utils/deploymentInfo'; -import config from 'config'; -import getExtraDependencies from 'utils/work/getExtraDependencies'; -import createObjectHash from './createObjectHash'; - -// Disable unique keys to reallow reuse of work results in development -const DISABLE_UNIQUE_KEYS = [ - 'GetEmbedding', -]; - -const generateETag = async ( - experimentId, - body, - extras, - qcPipelineStartDate, - environment, - dispatch, - getState, -) => { - // If caching is disabled, we add an additional randomized key to the hash so we never reuse - // past results. - let cacheUniquenessKey = null; - - if ( - environment !== Environment.PRODUCTION - && localStorage.getItem('disableCache') === 'true' - && !DISABLE_UNIQUE_KEYS.includes(body.name) - ) { - cacheUniquenessKey = Math.random(); - } - - const extraDependencies = await getExtraDependencies( - experimentId, body, dispatch, getState, - ); - - let ETagBody; - - // They `body` key to create ETAg for gene expression is different - // from the others, causing the generated ETag to be different - if (body.name === 'GeneExpression') { - ETagBody = { - experimentId, - missingGenesBody: body, - qcPipelineStartDate, - extras, - cacheUniquenessKey, - workerVersion: config.workerVersion, - extraDependencies, - }; - } else { - ETagBody = { - experimentId, - body, - qcPipelineStartDate, - extras, - cacheUniquenessKey, - workerVersion: config.workerVersion, - extraDependencies, - }; - } - - return createObjectHash(ETagBody); -}; - -export default generateETag; diff --git a/src/utils/work/getCellSetsThatAffectDownsampling.js b/src/utils/work/getCellSetsThatAffectDownsampling.js new file mode 100644 index 0000000000..2e1a8ce250 --- /dev/null +++ b/src/utils/work/getCellSetsThatAffectDownsampling.js @@ -0,0 +1,23 @@ +import loadCellSets from 'redux/actions/cellSets/loadCellSets'; +import { getCellSetsHierarchy, getCellSetsHierarchyByKeys } from 'redux/selectors'; + +// Check that the cell sets within the selected selectedCellSet and grouped tracks didn't change +// e.g., if cell set was deleted we can't use cache +const getCellSetsThatAffectDownsampling = async ( + experimentId, selectedCellSetKey, groupedTracks, dispatch, getState) => { + await dispatch(loadCellSets(experimentId)); + + const [{ children }] = getCellSetsHierarchyByKeys([selectedCellSetKey])(getState()); + + const selectedCellSetsKeys = children.map((cellSet) => cellSet.key); + + const groupedCellSetKeys = getCellSetsHierarchy(groupedTracks)(getState()) + .map((cellClass) => cellClass.children) + .flat() + .map(({ key }) => key); + + // Keep them in separate lists, they each represent different changes in the settings + return [selectedCellSetsKeys, groupedCellSetKeys]; +}; + +export default getCellSetsThatAffectDownsampling; diff --git a/src/utils/work/getEmbeddingETag.js b/src/utils/work/getEmbeddingETag.js deleted file mode 100644 index b88605c33e..0000000000 --- a/src/utils/work/getEmbeddingETag.js +++ /dev/null @@ -1,39 +0,0 @@ -import { getBackendStatus } from 'redux/selectors'; -import generateETag from 'utils/work/generateETag'; - -const getEmbeddingETag = async (experimentId, getState, dispatch, inputEmbeddingMethod = null) => { - const { - clusteringSettings, - embeddingSettings: { methodSettings, method: reduxEmbeddingMethod, useSaved }, - } = getState().experimentSettings.processing.configureEmbedding; - - const { environment } = getState().networkResources; - const { - pipeline: - { startDate: qcPipelineStartDate }, - } = getBackendStatus(experimentId)(getState()).status; - - const embeddingMethod = inputEmbeddingMethod ?? reduxEmbeddingMethod; - - const embeddingBody = { - name: 'GetEmbedding', - type: embeddingMethod, - config: methodSettings[embeddingMethod], - useSaved, - }; - - const embeddingETag = await generateETag( - experimentId, - embeddingBody, - undefined, - qcPipelineStartDate, - environment, - clusteringSettings, - dispatch, - getState, - ); - - return embeddingETag; -}; - -export default getEmbeddingETag; diff --git a/src/utils/work/getExtraDependencies.js b/src/utils/work/getExtraDependencies.js deleted file mode 100644 index 98310182c2..0000000000 --- a/src/utils/work/getExtraDependencies.js +++ /dev/null @@ -1,89 +0,0 @@ -import loadCellSets from 'redux/actions/cellSets/loadCellSets'; -import { loadProcessingSettings } from 'redux/actions/experimentSettings'; -import { getCellSetsHierarchy, getCellSetsHierarchyByKeys } from 'redux/selectors'; -import workerVersions from 'utils/work/workerVersions'; - -const getClusteringSettings = async (experimentId, body, dispatch, getState) => { - let clusteringSettings = getState().experimentSettings - .processing.configureEmbedding?.clusteringSettings; - - if (!clusteringSettings) { - await dispatch(loadProcessingSettings(experimentId)); - - clusteringSettings = getState().experimentSettings - .processing.configureEmbedding?.clusteringSettings; - } - - return clusteringSettings; -}; - -// Check that the cell sets within the selected selectedCellSet didn't change -// e.g., if cell set was deleted we can't use cache -const getCellSetsThatAffectDownsampling = async (experimentId, body, dispatch, getState) => { - // If not downsampling, then there's no dependency set by this getter - if (!body.downsampleSettings) return ''; - - const { downsampleSettings: { groupedTracks, selectedCellSet } } = body; - - await dispatch(loadCellSets(experimentId)); - - const [{ children }] = getCellSetsHierarchyByKeys([selectedCellSet])(getState()); - - const selectedCellSetKeys = children.map((cellSet) => cellSet.key); - - const groupedCellSetKeys = getCellSetsHierarchy(groupedTracks)(getState()) - .map((cellClass) => cellClass.children) - .flat() - .map(({ key }) => key); - - // Keep them in separate lists, they each represent different changes in the settings - return [selectedCellSetKeys, groupedCellSetKeys]; -}; - -const getCellSets = async (experimentId, body, dispatch, getState) => { - await dispatch(loadCellSets(experimentId)); - - const hierarchy = getCellSetsHierarchy()(getState()); - - return hierarchy; -}; - -const dependencyGetters = { - ClusterCells: [], - ScTypeAnnotate: [], - GetExpressionCellSets: [], - GetEmbedding: [], - ListGenes: [], - DifferentialExpression: [getClusteringSettings], - BatchDifferentialExpression: [getClusteringSettings], - GeneExpression: [getClusteringSettings, getCellSetsThatAffectDownsampling], - GetBackgroundExpressedGenes: [getClusteringSettings], - DotPlot: [getClusteringSettings], - GetDoubletScore: [], - GetMitochondrialContent: [], - GetNGenes: [], - GetNUmis: [], - MarkerHeatmap: [getClusteringSettings, getCellSetsThatAffectDownsampling], - GetTrajectoryAnalysisStartingNodes: [getClusteringSettings], - GetTrajectoryAnalysisPseudoTime: [getClusteringSettings], - GetNormalizedExpression: [getClusteringSettings], - DownloadAnnotSeuratObject: [getClusteringSettings, getCellSets], -}; - -const getExtraDependencies = async (experimentId, body, dispatch, getState) => { - const { name } = body; - - const dependencies = await Promise.all( - dependencyGetters[name].map( - (dependencyGetter) => dependencyGetter(experimentId, body, dispatch, getState), - ), - ); - - if (workerVersions[name]) { - dependencies.push(workerVersions[name]); - } - - return dependencies; -}; - -export default getExtraDependencies; diff --git a/src/utils/work/seekWorkResponse.js b/src/utils/work/waitForWorkRequest.js similarity index 55% rename from src/utils/work/seekWorkResponse.js rename to src/utils/work/waitForWorkRequest.js index 3f6b69bbda..956b03cf65 100644 --- a/src/utils/work/seekWorkResponse.js +++ b/src/utils/work/waitForWorkRequest.js @@ -1,55 +1,14 @@ import dayjs from 'dayjs'; -import getAuthJWT from 'utils/getAuthJWT'; -import fetchAPI from 'utils/http/fetchAPI'; -import unpackResult, { decompressUint8Array } from 'utils/work/unpackResult'; +import { decompressUint8Array } from 'utils/work/unpackResult'; import parseResult from 'utils/work/parseResult'; import WorkTimeoutError from 'utils/errors/http/WorkTimeoutError'; import WorkResponseError from 'utils/errors/http/WorkResponseError'; -import httpStatusCodes from 'utils/http/httpStatusCodes'; import { updateBackendStatus } from 'redux/actions/backendStatus'; const timeoutIds = {}; -// getRemainingWorkerStartTime returns how many more seconds the worker is expected to -// need to be running with an extra 1 minute for a bit of leeway -const getRemainingWorkerStartTime = (creationTimestamp) => { - const now = new Date(); - const creationTime = new Date(creationTimestamp); - const elapsed = parseInt((now - creationTime) / (1000), 10); // gives second difference - - // we assume a worker takes up to 5 minutes to start - const totalStartup = 5 * 60; - const remainingTime = totalStartup - elapsed; - // add an extra minute just in case - return remainingTime + 60; -}; - -const seekFromS3 = async (taskName, signedUrl) => { - const response = await fetch(signedUrl); - - // some WorkRequests like scType and runClustering do not upload data to S3 - // (nor return it via socket) instead they patch the cellsets through the API. - // In those cases the workResults will not exist, and it's fine because data will - // be updated through the cellsets. - // the forbidden (in addition to the not found) is required because of how signed URLs work - // when you try to download a file from a signedUrl that doesn't exist you get a 403 forbidden - // because the user is not authorized to access a file that does not exist - if (response.status === httpStatusCodes.NOT_FOUND - || response.status === httpStatusCodes.FORBIDDEN) { - return null; - } - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.text}`, { cause: response }); - } - - const unpackedResult = await unpackResult(response, taskName); - const parsedResult = parseResult(unpackedResult, taskName); - - return parsedResult; -}; - // getTimeoutDate returns the date resulting of adding 'timeout' seconds to // current time. const getTimeoutDate = (timeout) => dayjs().add(timeout, 's').toISOString(); @@ -85,18 +44,16 @@ const getWorkerTimeout = (taskName, defaultTimeout) => { } }; -const dispatchWorkRequest = async ( +const waitForWorkRequest = async ( + ETag, experimentId, - body, + request, timeout, - ETag, - requestProps, dispatch, ) => { const { default: connectionPromise } = await import('utils/socketConnection'); const io = await connectionPromise; - const { name: taskName } = body; // for listGenes, markerHeatmap, & getEmbedding we set a long timeout for the worker // after that timeout the worker will skip those requests @@ -104,35 +61,11 @@ const dispatchWorkRequest = async ( // as long as it receives "heartbeats" from the worker because that means the worker is alive // and progresing. // this should be removed if we make each request run in a different worker - const workerTimeoutDate = getWorkerTimeout(taskName, timeout); - - const authJWT = await getAuthJWT(); - - const request = { - ETag, - socketId: io.id, - experimentId, - ...(authJWT && { Authorization: `Bearer ${authJWT}` }), - timeout: workerTimeoutDate, - body, - ...requestProps, - }; + const workerTimeoutDate = getWorkerTimeout(request, timeout); const timeoutPromise = new Promise((resolve, reject) => { setOrRefreshTimeout(request, timeout, reject, ETag); - io.on(`WorkerInfo-${experimentId}`, (res) => { - const { response: { podInfo: { creationTimestamp, phase } } } = res; - const extraTime = getRemainingWorkerStartTime(creationTimestamp); - - // this worker info indicates that the work request has been received but the worker - // is still spinning up so we will add extra time to account for that. - if (phase === 'Pending' && extraTime > 0) { - const newTimeout = timeout + extraTime; - setOrRefreshTimeout(request, newTimeout, reject, ETag); - } - }); - // this experiment update is received whenever a worker finishes any work request // related to the current experiment. We extend the timeout because we know // the worker is alive and was working on another request of our experiment @@ -161,7 +94,6 @@ const dispatchWorkRequest = async ( if (response.error) { const { errorCode, userMessage } = response; - console.error(errorCode, userMessage); return reject( new WorkResponseError(errorCode, userMessage, request), @@ -170,7 +102,6 @@ const dispatchWorkRequest = async ( return resolve({ signedUrl: response.signedUrl }); } - // If type isn't object, then we have the actual work result, // no further downloads are necessary, we just need to decompress and return it const decompressedData = await decompressUint8Array(Uint8Array.from(Buffer.from(res, 'base64'))); @@ -179,18 +110,8 @@ const dispatchWorkRequest = async ( }); }); - // TODO test what happens when api throws an error here - await fetchAPI( - `/v2/workRequest/${experimentId}/${ETag}`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request), - }, - ); - // TODO switch to using normal WorkRequest for v2 requests return await Promise.race([timeoutPromise, responsePromise]); }; -export { dispatchWorkRequest, seekFromS3 }; +export default waitForWorkRequest;