@datagrok/sequence-translator 1.10.17 → 1.10.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ import {after, category, test, expect, awaitCheck, delay} from '@datagrok-libraries/test/src/test';
2
+ import * as grok from 'datagrok-api/grok';
3
+ import * as DG from 'datagrok-api/dg';
4
+
5
+ import $ from 'cash-dom';
6
+
7
+ import {tagAsOligoNucleotide} from '../oligo-renderer/converters';
8
+
9
+ const SAMPLE_HELM =
10
+ 'RNA1{r(A)p.r(C)p.r(G)p.r(U)p}|RNA2{r(U)p.r(C)p.r(G)p.r(A)p}$$$$';
11
+
12
+ function dialogCount(): number {
13
+ return $('.d4-dialog').length;
14
+ }
15
+
16
+ function closeAllDialogs(): void {
17
+ $('.d4-dialog .ui-btn-cancel, .d4-dialog .d4-dialog-header .grok-icon.fa-times').trigger('click');
18
+ }
19
+
20
+ category('OligoCellEditor', () => {
21
+ after(async () => {
22
+ closeAllDialogs();
23
+ grok.shell.closeAll();
24
+ });
25
+
26
+ test('cellEditor opens HELM editor for OligoNucleotide cell and saves on OK', async () => {
27
+ const col = DG.Column.fromStrings('seq', [SAMPLE_HELM]);
28
+ tagAsOligoNucleotide(col);
29
+ const df = DG.DataFrame.fromColumns([col]);
30
+ df.name = 'oligo-edit-test';
31
+ const tv = grok.shell.addTableView(df);
32
+
33
+ await awaitCheck(() => $(tv.root).find('.d4-grid canvas').length > 0,
34
+ 'Grid canvas did not appear', 5000);
35
+
36
+ // Find the cellEditor that the platform would dispatch on double-click for
37
+ // a column tagged quality=OligoNucleotide. Pre-fix: zero matches (the
38
+ // registration didn't exist). Post-fix: exactly one — editOligoNucleotideCell.
39
+ const matches = DG.Func.find({tags: ['cellEditor'], package: 'SequenceTranslator'})
40
+ .filter((f) => f.description === 'OligoNucleotide');
41
+ expect(matches.length, 1);
42
+ expect(matches[0].name, 'editOligoNucleotideCell');
43
+
44
+ const gridCell = tv.grid.cell('seq', 0);
45
+ expect(gridCell != null, true);
46
+ expect(gridCell.cell.value, SAMPLE_HELM);
47
+
48
+ const dialogsBefore = dialogCount();
49
+
50
+ // Invoke the cellEditor as the platform would on double-click.
51
+ // Pre-fix (delegating to Helm:editMoleculeCell): throws synchronously
52
+ // "The column of notation 'helm' must be 'Macromolecule'" — dialog never opens.
53
+ // Post-fix (using helmHelper.createWebEditorApp directly): dialog opens.
54
+ await matches[0].apply({cell: gridCell});
55
+
56
+ await awaitCheck(() => dialogCount() > dialogsBefore,
57
+ 'HELM editor dialog did not open within 15s', 15000);
58
+
59
+ // Wait for HWE async init (Dojo + JSDraw2 + monomer lib) to mount the editor.
60
+ // JSDraw2 renders to SVG, so wait for the OK button to be wired up — that's
61
+ // a reliable signal that the dialog footer is fully constructed.
62
+ await awaitCheck(() => $('.d4-dialog .ui-btn-ok, .d4-dialog button.ui-btn.ui-btn-ok').length > 0,
63
+ 'OK button did not appear in HELM editor dialog within 15s', 15000);
64
+
65
+ // Allow the editor a moment to load the HELM string into the canvas before we read it back.
66
+ await delay(1000);
67
+
68
+ const okBtn = $('.d4-dialog .ui-btn-ok, .d4-dialog button.ui-btn.ui-btn-ok').first();
69
+ expect(okBtn.length > 0, true, 'OK button not found in dialog');
70
+
71
+ okBtn.trigger('click');
72
+
73
+ await awaitCheck(() => dialogCount() <= dialogsBefore,
74
+ 'Dialog did not close after OK', 5000);
75
+
76
+ // After OK: cell.setValue(helmValue) must have run with the editor's HELM.
77
+ // The editor may canonicalize formatting, so we don't require byte-equality —
78
+ // we require it remains a valid two-strand HELM string for our input.
79
+ const after = gridCell.cell.value as string;
80
+ expect(typeof after === 'string' && after.includes('RNA1{') && after.includes('RNA2{') && after.includes('$$$$'), true,
81
+ `Expected cell value to remain valid HELM after OK; got: ${after}`);
82
+ });
83
+ });
@@ -1,6 +1,7 @@
1
1
  import {category, test, expect} from '@datagrok-libraries/test/src/test';
2
2
 
3
- import {parseHelmDuplex, looksLikeHelm} from '../oligo-renderer/helm-parser';
3
+ import {parseHelmDuplex, looksLikeHelm, canonicalizeHelm} from '../oligo-renderer/helm-parser';
4
+ import {ParsedNucleotide} from '../oligo-renderer/types';
4
5
  import {computeLayout, hitTest, drawDuplex} from '../oligo-renderer/canvas-renderer';
5
6
  import {
6
7
  resolveSugar, resolvePhosphate, resolveConjugate,
@@ -94,6 +95,68 @@ category('OligoRenderer: parser', () => {
94
95
  expect(m0.phosphate, 'zp');
95
96
  }
96
97
  });
98
+
99
+ test('parser strips brackets from multi-char base in parens', async () => {
100
+ // Bug: pre-fix, cleanupHelmSymbol was not called on the base, so base === '[5Br-dC]'.
101
+ // Post-fix: cleanupHelmSymbol('[5Br-dC]') → '5Br-dC'.
102
+ const dup = parseHelmDuplex('RNA1{r([5Br-dC])p}$$$$');
103
+ const m = dup.sense.monomers[0] as ParsedNucleotide;
104
+ expect(m.kind, 'nucleotide');
105
+ expect(m.sugar, 'r');
106
+ // pre-fix: '[5Br-dC]' post-fix: '5Br-dC'
107
+ expect(m.base, '5Br-dC');
108
+ expect(m.phosphate, 'p');
109
+ });
110
+
111
+ test('parser strips brackets from multi-char base on both strands', async () => {
112
+ // Covers both strands and different modification combinations.
113
+ // pre-fix: base === '[5meC]', '[5Br-dC]', '[5fU]' post-fix: '5meC', '5Br-dC', '5fU'
114
+ const dup = parseHelmDuplex('RNA1{r([5meC])p.[fl2r]([5Br-dC])[sp]}|RNA2{r([5fU])p}$$$$');
115
+ const senseFirst = dup.sense.monomers[0] as ParsedNucleotide;
116
+ const senseSecond = dup.sense.monomers[1] as ParsedNucleotide;
117
+ const antiFirst = dup.antisense!.monomers[0] as ParsedNucleotide;
118
+ // pre-fix: '[5meC]' post-fix: '5meC'
119
+ expect(senseFirst.base, '5meC');
120
+ expect(senseSecond.sugar, 'fl2r');
121
+ // pre-fix: '[5Br-dC]' post-fix: '5Br-dC'
122
+ expect(senseSecond.base, '5Br-dC');
123
+ expect(senseSecond.phosphate, 'sp');
124
+ // pre-fix: '[5fU]' post-fix: '5fU'
125
+ expect(antiFirst.base, '5fU');
126
+ });
127
+
128
+ test('parser leaves bare-letter base unchanged', async () => {
129
+ // Regression guard: single-letter bases must NOT be transformed.
130
+ // A, G, C, T, U are all single-char and do not have brackets → unchanged by cleanupHelmSymbol.
131
+ const dup = parseHelmDuplex('RNA1{r(A)p.r(G)p.r(C)p.r(T)p.r(U)p}$$$$');
132
+ const bases = dup.sense.monomers.map((m) => (m as ParsedNucleotide).base);
133
+ // Expected: ['A', 'G', 'C', 'T', 'U'] — unchanged for all five bases.
134
+ expect(bases.join(','), 'A,G,C,T,U');
135
+ });
136
+
137
+ test('canonicalizeHelm re-brackets multi-char base on output', async () => {
138
+ // serializeCanonicalMonomer emits `([${base}])` when base.length > 1.
139
+ // pre-fix: emitted '(5Br-dC)' (invalid HELM, missing brackets).
140
+ // post-fix: emits '([5Br-dC])' — round-trip is valid HELM.
141
+ const out = canonicalizeHelm('RNA1{r([5Br-dC])p}$$$$');
142
+ // Must contain the bracketed base form.
143
+ expect(out.includes('([5Br-dC])'), true,
144
+ `expected canonicalized HELM to contain '([5Br-dC])', got: ${out}`);
145
+ // Must NOT contain the bare (unbracketed) form.
146
+ expect(out.includes('(5Br-dC)') && !out.includes('([5Br-dC])'), false,
147
+ `must not emit unbracketed (5Br-dC) without wrapping brackets, got: ${out}`);
148
+ });
149
+
150
+ test('canonicalizeHelm keeps single-letter base unbracketed', async () => {
151
+ // serializeCanonicalMonomer: base.length === 1 → `(${base})`, not `([${base}])`.
152
+ // Expected output contains 'r(A)p' — single letter stays bare.
153
+ const out = canonicalizeHelm('RNA1{r(A)p}$$$$');
154
+ expect(out.includes('r(A)p'), true,
155
+ `expected single-letter base to stay unbracketed, got: ${out}`);
156
+ // Single-letter base must NOT get double-bracketed.
157
+ expect(out.includes('([A])'), false,
158
+ `single-letter base must NOT be wrapped in brackets, got: ${out}`);
159
+ });
97
160
  });
98
161
 
99
162
  category('OligoRenderer: modification dictionary', () => {