Skip to content

Commit 6a8d78e

Browse files
version 2 features (#49)
* allow user to direct import to chemotion (#29) * Chem scanner zip processing (#34) * Feat/excel import collection (#36) * Feat/generate molfile ketcher (#40) * Chore/minor fixes (#43) * removed rxno from template list * added documentaiton link, fixed context menu hiding behind inspector * show kekulized structure of molecule * fix: restore context menu functionality broken by molecule tooltip, samle delete, hide tooltip on context menu open * fix: improve SMILES validation to support valid chemical structures, hide keys * fix: improve Chemotion ELN import compatibility Add vessel_size, gaseous, log_data to reaction template/schema * Feat/quill editor (#41) * Add Quill editor support for analysis content field - Update zodSchemes to handle content as both string and object - Add transform to automatically parse JSON stringified Delta objects - Support Quill Delta format with catchall for additional properties - Add content field to analysis extended metadata fields in utils - Implement ReactQuill editor for content field rendering - Create renderContentField to handle Delta object parsing - Support JSON string parsing for imported content - Graceful fallback for plain text or empty content - Make content field visible in analysis containers --------- Co-authored-by: Ali Zaib <45627267+alizaib1217@users.noreply.github.com>
1 parent 8080427 commit 6a8d78e

61 files changed

Lines changed: 301494 additions & 377 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ cypress/results/*
3939
cypress/reports/*
4040
cypress/screenshots/*
4141
cypress/videos/*
42+
43+
# demo/test data - ignore only specific large test files
44+
demo-fixtures/export-example.json
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { chemotionApi, ChemotionUploadResponse } from '../../../src/services/chemotionApi'
2+
3+
describe('ChemotionApiService - Essential Tests', () => {
4+
beforeEach(() => {
5+
chemotionApi.clearSession()
6+
})
7+
8+
describe('Session Management', () => {
9+
it('should start with no authentication', () => {
10+
expect(chemotionApi.isAuthenticated()).to.be.false
11+
})
12+
13+
it('should clear session data', () => {
14+
chemotionApi.clearSession()
15+
expect(chemotionApi.isAuthenticated()).to.be.false
16+
})
17+
})
18+
19+
describe('Upload Without Authentication', () => {
20+
it('should fail upload when not authenticated', () => {
21+
const blob = new Blob(['test'], { type: 'application/zip' })
22+
cy.wrap(chemotionApi.uploadToChemotion(blob, 'test.zip'))
23+
.then(result => result as ChemotionUploadResponse)
24+
.then((result) => {
25+
expect(result.success).to.be.false
26+
expect(result.message).to.equal('Not authenticated')
27+
})
28+
})
29+
})
30+
31+
describe('Error Messages', () => {
32+
it('should return proper structure for all responses', () => {
33+
const blob = new Blob(['test'], { type: 'application/zip' })
34+
cy.wrap(chemotionApi.uploadToChemotion(blob, 'test.zip'))
35+
.then(result => result as ChemotionUploadResponse)
36+
.then((result) => {
37+
expect(result).to.have.property('success')
38+
expect(result).to.have.property('message')
39+
expect(typeof result.success).to.equal('boolean')
40+
expect(typeof result.message).to.equal('string')
41+
})
42+
})
43+
})
44+
})
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import { parseExcelToExportJson } from '@/helper/excelParser'
2+
import * as XLSX from 'xlsx'
3+
4+
describe('excelParser', () => {
5+
describe('parseExcelToExportJson', () => {
6+
it('should parse Excel with samples and reactions', async () => {
7+
// Create a simple test Excel file
8+
const wb = XLSX.utils.book_new()
9+
10+
const samplesData = [
11+
['identifier', 'parent', 'name', 'smiles'],
12+
['sample1', 'reaction1', 'Ethanol', 'CCO'],
13+
]
14+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
15+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
16+
17+
const reactionsData = [
18+
['identifier', 'name', 'description'],
19+
['reaction1', 'Test Reaction', 'A test'],
20+
]
21+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
22+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
23+
24+
// Convert to File
25+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
26+
const blob = new Blob([wbout], {
27+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28+
})
29+
const file = new File([blob], 'test.xlsx')
30+
31+
// Parse the file
32+
const result = await parseExcelToExportJson(file)
33+
34+
// Verify structure
35+
expect(result).to.have.property('Collection')
36+
expect(result).to.have.property('Reaction')
37+
expect(result).to.have.property('Sample')
38+
expect(result).to.have.property('CollectionsReaction')
39+
expect(result).to.have.property('CollectionsSample')
40+
41+
// Verify we have one collection
42+
const collections = Object.values(result.Collection as object)
43+
expect(collections).to.have.length(1)
44+
expect(collections[0]).to.have.property('label')
45+
expect(collections[0].label).to.include('Excel Import')
46+
47+
// Verify we have one reaction
48+
const reactions = Object.values(result.Reaction as object)
49+
expect(reactions).to.have.length(1)
50+
expect(reactions[0]).to.have.property('name', 'Test Reaction')
51+
52+
// Verify we have one sample
53+
const samples = Object.values(result.Sample as object)
54+
expect(samples).to.have.length(1)
55+
expect(samples[0]).to.have.property('name', 'Ethanol')
56+
})
57+
58+
it('should parse Excel with only samples', async () => {
59+
const wb = XLSX.utils.book_new()
60+
61+
const samplesData = [
62+
['identifier', 'name', 'smiles'],
63+
['sample1', 'Water', 'O'],
64+
]
65+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
66+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
67+
68+
const reactionsData = [['identifier', 'name']]
69+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
70+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
71+
72+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
73+
const blob = new Blob([wbout])
74+
const file = new File([blob], 'test.xlsx')
75+
76+
const result = await parseExcelToExportJson(file)
77+
78+
// Should have collection and sample
79+
expect(result).to.have.property('Collection')
80+
expect(result).to.have.property('Sample')
81+
82+
const samples = Object.values(result.Sample as object)
83+
expect(samples).to.have.length(1)
84+
expect(samples[0]).to.have.property('name', 'Water')
85+
})
86+
87+
it('should handle samples with parent relationships', async () => {
88+
const wb = XLSX.utils.book_new()
89+
90+
const samplesData = [
91+
['identifier', 'parent', 'name'],
92+
['sample1', 'reaction1', 'Parent Sample'],
93+
['sample2', 'sample1', 'Child Sample'],
94+
]
95+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
96+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
97+
98+
const reactionsData = [
99+
['identifier', 'name'],
100+
['reaction1', 'Parent Reaction'],
101+
]
102+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
103+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
104+
105+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
106+
const blob = new Blob([wbout])
107+
const file = new File([blob], 'test.xlsx')
108+
109+
const result = await parseExcelToExportJson(file)
110+
111+
// Both samples should be created
112+
const samples = Object.values(result.Sample as object)
113+
expect(samples).to.have.length(2)
114+
})
115+
116+
it('should handle SMILES in samples', async () => {
117+
const wb = XLSX.utils.book_new()
118+
119+
const samplesData = [
120+
['identifier', 'name', 'smiles'],
121+
['sample1', 'Ethanol', 'CCO'],
122+
['sample2', 'Methanol', 'CO'],
123+
]
124+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
125+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
126+
127+
const reactionsData = [['identifier', 'name']]
128+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
129+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
130+
131+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
132+
const blob = new Blob([wbout])
133+
const file = new File([blob], 'test.xlsx')
134+
135+
const result = await parseExcelToExportJson(file)
136+
137+
// Should create molecules for samples with SMILES
138+
expect(result).to.have.property('Molecule')
139+
const molecules = Object.values(result.Molecule as object)
140+
expect(molecules).to.have.length(2)
141+
})
142+
143+
it('should create molecules for all samples (even without SMILES)', async () => {
144+
const wb = XLSX.utils.book_new()
145+
146+
const samplesData = [
147+
['identifier', 'name', 'smiles'],
148+
['sample1', 'Ethanol', 'CCO'],
149+
['sample2', 'No Structure', ''],
150+
]
151+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
152+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
153+
154+
const reactionsData = [['identifier', 'name']]
155+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
156+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
157+
158+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
159+
const blob = new Blob([wbout])
160+
const file = new File([blob], 'test.xlsx')
161+
162+
const result = await parseExcelToExportJson(file)
163+
164+
// Should create molecules for ALL samples
165+
expect(result).to.have.property('Molecule')
166+
const molecules = Object.values(result.Molecule as object)
167+
expect(molecules).to.have.length(2)
168+
169+
// Second sample should have molecule with null SMILES
170+
const samples = Object.values(result.Sample as object) as any[]
171+
expect(samples).to.have.length(2)
172+
samples.forEach((sample) => {
173+
expect(sample.molecule_id).to.not.be.null
174+
expect(sample.decoupled).to.equal(false)
175+
})
176+
})
177+
178+
it('should create reaction-sample links with reactionSchemeType', async () => {
179+
const wb = XLSX.utils.book_new()
180+
181+
const samplesData = [
182+
[
183+
'identifier',
184+
'parent',
185+
'name',
186+
'reactionSchemeType',
187+
'smiles',
188+
],
189+
['sample1', 'reaction1', 'Ethanol', 'startingMaterial', 'CCO'],
190+
['sample2', 'reaction1', 'Acetic Acid', 'reactant', 'CC(=O)O'],
191+
['sample3', 'reaction1', 'Ethyl Acetate', 'product', 'CCOC(C)=O'],
192+
['sample4', 'reaction1', 'Water', 'solvent', 'O'],
193+
]
194+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
195+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
196+
197+
const reactionsData = [
198+
['identifier', 'name', 'description'],
199+
['reaction1', 'Esterification', 'Test reaction'],
200+
]
201+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
202+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
203+
204+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
205+
const blob = new Blob([wbout])
206+
const file = new File([blob], 'test.xlsx')
207+
208+
const result = await parseExcelToExportJson(file)
209+
210+
// Verify reaction-sample links were created
211+
expect(result).to.have.property('ReactionsStartingMaterialSample')
212+
expect(result).to.have.property('ReactionsReactantSample')
213+
expect(result).to.have.property('ReactionsProductSample')
214+
expect(result).to.have.property('ReactionsSolventSample')
215+
216+
const startingMaterials = Object.values(
217+
result.ReactionsStartingMaterialSample as object,
218+
)
219+
const reactants = Object.values(result.ReactionsReactantSample as object)
220+
const products = Object.values(result.ReactionsProductSample as object)
221+
const solvents = Object.values(result.ReactionsSolventSample as object)
222+
223+
expect(startingMaterials).to.have.length(1)
224+
expect(reactants).to.have.length(1)
225+
expect(products).to.have.length(1)
226+
expect(solvents).to.have.length(1)
227+
})
228+
229+
it('should not create reaction-sample links for samples without reactionSchemeType', async () => {
230+
const wb = XLSX.utils.book_new()
231+
232+
const samplesData = [
233+
['identifier', 'parent', 'name', 'reactionSchemeType', 'smiles'],
234+
['sample1', 'reaction1', 'Ethanol', '', 'CCO'],
235+
['sample2', '', 'Standalone', 'none', 'CO'],
236+
]
237+
const ws_samples = XLSX.utils.aoa_to_sheet(samplesData)
238+
XLSX.utils.book_append_sheet(wb, ws_samples, 'Samples')
239+
240+
const reactionsData = [
241+
['identifier', 'name'],
242+
['reaction1', 'Test Reaction'],
243+
]
244+
const ws_reactions = XLSX.utils.aoa_to_sheet(reactionsData)
245+
XLSX.utils.book_append_sheet(wb, ws_reactions, 'Reactions')
246+
247+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
248+
const blob = new Blob([wbout])
249+
const file = new File([blob], 'test.xlsx')
250+
251+
const result = await parseExcelToExportJson(file)
252+
253+
// Should not create any reaction-sample links
254+
const startingMaterials = Object.values(
255+
result.ReactionsStartingMaterialSample as object,
256+
)
257+
const reactants = Object.values(result.ReactionsReactantSample as object)
258+
const products = Object.values(result.ReactionsProductSample as object)
259+
const solvents = Object.values(result.ReactionsSolventSample as object)
260+
261+
expect(startingMaterials).to.have.length(0)
262+
expect(reactants).to.have.length(0)
263+
expect(products).to.have.length(0)
264+
expect(solvents).to.have.length(0)
265+
})
266+
})
267+
})

0 commit comments

Comments
 (0)