Skip to content

Commit f948e9f

Browse files
authored
Update ebic template (#533)
* WIP * WIP * rename settings fields + start using it when calling endpoints * update backend * WIP with tests * copilot suggestion * update tests * improve tests * update backend * fix tests
1 parent 27df020 commit f948e9f

File tree

16 files changed

+717
-85
lines changed

16 files changed

+717
-85
lines changed

cypress/e2e/common_functions.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function selectOptionShould(label, option, shouldWhat = 'exist', parentSe
4545
cy.get('body').click(0, 0);
4646
}
4747

48-
export function setInputValue(label, value, parentSelector = '') {
48+
export function setInputValue(label, value, parentSelector = 'body') {
4949
cy.get(parentSelector).contains(label).siblings('div').first()
5050
.children('input')
5151
.clear('');
@@ -54,26 +54,26 @@ export function setInputValue(label, value, parentSelector = '') {
5454
.type(value);
5555
}
5656

57-
export function setAutocompleteValue(label, value, parentSelector = '') {
57+
export function setAutocompleteValue(label, value, parentSelector = 'body') {
5858
setInputValue(label, value, parentSelector);
5959
// We use the regex so that its the exact match
6060
cy.get('div[role="presentation"]').contains(new RegExp(`^${value}$`)).click();
6161
}
6262

63-
export function clearAutocompleteValue(label, parentSelector = '') {
63+
export function clearAutocompleteValue(label, parentSelector = 'body') {
6464
cy.get(parentSelector).contains(label).siblings('div').click();
6565
cy.get(parentSelector).contains(label).siblings('div').find('button.MuiAutocomplete-clearIndicator')
6666
.first()
6767
.click();
6868
}
6969

70-
export function clearInputValue(label, parentSelector = '') {
70+
export function clearInputValue(label, parentSelector = 'body') {
7171
cy.get(parentSelector).contains(label).siblings('div').first()
7272
.children('input')
7373
.clear('');
7474
}
7575

76-
export function checkInputValue(label, value, parentSelector = '') {
76+
export function checkInputValue(label, value, parentSelector = 'body') {
7777
cy.get(parentSelector).contains(label).siblings('div').first()
7878
.children('input')
7979
.should('have.value', value);
@@ -134,7 +134,7 @@ export function manuallyTypeSequence(seq, circular = false, overhangs = []) {
134134
});
135135
}
136136

137-
export function waitForEnzymes(parentSelector = '') {
137+
export function waitForEnzymes(parentSelector = 'body') {
138138
cy.get(`${parentSelector} .enzyme-multi-select`, { timeout: 20000 }).should('exist');
139139
}
140140

cypress/e2e/group-1/primer_design.cy.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ function updateSpacer(index, value) {
2020
cy.wait(500);
2121
}
2222

23+
const defaultPrimerDesignSettings = {
24+
primer_dna_conc: 50,
25+
primer_salt_monovalent: 50,
26+
primer_salt_divalent: 1.5
27+
};
28+
2329
describe('Test primer designer functionality', () => {
2430
beforeEach(() => {
2531
cy.visit('/');
@@ -133,6 +139,7 @@ describe('Test primer designer functionality', () => {
133139
expect(interception.request.query.homology_length).to.equal('20');
134140
expect(interception.request.query.target_tm).to.equal('30');
135141
expect(interception.request.query.minimal_hybridization_length).to.equal('10');
142+
expect(interception.request.body.settings).to.deep.equal(defaultPrimerDesignSettings);
136143
});
137144

138145
// Back to default values
@@ -264,6 +271,7 @@ describe('Test primer designer functionality', () => {
264271
expect(interception.request.query.homology_length).to.equal('20');
265272
expect(interception.request.query.minimal_hybridization_length).to.equal('30');
266273
expect(interception.request.query.target_tm).to.equal('30');
274+
expect(interception.request.body.settings).to.deep.equal(defaultPrimerDesignSettings);
267275
});
268276

269277
// Back to sensible values
@@ -410,6 +418,7 @@ describe('Test primer designer functionality', () => {
410418
expect(interception.request.query.target_tm).to.equal('40');
411419
expect(interception.request.query.left_enzyme_inverted).to.equal('false');
412420
expect(interception.request.query.right_enzyme_inverted).to.equal('false');
421+
expect(interception.request.body.settings).to.deep.equal(defaultPrimerDesignSettings);
413422
});
414423

415424
// We should be on the Results tab
@@ -498,6 +507,7 @@ describe('Test primer designer functionality', () => {
498507
cy.wait('@primerDesign').then((interception) => {
499508
expect(interception.request.query.left_enzyme_inverted).to.equal('true');
500509
expect(interception.request.query.right_enzyme_inverted).to.equal('true');
510+
expect(interception.request.body.settings).to.deep.equal(defaultPrimerDesignSettings);
501511
});
502512

503513
// We should be on the Results tab
@@ -545,6 +555,7 @@ describe('Test primer designer functionality', () => {
545555
expect(interception.request.query.minimal_hybridization_length).to.equal('10');
546556
expect(interception.request.query.target_tm).to.equal('40');
547557
expect(interception.request.body.pcr_template.forward_orientation).to.equal(false);
558+
expect(interception.request.body.settings).to.deep.equal(defaultPrimerDesignSettings);
548559
});
549560

550561
// We should be on the Results tab

cypress/e2e/group-4/tab_navigation.cy.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { checkInputValue } from "../common_functions";
2+
13
describe('Test tab navigation functionality', () => {
24
beforeEach(() => {
35
cy.visit('/');
@@ -6,27 +8,47 @@ describe('Test tab navigation functionality', () => {
68
// Starts on cloning tab
79
cy.get('div.tf-ancestor-tree').should('be.visible');
810
cy.get('.primer-table-container').should('not.be.visible');
11+
cy.get('.settings-tab').should('not.be.visible');
912
// Move to primers tab
1013
cy.get('button.MuiTab-root').contains('Primers').click();
1114
cy.get('.primer-table-container').should('be.visible');
1215
cy.get('div.tf-ancestor-tree').should('not.be.visible');
16+
cy.get('.settings-tab').should('not.be.visible');
1317
// Move to description tab
1418
cy.get('button.MuiTab-root').contains('Description').click();
1519
cy.get('.primer-table-container').should('not.be.visible');
1620
cy.get('div.tf-ancestor-tree').should('not.be.visible');
1721
cy.get('.description-container').should('be.visible');
22+
cy.get('.settings-tab').should('not.be.visible');
1823
// Move to the sequence tab
1924
cy.get('button.MuiTab-root').contains('Sequence').click();
2025
cy.get('.primer-table-container').should('not.be.visible');
2126
cy.get('div.tf-ancestor-tree').should('not.be.visible');
2227
cy.get('.description-container').should('not.be.visible');
2328
cy.get('.main-sequence-editor').should('be.visible');
29+
cy.get('.settings-tab').should('not.be.visible');
2430
// Move to the data model tab
2531
cy.get('button.MuiTab-root').contains('Data model').click();
2632
cy.get('.primer-table-container').should('not.be.visible');
2733
cy.get('div.tf-ancestor-tree').should('not.be.visible');
2834
cy.get('.description-container').should('not.be.visible');
2935
cy.get('.main-sequence-editor').should('not.be.visible');
3036
cy.get('code').contains('input').should('be.visible');
37+
cy.get('.settings-tab').should('not.be.visible');
38+
// Move to the settings tab
39+
cy.get('button.MuiTab-root').contains('Settings').click();
40+
cy.get('.primer-table-container').should('not.be.visible');
41+
cy.get('div.tf-ancestor-tree').should('not.be.visible');
42+
cy.get('.description-container').should('not.be.visible');
43+
cy.get('.main-sequence-editor').should('not.be.visible');
44+
cy.get('code').contains('input').should('not.be.visible');
45+
cy.get('.settings-tab').should('be.visible');
46+
// Check that the values are displayed
47+
cy.get('.settings-tab').contains('Primer DNA concentration').should('be.visible');
48+
cy.get('.settings-tab').contains('Monovalent ions').should('be.visible');
49+
cy.get('.settings-tab').contains('Divalent ions').should('be.visible');
50+
checkInputValue('Primer DNA concentration', '50');
51+
checkInputValue('Monovalent ions', '50');
52+
checkInputValue('Divalent ions', '1.5');
3153
});
3254
});

src/components/OpenCloning.jsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Tabs from '@mui/material/Tabs';
44
import { isEqual } from 'lodash-es';
55
import DescriptionEditor from './DescriptionEditor';
66
import PrimerList from './primers/PrimerList';
7+
import SettingsTab from './settings/SettingsTab';
78
import { cloningActions } from '../store/cloning';
89
import TabPanel from './navigation/TabPanel';
910
import CustomTab from './navigation/CustomTab';
@@ -59,7 +60,8 @@ function OpenCloning() {
5960
<CustomTab label="Description" index={2} />
6061
<CustomTab label="Sequence" index={3} />
6162
<CustomTab label="Data model" index={4} />
62-
{enableAssembler && <CustomTab label="Assembler" index={5} />}
63+
<CustomTab label="Settings" index={5} />
64+
{enableAssembler && <CustomTab label="Assembler" index={6} />}
6365
</Tabs>
6466
<div className="tab-panels-container" ref={tabPanelsRef}>
6567
<TabPanel index={1} value={currentTab} className="primer-tab-pannel">
@@ -86,7 +88,10 @@ function OpenCloning() {
8688
<TabPanel index={4} value={currentTab} className="data-model-tab-pannel">
8789
<DataModelDisplayer />
8890
</TabPanel>
89-
{enableAssembler && <TabPanel index={5} value={currentTab} className="assembler-tab-pannel">
91+
<TabPanel index={5} value={currentTab} className="settings-tab-pannel">
92+
<SettingsTab />
93+
</TabPanel>
94+
{enableAssembler && <TabPanel index={6} value={currentTab} className="assembler-tab-pannel">
9095
<Assembler />
9196
</TabPanel>}
9297
</div>
Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,95 @@
11
import React from 'react';
2-
import PrimerTableRow from './PrimerTableRow';
3-
import { mockPrimerDetails, mockPCRDetails, mockPrimer } from '../../../tests/mockPrimerDetailsData';
2+
import PrimerList from './PrimerList';
3+
import store from '../../store';
4+
import { cloningActions } from '../../store/cloning';
5+
import { Provider } from 'react-redux';
6+
7+
const { setConfig, setPrimers, setGlobalPrimerSettings } = cloningActions;
8+
9+
const mockReply = {
10+
statusCode: 200, body: {
11+
melting_temperature: 60, gc_content: .5, homodimer: {
12+
melting_temperature: 0,
13+
deltaG: 0,
14+
figure: "dummy_figure"
15+
},
16+
hairpin: {
17+
melting_temperature: 0,
18+
deltaG: 0,
19+
figure: "dummy_figure"
20+
},
21+
}
22+
}
23+
24+
describe('PrimerList', () => {
25+
beforeEach(() => {
26+
store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
27+
});
28+
it('displays the right information', () => {
29+
store.dispatch(setPrimers([
30+
{ id: 1, name: 'P1', sequence: 'TCATTAAAGTTAACG' },
31+
]));
32+
33+
cy.mount(
34+
<Provider store={store}>
35+
<PrimerList />
36+
</Provider>
37+
);
38+
cy.get('td.name').contains('P1');
39+
cy.get('td.length').contains('15');
40+
cy.get('td.gc-content').contains('27');
41+
cy.get('td.melting-temperature').contains('37.5');
42+
cy.get('td.sequence').contains('TCATTAAAGTTAACG');
43+
});
44+
45+
it('caches primer details across re-renders and re-renders on global settings change', () => {
46+
let calls = 0;
47+
store.dispatch(setPrimers([
48+
{ id: 1, name: 'P1', sequence: 'AAA' },
49+
]));
50+
cy.intercept('POST', 'http://127.0.0.1:8000/primer_details*', (req) => {
51+
calls += 1;
52+
const respReply = calls === 1 ? mockReply : {
53+
statusCode: 200, body: {
54+
...mockReply.body,
55+
melting_temperature: calls === 1 ? 60 : 70,
56+
}
57+
}
58+
expect(req.body).to.deep.equal({
59+
sequence: 'AAA',
60+
settings: {
61+
primer_dna_conc: calls === 1 ? 50 : 100,
62+
primer_salt_monovalent: 50,
63+
primer_salt_divalent: 1.5,
64+
},
65+
});
66+
req.reply(respReply);
67+
}).as('primerDetails');
68+
69+
// First mount triggers two network calls (one per unique primer sequence)
70+
cy.mount(
71+
<Provider store={store}>
72+
<PrimerList />
73+
</Provider>)
74+
cy.contains('Loading...').should('not.exist');
75+
cy.wait('@primerDetails');
76+
cy.then(() => {
77+
expect(calls).to.equal(1);
78+
cy.mount(
79+
<Provider store={store}>
80+
<PrimerList />
81+
</Provider>)
82+
cy.then(() => {
83+
expect(calls).to.equal(1);
84+
});
85+
store.dispatch(setGlobalPrimerSettings({ primer_dna_conc: 100 }))
86+
cy.wait('@primerDetails');
87+
cy.then(() => {
88+
expect(calls).to.equal(2);
89+
cy.get('td.melting-temperature').contains('70');
90+
});
91+
92+
});
93+
94+
});
95+
});

src/components/primers/PrimerList.jsx

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,47 +76,47 @@ function PrimerList() {
7676
</div>
7777
<div className="primer-form-container">
7878
{(editingPrimerId && (
79-
<PrimerForm
80-
key="primer-edit"
81-
submitPrimer={editPrimer}
82-
cancelForm={() => setEditingPrimerId(null)}
83-
existingNames={primers.filter((p) => p.name !== editingPrimer.name).map((p) => p.name)}
84-
disabledSequenceText={primerIdsInUse.includes(editingPrimerId) ? 'Cannot edit sequence in use' : ''}
85-
primer={editingPrimer}
86-
/>
79+
<PrimerForm
80+
key="primer-edit"
81+
submitPrimer={editPrimer}
82+
cancelForm={() => setEditingPrimerId(null)}
83+
existingNames={primers.filter((p) => p.name !== editingPrimer.name).map((p) => p.name)}
84+
disabledSequenceText={primerIdsInUse.includes(editingPrimerId) ? 'Cannot edit sequence in use' : ''}
85+
primer={editingPrimer}
86+
/>
8787
)) || (addingPrimer && (
88-
<PrimerForm
89-
key="primer-add"
90-
submitPrimer={addPrimer}
91-
cancelForm={switchAddingPrimer}
92-
existingNames={primers.map((p) => p.name)}
93-
/>
88+
<PrimerForm
89+
key="primer-add"
90+
submitPrimer={addPrimer}
91+
cancelForm={switchAddingPrimer}
92+
existingNames={primers.map((p) => p.name)}
93+
/>
9494
)) || (importingPrimer && (
9595
<PrimerDatabaseImportForm
9696
submitPrimer={addPrimer}
9797
cancelForm={() => setImportingPrimer(false)}
9898
existingNames={primers.map((p) => p.name)}
9999
/>
100100
)) || (
101-
<div className="primer-add-container">
102-
<Button
103-
variant="contained"
104-
onClick={switchAddingPrimer}
105-
>
106-
Add Primer
107-
</Button>
108-
<ImportPrimersButton addPrimer={addPrimer} />
109-
<DownloadPrimersButton primers={primers} />
110-
{database && (
101+
<div className="primer-add-container">
111102
<Button
112103
variant="contained"
113-
onClick={() => setImportingPrimer(true)}
104+
onClick={switchAddingPrimer}
114105
>
115-
{`Import from ${database.name}`}
106+
Add Primer
116107
</Button>
117-
)}
118-
</div>
119-
)}
108+
<ImportPrimersButton addPrimer={addPrimer} />
109+
<DownloadPrimersButton primers={primers} />
110+
{database && (
111+
<Button
112+
variant="contained"
113+
onClick={() => setImportingPrimer(true)}
114+
>
115+
{`Import from ${database.name}`}
116+
</Button>
117+
)}
118+
</div>
119+
)}
120120
</div>
121121

122122
</>

src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
191191
throw new Error('Invalid fragment orientation');
192192
}
193193
});
194-
const { cloning: { sequences, teselaJsonCache } } = store.getState();
194+
const { cloning: { sequences, teselaJsonCache, globalPrimerSettings } } = store.getState();
195195
let requestData;
196196
let params;
197197
let endpoint;
@@ -258,6 +258,7 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
258258
};
259259
}
260260

261+
requestData.settings = globalPrimerSettings;
261262
const url = backendRoute(`primer_design/${endpoint}`);
262263

263264
try {

0 commit comments

Comments
 (0)