Skip to content

Commit 82aee26

Browse files
committed
refactor: clean and harden LCMS flow with general codebase cleanup
1 parent 3e17743 commit 82aee26

46 files changed

Lines changed: 1273 additions & 691 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import configureStore from 'redux-mock-store';
5+
import { Provider } from 'react-redux';
6+
import { createTheme } from '@mui/material';
7+
import { ThemeProvider } from '@mui/styles';
8+
9+
import HPLCViewer from '../../../components/hplc_viewer';
10+
import { LIST_LAYOUT } from '../../../constants/list_layout';
11+
12+
jest.mock('../../../components/cmd_bar/index', () => (props) => (
13+
<div data-testid="cmd-bar">
14+
{props.editorOnly ? 'editorOnly' : 'editable'}-{props.hideThreshold ? 'hideThreshold' : 'showThreshold'}
15+
</div>
16+
));
17+
18+
jest.mock('../../../components/d3_line_rect/index', () => (props) => (
19+
<div data-testid="viewer-line-rect">
20+
tic:{props.ticEntities?.length || 0}-uvvis:{props.uvvisEntities?.length || 0}-mz:{props.mzEntities?.length || 0}
21+
</div>
22+
));
23+
24+
jest.mock('../../../components/panel/index', () => (props) => (
25+
<div data-testid="panel-viewer">
26+
{props.integration ? 'hasIntegration' : 'noIntegration'}
27+
</div>
28+
));
29+
30+
const mockStore = configureStore([]);
31+
const theme = createTheme();
32+
33+
const renderWithStore = (state, extraProps = {}) => {
34+
const store = mockStore(state);
35+
return render(
36+
<Provider store={store}>
37+
<ThemeProvider theme={theme}>
38+
<HPLCViewer
39+
operations={[]}
40+
entityFileNames={[]}
41+
userManualLink={{}}
42+
molSvg=""
43+
theoryMass=""
44+
descriptions={[]}
45+
canChangeDescription={false}
46+
onDescriptionChanged={() => {}}
47+
editorOnly={true}
48+
{...extraProps}
49+
/>
50+
</ThemeProvider>
51+
</Provider>,
52+
);
53+
};
54+
55+
describe('<HPLCViewer />', () => {
56+
it('renders empty view when there are no entities', () => {
57+
renderWithStore({
58+
curve: { listCurves: [], curveIdx: 0 },
59+
layout: LIST_LAYOUT.LC_MS,
60+
integration: { present: { integrations: [] } },
61+
hplcMs: {},
62+
});
63+
64+
expect(screen.queryByTestId('cmd-bar')).not.toBeInTheDocument();
65+
expect(screen.queryByTestId('viewer-line-rect')).not.toBeInTheDocument();
66+
expect(screen.queryByTestId('panel-viewer')).not.toBeInTheDocument();
67+
});
68+
69+
it('renders LCMS blocks with split entities and selected integration', () => {
70+
const entities = [
71+
{
72+
csCategory: ['tic', 'positive'],
73+
feature: { xUnit: 'min', yUnit: 'intensity' },
74+
topic: { x: [1, 2], y: [10, 20] },
75+
},
76+
{
77+
csCategory: ['uvvis'],
78+
feature: { xUnit: 'nm', yUnit: 'AU' },
79+
topic: { x: [210], y: [0.4] },
80+
},
81+
{
82+
csCategory: ['mz', 'negative'],
83+
feature: { xUnit: 'm/z', yUnit: 'counts' },
84+
topic: { x: [100], y: [5] },
85+
},
86+
];
87+
88+
renderWithStore({
89+
curve: { listCurves: entities, curveIdx: 1 },
90+
layout: LIST_LAYOUT.LC_MS,
91+
integration: { present: { integrations: [null, { id: 'int-2' }, null] } },
92+
hplcMs: { tic: { polarity: 'positive' } },
93+
});
94+
95+
expect(screen.getByTestId('cmd-bar')).toHaveTextContent('editorOnly-hideThreshold');
96+
expect(screen.getByTestId('viewer-line-rect')).toHaveTextContent('tic:1-uvvis:1-mz:1');
97+
expect(screen.getByTestId('panel-viewer')).toHaveTextContent('hasIntegration');
98+
});
99+
});

src/__tests__/units/components/panel/info.test.js

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { render, screen } from '@testing-library/react';
2+
import { render } from '@testing-library/react';
33
import InfoPanel from '../../../../components/panel/info';
44
import configureStore from 'redux-mock-store'
55
import { Provider } from 'react-redux';
@@ -19,7 +19,7 @@ const store = mockStore({
1919
shifts: [],
2020
},
2121
simulation: {
22-
22+
nmrSimPeaks: [],
2323
},
2424
detector: {
2525
curves: [
@@ -29,10 +29,12 @@ const store = mockStore({
2929
},
3030
],
3131
},
32-
meta: {}
33-
});
34-
const failedStore = mockStore({
35-
32+
meta: {
33+
dscMetaData: {},
34+
},
35+
hplcMs: {
36+
uvvis: { spectraList: [], listWaveLength: [] },
37+
},
3638
});
3739
const dispatchMock = () => Promise.resolve({});
3840
store.dispatch = jest.fn(dispatchMock);
@@ -43,8 +45,14 @@ const theme = createTheme({
4345
},
4446
});
4547

46-
const feature = {
47-
48+
const feature = {};
49+
const baseProps = {
50+
feature,
51+
integration: {},
52+
editorOnly: false,
53+
molSvg: '',
54+
descriptions: '',
55+
canChangeDescription: false,
4856
};
4957

5058
describe("<InfoPanel />", () => {
@@ -55,23 +63,11 @@ describe("<InfoPanel />", () => {
5563
}
5664
});
5765

58-
test('Cannot render info panel', () => {
59-
const renderer =
60-
<AppWrapper store={store}>
61-
<ThemeProvider theme={theme}>
62-
<InfoPanel expand={false} onExapnd={() => {}} />
63-
</ThemeProvider>
64-
</AppWrapper>
65-
;
66-
const {queryByTestId} = render(renderer);
67-
expect(queryByTestId('PanelInfo')).not.toBeInTheDocument();
68-
});
69-
70-
test('Render info panel', () => {
66+
test('Render info panel', () => {
7167
const renderer =
7268
<AppWrapper store={store}>
7369
<ThemeProvider theme={theme}>
74-
<InfoPanel expand={false} onExapnd={() => {}} feature={feature} />
70+
<InfoPanel expand={false} onExapnd={() => {}} {...baseProps} />
7571
</ThemeProvider>
7672
</AppWrapper>
7773
;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {
2+
classify,
3+
getLcMsInfo,
4+
isLcMsGroup,
5+
splitAndReindexEntities,
6+
} from '../../../helpers/extractEntityLCMS';
7+
8+
describe('extractEntityLCMS helpers', () => {
9+
describe('getLcMsInfo', () => {
10+
it('prefers explicit lcms fields when present', () => {
11+
const entity = {
12+
lcmsKind: 'mz',
13+
lcmsPolarity: 'negative',
14+
csCategory: ['tic positive'],
15+
};
16+
17+
expect(getLcMsInfo(entity)).toEqual({
18+
kind: 'mz',
19+
polarity: 'negative',
20+
});
21+
});
22+
23+
it('detects tic positive from csCategory', () => {
24+
const entity = {
25+
csCategory: ['mass tic', 'positive mode'],
26+
};
27+
28+
expect(getLcMsInfo(entity)).toEqual({
29+
kind: 'tic',
30+
polarity: 'positive',
31+
});
32+
});
33+
34+
it('detects mz from dataType when categories are missing', () => {
35+
const entity = {
36+
dataType: 'Mass Spectrum',
37+
};
38+
39+
expect(getLcMsInfo(entity)).toEqual({
40+
kind: 'mz',
41+
polarity: 'neutral',
42+
});
43+
});
44+
45+
it('detects tic from units fallback', () => {
46+
const entity = {
47+
spectra: [
48+
{
49+
xUnit: 'Time',
50+
yUnit: 'Intensity',
51+
},
52+
],
53+
};
54+
55+
expect(getLcMsInfo(entity)).toEqual({
56+
kind: 'tic',
57+
polarity: 'neutral',
58+
});
59+
});
60+
61+
it('returns unknown when no signal can classify entity', () => {
62+
expect(getLcMsInfo({})).toEqual({
63+
kind: 'unknown',
64+
polarity: 'neutral',
65+
});
66+
});
67+
});
68+
69+
describe('classify', () => {
70+
it('returns unknown label for unknown entities', () => {
71+
expect(classify({})).toEqual('unknown');
72+
});
73+
74+
it('returns composite label for known entities', () => {
75+
expect(classify({ csCategory: ['uvvis', 'positive'] })).toEqual('uvvis_positive');
76+
});
77+
});
78+
79+
describe('splitAndReindexEntities', () => {
80+
it('splits by kind, sorts by polarity and sets curveIdx', () => {
81+
const ticNegative: any = { csCategory: ['tic', 'negative'] };
82+
const ticPositive: any = { csCategory: ['tic', 'positive'] };
83+
const mzNeutral = { csCategory: ['mz'] };
84+
const uvvis = { csCategory: ['uvvis'] };
85+
const unknown = {};
86+
87+
const result = splitAndReindexEntities([
88+
ticNegative,
89+
ticPositive,
90+
mzNeutral,
91+
uvvis,
92+
unknown,
93+
]);
94+
95+
expect(result.ticEntities).toHaveLength(2);
96+
expect(result.ticEntities[0]).not.toBe(ticPositive);
97+
expect(result.ticEntities[1]).not.toBe(ticNegative);
98+
expect(result.ticEntities[0]).toMatchObject({ csCategory: ['tic', 'positive'] });
99+
expect(result.ticEntities[1]).toMatchObject({ csCategory: ['tic', 'negative'] });
100+
expect(result.ticEntities[0].curveIdx).toEqual(0);
101+
expect(result.ticEntities[1].curveIdx).toEqual(1);
102+
103+
expect(result.mzEntities).toHaveLength(1);
104+
expect(result.mzEntities[0]).not.toBe(mzNeutral);
105+
expect(result.mzEntities[0]).toMatchObject({ csCategory: ['mz'] });
106+
expect(result.mzEntities[0].curveIdx).toEqual(0);
107+
108+
expect(result.uvvisEntities).toEqual([
109+
expect.objectContaining({ csCategory: ['uvvis'] }),
110+
]);
111+
expect(result.unknownEntities).toEqual([
112+
expect.objectContaining({}),
113+
]);
114+
expect(result.dataEntities[0]).toMatchObject({ csCategory: ['mz'] });
115+
expect(result.allEntities).toHaveLength(5);
116+
expect(result.ticEntities[0].lcmsKind).toEqual('tic');
117+
expect(result.ticEntities[0].lcmsPolarity).toEqual('positive');
118+
expect(ticPositive.lcmsKind).toBeUndefined();
119+
expect(ticPositive.lcmsPolarity).toBeUndefined();
120+
});
121+
});
122+
123+
describe('isLcMsGroup', () => {
124+
it('returns false on empty input', () => {
125+
expect(isLcMsGroup([])).toEqual(false);
126+
expect(isLcMsGroup(null as any)).toEqual(false);
127+
});
128+
129+
it('returns true when uvvis + tic is present', () => {
130+
const entities = [
131+
{ csCategory: ['uvvis'] },
132+
{ csCategory: ['tic', 'positive'] },
133+
];
134+
135+
expect(isLcMsGroup(entities)).toEqual(true);
136+
});
137+
138+
it('returns true when uvvis + mz is present', () => {
139+
const entities = [
140+
{ csCategory: ['uvvis'] },
141+
{ csCategory: ['mz', 'negative'] },
142+
];
143+
144+
expect(isLcMsGroup(entities)).toEqual(true);
145+
});
146+
147+
it('returns false when uvvis exists alone', () => {
148+
expect(isLcMsGroup([{ csCategory: ['uvvis'] }])).toEqual(false);
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)