Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi view for continuous embedding plot #61

Merged
merged 23 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,13 @@ const initialState = {
};
const store = mockStore(initialState);
let component;

const method = 'UMAP';
const {
plotData,
cellSetLegendsData,
} = generateData(cellSets, config.selectedSample, config.selectedCellSet, embeddingData);

const spec = generateSpec(config, plotData, cellSetLegendsData);
const spec = generateSpec(config, method, plotData, cellSetLegendsData);

const testPlot = () => mount(
<Provider store={store}>
Expand Down
192 changes: 83 additions & 109 deletions src/__test__/components/plots/MultiViewGrid.test.jsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,107 @@
/* eslint-disable no-shadow */
import React from 'react';
import {
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from '@testing-library/react';
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
import _ from 'lodash';
import { Provider } from 'react-redux';

import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import MultiViewGrid from 'components/plots/MultiViewGrid';
import { generateMultiViewGridPlotUuid } from 'utils/generateCustomPlotUuid';

import { makeStore } from 'redux/store';
import { updatePlotConfig } from 'redux/actions/componentConfig';
import loadConditionalComponentConfig from 'redux/actions/componentConfig/loadConditionalComponentConfig';
import { arrayMoveImmutable } from 'utils/arrayUtils';

import fake from '__test__/test-utils/constants';
import mockAPI, {
generateDefaultMockAPIResponses,
} from '__test__/test-utils/mockAPI';

const experimentId = fake.EXPERIMENT_ID;
const plotUuid = 'ViolinMain';
const plotType = 'violin';
const multiViewType = 'multiView';
const multiViewUuid = 'multiView-ViolinMain';

const mockRenderPlot = jest.fn((plotUuid) => (<>{plotUuid}</>));
const mockUpdateAllWithChanges = jest.fn(() => {});

const plotUuids = [0, 1, 2].map((index) => generateMultiViewGridPlotUuid(plotUuid, index));

const defaultResponses = _.merge(
generateDefaultMockAPIResponses(experimentId),
);

const renderMultiView = (store) => {
render(
<Provider store={store}>
<MultiViewGrid
renderPlot={mockRenderPlot}
multiViewUuid={multiViewUuid}
updateAllWithChanges={mockUpdateAllWithChanges}
/>
</Provider>,
);
};
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

enableFetchMocks();
// Mock the named exports of the action creators
jest.mock('redux/actions/componentConfig/loadConditionalComponentConfig', () => ({
__esModule: true,
default: jest.fn().mockReturnValue({ type: 'MOCK_LOAD_CONDITIONAL_COMPONENT_CONFIG' }),
}));

let store = null;
jest.mock('redux/actions/componentConfig/savePlotConfig', () => ({
savePlotConfig: jest.fn().mockReturnValue({ type: 'MOCK_SAVE_PLOT_CONFIG' }),
}));

const loadComponent = async (componentUuid, type, skipAPI, customConfig) => {
store.dispatch(
loadConditionalComponentConfig(experimentId, componentUuid, type, skipAPI, customConfig),
);
};
const mockStore = configureMockStore([thunk]);

describe('MultiViewGrid', () => {
beforeEach(async () => {
jest.clearAllMocks();

fetchMock.resetMocks();
fetchMock.mockIf(/.*/, mockAPI(defaultResponses));

store = makeStore();
let store;
const multiViewConfig = {
plotUuids: ['ViolinMain-0', 'ViolinMain-1', 'ViolinMain-2'],
nrows: 1,
ncols: 3,
};
let plotConfigs;
const experimentId = 'experiment1';
const plotUuid = 'ViolinMain';
const plotType = 'violin';
const multiViewUuid = 'multiView-violin';

beforeEach(() => {
store = mockStore({
componentConfig: {
[multiViewUuid]: { config: multiViewConfig },
},
genes: {
properties: {
views: {
[plotUuid]: { data: ['gene1'] },
},
},
expression: {
full: {
matrix: {
geneIsLoaded: () => true,
},
},
},
},
embeddings: {},
cellSets: { accessible: true },
});

const customMultiViewConfig = { nrows: 2, ncols: 2, plotUuids };
await loadComponent(multiViewUuid, multiViewType, true, customMultiViewConfig);
plotConfigs = {
'ViolinMain-0': { shownGene: 'gene1' },
'ViolinMain-1': { shownGene: 'gene2' },
'ViolinMain-2': { shownGene: 'gene3' },
};
// act(() => {

plotUuids.map(async (uuid) => {
await loadComponent(uuid, plotType, true);
});
// });
});

const renderComponent = () => {
render(
<Provider store={store}>
<MultiViewGrid
experimentId={experimentId}
renderPlot={(uuid) => <div>{uuid}</div>}
updateAllWithChanges={() => {}}
plotType={plotType}
plotUuid={plotUuid}
/>
</Provider>,
);
};
it('Renders itself and its children', async () => {
renderMultiView(store);

plotUuids.forEach((plotUuid) => {
expect(screen.getByText(plotUuid)).toBeInTheDocument();
await renderComponent();
await waitFor(() => {
multiViewConfig.plotUuids.forEach((uuid) => {
expect(screen.getByText(uuid)).toBeInTheDocument();
});
});
});

it('Adds plots to multi view', async () => {
renderMultiView(store);

await store.dispatch(updatePlotConfig(multiViewUuid, {
nrows: 2,
ncols: 2,
plotUuids: [...plotUuids, 'ViolinMain-3'],
}));

await loadComponent('ViolinMain-3', plotType, true);

await waitFor(() => expect(screen.getByText('ViolinMain-3')).toBeInTheDocument());

expect(mockRenderPlot).toHaveBeenCalledTimes(4);
});

it('Re-orders plots in multi view', async () => {
renderMultiView(store);

const multiViewContainer = document.getElementById('multiViewContainer');

expect(multiViewContainer.textContent).toBe(plotUuids.join(''));
await renderComponent();

const reorderedUuids = arrayMoveImmutable(plotUuids, 0, 2);
const reorderedUuids = ['ViolinMain-0', 'ViolinMain-1', 'ViolinMain-2'];
multiViewConfig.plotUuids = reorderedUuids;

await store.dispatch(updatePlotConfig(multiViewUuid, { plotUuids: reorderedUuids }));
await waitFor(() => expect(multiViewContainer.textContent).toBe(reorderedUuids.join('')));

expect(mockRenderPlot).toHaveBeenCalledTimes(3);
const multiViewContainer = document.getElementById('multiViewContainer');
expect(multiViewContainer.textContent).toBe(reorderedUuids.join(''));
});

it('Removes plots from multi view', async () => {
renderMultiView(store);

const multiViewContainer = document.getElementById('multiViewContainer');

expect(multiViewContainer.textContent).toBe(plotUuids.join(''));
await renderComponent();

const newUuids = plotUuids.slice(1);
const newUuids = multiViewConfig.plotUuids.slice(1);
multiViewConfig.plotUuids = newUuids;

await store.dispatch(updatePlotConfig(multiViewUuid, { plotUuids: newUuids }));
await waitFor(() => expect(multiViewContainer.textContent).toBe(newUuids.join('')));

expect(mockRenderPlot).toHaveBeenCalledTimes(3);
const multiViewContainer = document.getElementById('multiViewContainer');
expect(multiViewContainer.textContent).toBe('ViolinMain-0ViolinMain-1ViolinMain-2');
});
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ describe('Continuous embedding plot', () => {
it('Loads controls and elements', async () => {
await renderContinuousEmbeddingPage(storeState);

expect(screen.getByText(/Gene selection/i)).toBeInTheDocument();
expect(screen.getByText(/Select data/i)).toBeInTheDocument();
expect(screen.getByText(/Expression values/i)).toBeInTheDocument();
expect(screen.getByText(/Main schema/i)).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const mockWorkerResponses = {
};

const experimentId = fake.EXPERIMENT_ID;
const plotUuid = 'ViolinMain';
const multiViewUuid = 'multiView-ViolinMain';
const plotUuid = 'ViolinMain-0';
const multiViewUuid = 'multiView-violin';

const customAPIResponses = {
[`/plots/${plotUuid}$`]: (req) => {
Expand Down Expand Up @@ -90,8 +90,8 @@ describe('ViolinIndex', () => {

it('Loads controls and elements', async () => {
await renderViolinPage(storeState);

expect(screen.getByText(/Gene selection/i)).toBeInTheDocument();

expect(screen.getByText(/View multiple plots/i)).toBeInTheDocument();
expect(screen.getByText(/Select data/i)).toBeInTheDocument();
expect(screen.getByText(/Data transformation/i)).toBeInTheDocument();
Expand All @@ -107,33 +107,6 @@ describe('ViolinIndex', () => {
expect(screen.getByRole('graphics-document', { name: 'Violin plot' })).toBeInTheDocument();
});

it('Changes the shown gene', async () => {
await renderViolinPage(storeState);

userEvent.click(screen.getByText(/Gene selection/i));

const searchBox = screen.getByRole('combobox', { name: 'SearchBar' });

userEvent.clear(searchBox);
userEvent.type(searchBox, 'cc');

const option = screen.getByTitle('Ccl5');

await act(async () => {
// the element has pointer-events set to 'none', skip check
// based on https://stackoverflow.com/questions/61080116
userEvent.click(option, undefined, { skipPointerEventsCheck: true });
});

userEvent.click(screen.getByText('Search'));

expect(searchBox.textContent).toBe('');

await waitFor(() => expect(screen.getByRole('graphics-document', { name: 'Violin plot' })).toBeInTheDocument());

expect(storeState.getState().componentConfig[plotUuid].config.shownGene).toBe('Ccl5');
});

it('Changes to raw expression', async () => {
await renderViolinPage(storeState);

Expand Down Expand Up @@ -171,7 +144,7 @@ describe('ViolinIndex', () => {
expect(_.isEqual([multiViewConfig.nrows, multiViewConfig.ncols], [2, 2])).toBe(true);

// New plot's config is correct
expect(storeState.getState().componentConfig['ViolinMain-0']).toMatchSnapshot('new-config');
expect(storeState.getState().componentConfig['ViolinMain-1']).toMatchSnapshot('new-config');
});

it('Adds new plot using same config as previous plot', async () => {
Expand Down Expand Up @@ -202,4 +175,30 @@ describe('ViolinIndex', () => {
// New plot's config contains normalised set to raw too
expect(storeState.getState().componentConfig['ViolinMain-0'].config.normalised).toEqual('raw');
});
it('Changes the shown gene', async () => {
await renderViolinPage(storeState);

userEvent.click(screen.getByText(/Gene selection/i));

const searchBox = screen.getAllByRole('combobox', { name: 'SearchBar' })[0];

userEvent.clear(searchBox);
userEvent.type(searchBox, 'cc');

const option = screen.getByTitle('Ccl5');

await act(async () => {
// the element has pointer-events set to 'none', skip check
// based on https://stackoverflow.com/questions/61080116
userEvent.click(option, undefined, { skipPointerEventsCheck: true });
});

userEvent.click(screen.getByText('Search'));

expect(searchBox.textContent).toBe('');

await waitFor(() => expect(screen.getByRole('graphics-document', { name: 'Violin plot' })).toBeInTheDocument());

expect(storeState.getState().componentConfig[plotUuid].config.shownGene).toBe('Ccl5');
});
});
Loading
Loading