@ccp-nc/crystvis-js 0.6.0 → 0.7.0
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.
- package/.github/dependabot.yml +69 -0
- package/.github/workflows/dependency-review.yml +91 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/test.yml +191 -0
- package/.github/workflows/update-dependencies.yml +214 -0
- package/.mocharc.yml +4 -0
- package/.vscode/settings.json +5 -1
- package/CHANGELOG.md +61 -14
- package/README.md +75 -2
- package/audit.txt +24 -0
- package/changes.txt +2 -0
- package/demo/demo.css +15 -10
- package/demo/index.html +32 -5
- package/demo/main.js +149 -42
- package/docs/data/search.json +1 -1
- package/docs/index.html +51 -2
- package/docs/lib_model.module_js-AtomImage.html +1 -1
- package/docs/lib_model.module_js-BondImage.html +1 -1
- package/docs/lib_model.module_js-Model.html +1 -1
- package/docs/lib_modelview.module_js-ModelView.html +1 -1
- package/docs/lib_visualizer.module_js-CrystVis.html +1 -1
- package/docs/model.js.html +31 -3
- package/docs/modelview.js.html +24 -0
- package/docs/visualizer.js.html +237 -1
- package/eslint.config.js +41 -0
- package/lib/assets/fonts/threebmfont.js +0 -1
- package/lib/formats/magres.js +156 -1
- package/lib/formats/xyz.js +149 -52
- package/lib/loader.js +28 -7
- package/lib/model.js +31 -3
- package/lib/modelview.js +24 -0
- package/lib/primitives/ellipsoid.js +0 -4
- package/lib/primitives/geometries.js +0 -0
- package/lib/primitives/isosurface.js +1 -4
- package/lib/query.js +3 -28
- package/lib/render.js +97 -2
- package/lib/selbox.js +18 -4
- package/lib/tensor.js +0 -1
- package/lib/visualizer.js +237 -1
- package/outdated.txt +9 -0
- package/package.json +15 -15
- package/test/data/ethanol_with_tensors.xyz +33 -0
- package/test/data/hf_mu_test.magres +62 -0
- package/test/data/hf_test.magres +61 -0
- package/test/data/optimized_muon_65-hf.magres +1424 -0
- package/test/loader.js +283 -9
- package/test/model.js +120 -4
- package/test/setup-dom.cjs +147 -0
- package/test/visualizer.js +475 -0
- package/.eslintrc.json +0 -16
- package/.github/workflows/test-mocha.yml +0 -30
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for CrystVis.dispose() and the isDisposed guard.
|
|
5
|
+
*
|
|
6
|
+
* These tests run in Node.js (no DOM / WebGL) by constructing a CrystVis
|
|
7
|
+
* instance via Object.create so we can supply lightweight mock objects in
|
|
8
|
+
* place of the real THREE.WebGLRenderer and OrbitControls.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as chai from 'chai';
|
|
12
|
+
import { CrystVis } from '../lib/visualizer.js';
|
|
13
|
+
|
|
14
|
+
const expect = chai.expect;
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the bare minimum mock Renderer that dispose() expects to find on
|
|
22
|
+
* this._renderer. Each call-count field starts at 0 so tests can assert
|
|
23
|
+
* that the matching method was called exactly once.
|
|
24
|
+
*/
|
|
25
|
+
function makeMockRenderer() {
|
|
26
|
+
const renderer = {
|
|
27
|
+
_disposed: false,
|
|
28
|
+
_disposeCalls: 0,
|
|
29
|
+
_setCameraStateCalls: [],
|
|
30
|
+
_cameraChangeHandlers: [],
|
|
31
|
+
_cameraState: { position: { x: 0, y: 0, z: 10 }, target: { x: 0, y: 0, z: 0 }, zoom: 1 },
|
|
32
|
+
dispose() {
|
|
33
|
+
this._disposed = true;
|
|
34
|
+
this._disposeCalls++;
|
|
35
|
+
},
|
|
36
|
+
clear() {},
|
|
37
|
+
addNotifications() {},
|
|
38
|
+
clearNotifications() {},
|
|
39
|
+
resetOrbitCenter() {},
|
|
40
|
+
add() {},
|
|
41
|
+
remove() {},
|
|
42
|
+
getCameraState() {
|
|
43
|
+
return Object.assign({}, this._cameraState,
|
|
44
|
+
{ position: Object.assign({}, this._cameraState.position),
|
|
45
|
+
target: Object.assign({}, this._cameraState.target) });
|
|
46
|
+
},
|
|
47
|
+
setCameraState(state) {
|
|
48
|
+
this._setCameraStateCalls.push(state);
|
|
49
|
+
if (state) Object.assign(this._cameraState, state);
|
|
50
|
+
},
|
|
51
|
+
onCameraChange(cb) {
|
|
52
|
+
this._cameraChangeHandlers.push(cb);
|
|
53
|
+
return () => {
|
|
54
|
+
this._cameraChangeHandlers =
|
|
55
|
+
this._cameraChangeHandlers.filter(h => h !== cb);
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
/** Trigger all registered camera-change handlers (test helper). */
|
|
59
|
+
_fireCameraChange() {
|
|
60
|
+
const state = this.getCameraState();
|
|
61
|
+
this._cameraChangeHandlers.forEach(h => h(state));
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return renderer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Return a CrystVis instance whose constructor has been bypassed so that
|
|
69
|
+
* no DOM element or WebGL context is needed. The internal _renderer is
|
|
70
|
+
* replaced with the supplied mock (or a fresh mock if none is given).
|
|
71
|
+
*/
|
|
72
|
+
function makeMockVis(mockRenderer) {
|
|
73
|
+
const r = mockRenderer || makeMockRenderer();
|
|
74
|
+
|
|
75
|
+
// Bypass the real constructor to avoid DOM / WebGL requirements
|
|
76
|
+
const vis = Object.create(CrystVis.prototype);
|
|
77
|
+
|
|
78
|
+
vis._isDisposed = false;
|
|
79
|
+
vis._renderer = r;
|
|
80
|
+
vis._loader = { load() { return {}; } };
|
|
81
|
+
vis._models = {};
|
|
82
|
+
vis._current_model = null;
|
|
83
|
+
vis._current_mname = null;
|
|
84
|
+
vis._displayed = null;
|
|
85
|
+
vis._selected = null;
|
|
86
|
+
vis._notifications = [];
|
|
87
|
+
vis._atom_click_events = {};
|
|
88
|
+
vis._atom_click_defaults = {};
|
|
89
|
+
vis._atom_box_event = null;
|
|
90
|
+
vis._hsel = false;
|
|
91
|
+
vis.cifsymtol = 1e-2;
|
|
92
|
+
|
|
93
|
+
// New state added by this batch of features
|
|
94
|
+
vis._model_sources = {};
|
|
95
|
+
vis._model_parameters = {};
|
|
96
|
+
vis._model_meta = {};
|
|
97
|
+
vis._model_list_change_cbs = [];
|
|
98
|
+
vis._display_change_cbs = [];
|
|
99
|
+
vis._camera_change_cbs = [];
|
|
100
|
+
// Wire the renderer's camera-change signal to the vis callback array,
|
|
101
|
+
// mirroring what the real constructor does.
|
|
102
|
+
vis._camera_unsub = r.onCameraChange((state) => {
|
|
103
|
+
vis._camera_change_cbs.forEach(cb => cb(state));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { vis, renderer: r };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Tests: dispose()
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
describe('CrystVis#dispose', function () {
|
|
114
|
+
|
|
115
|
+
it('should set isDisposed to true', function () {
|
|
116
|
+
const { vis } = makeMockVis();
|
|
117
|
+
expect(vis.isDisposed).to.be.false;
|
|
118
|
+
vis.dispose();
|
|
119
|
+
expect(vis.isDisposed).to.be.true;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should call renderer.dispose() exactly once', function () {
|
|
123
|
+
const { vis, renderer } = makeMockVis();
|
|
124
|
+
vis.dispose();
|
|
125
|
+
expect(renderer._disposeCalls).to.equal(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should null the internal _renderer reference', function () {
|
|
129
|
+
const { vis } = makeMockVis();
|
|
130
|
+
vis.dispose();
|
|
131
|
+
expect(vis._renderer).to.be.null;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should null _current_model and _current_mname', function () {
|
|
135
|
+
const { vis } = makeMockVis();
|
|
136
|
+
vis._current_mname = 'test';
|
|
137
|
+
vis.dispose();
|
|
138
|
+
expect(vis._current_model).to.be.null;
|
|
139
|
+
expect(vis._current_mname).to.be.null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should clear the _models map', function () {
|
|
143
|
+
const { vis } = makeMockVis();
|
|
144
|
+
vis._models = { a: {}, b: {} };
|
|
145
|
+
vis.dispose();
|
|
146
|
+
expect(Object.keys(vis._models)).to.have.length(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should clear model source / parameter / meta stores', function () {
|
|
150
|
+
const { vis } = makeMockVis();
|
|
151
|
+
vis._model_sources = { a: { text: 'x', extension: 'cif' } };
|
|
152
|
+
vis._model_parameters = { a: { supercell: [1,1,1] } };
|
|
153
|
+
vis._model_meta = { a: { prefix: 'cif', originalName: 'a' } };
|
|
154
|
+
vis.dispose();
|
|
155
|
+
expect(Object.keys(vis._model_sources)).to.have.length(0);
|
|
156
|
+
expect(Object.keys(vis._model_parameters)).to.have.length(0);
|
|
157
|
+
expect(Object.keys(vis._model_meta)).to.have.length(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should clear lifecycle and camera-change callback arrays', function () {
|
|
161
|
+
const { vis } = makeMockVis();
|
|
162
|
+
vis._model_list_change_cbs.push(() => {});
|
|
163
|
+
vis._display_change_cbs.push(() => {});
|
|
164
|
+
vis._camera_change_cbs.push(() => {});
|
|
165
|
+
vis.dispose();
|
|
166
|
+
expect(vis._model_list_change_cbs).to.have.length(0);
|
|
167
|
+
expect(vis._display_change_cbs).to.have.length(0);
|
|
168
|
+
expect(vis._camera_change_cbs).to.have.length(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should unsubscribe from renderer camera-change on dispose', function () {
|
|
172
|
+
const { vis, renderer } = makeMockVis();
|
|
173
|
+
// One handler was registered by makeMockVis wiring
|
|
174
|
+
expect(renderer._cameraChangeHandlers).to.have.length(1);
|
|
175
|
+
vis.dispose();
|
|
176
|
+
expect(renderer._cameraChangeHandlers).to.have.length(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should clear atom event callbacks', function () {
|
|
180
|
+
const { vis } = makeMockVis();
|
|
181
|
+
vis._atom_click_events = { 1: () => {} };
|
|
182
|
+
vis._atom_box_event = () => {};
|
|
183
|
+
vis.dispose();
|
|
184
|
+
expect(Object.keys(vis._atom_click_events)).to.have.length(0);
|
|
185
|
+
expect(vis._atom_box_event).to.be.null;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should be idempotent: calling dispose() twice is harmless', function () {
|
|
189
|
+
const { vis, renderer } = makeMockVis();
|
|
190
|
+
vis.dispose();
|
|
191
|
+
vis.dispose();
|
|
192
|
+
// renderer.dispose() should only have been called the first time
|
|
193
|
+
expect(renderer._disposeCalls).to.equal(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Tests: isDisposed guard
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
describe('CrystVis#isDisposed guard', function () {
|
|
203
|
+
|
|
204
|
+
it('loadModels() should throw after disposal', function () {
|
|
205
|
+
const { vis } = makeMockVis();
|
|
206
|
+
vis.dispose();
|
|
207
|
+
expect(() => vis.loadModels('data', 'xyz')).to.throw(
|
|
208
|
+
/cannot call loadModels\(\) on a disposed instance/
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('displayModel() should throw after disposal', function () {
|
|
213
|
+
const { vis } = makeMockVis();
|
|
214
|
+
vis.dispose();
|
|
215
|
+
expect(() => vis.displayModel('mymodel')).to.throw(
|
|
216
|
+
/cannot call displayModel\(\) on a disposed instance/
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('reloadModel() should throw after disposal', function () {
|
|
221
|
+
const { vis } = makeMockVis();
|
|
222
|
+
vis.dispose();
|
|
223
|
+
expect(() => vis.reloadModel('mymodel')).to.throw(
|
|
224
|
+
/cannot call reloadModel\(\) on a disposed instance/
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('unloadAll() should throw after disposal', function () {
|
|
229
|
+
const { vis } = makeMockVis();
|
|
230
|
+
vis.dispose();
|
|
231
|
+
expect(() => vis.unloadAll()).to.throw(
|
|
232
|
+
/cannot call unloadAll\(\) on a disposed instance/
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('isDisposed getter returns false before disposal', function () {
|
|
237
|
+
const { vis } = makeMockVis();
|
|
238
|
+
expect(vis.isDisposed).to.be.false;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('isDisposed getter returns true after disposal', function () {
|
|
242
|
+
const { vis } = makeMockVis();
|
|
243
|
+
vis.dispose();
|
|
244
|
+
expect(vis.isDisposed).to.be.true;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Tests: camera state (§1)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
describe('CrystVis#getCameraState / setCameraState', function () {
|
|
254
|
+
|
|
255
|
+
it('getCameraState() delegates to the renderer', function () {
|
|
256
|
+
const { vis, renderer } = makeMockVis();
|
|
257
|
+
const state = vis.getCameraState();
|
|
258
|
+
expect(state).to.deep.equal(renderer._cameraState);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('getCameraState() returns a snapshot (not the live object)', function () {
|
|
262
|
+
const { vis, renderer } = makeMockVis();
|
|
263
|
+
const state = vis.getCameraState();
|
|
264
|
+
// Mutate the returned snapshot – the renderer's state must not change
|
|
265
|
+
state.zoom = 999;
|
|
266
|
+
expect(renderer._cameraState.zoom).to.not.equal(999);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('setCameraState() delegates to the renderer', function () {
|
|
270
|
+
const { vis, renderer } = makeMockVis();
|
|
271
|
+
const newState = { position: { x: 1, y: 2, z: 3 }, target: { x: 0, y: 0, z: 0 }, zoom: 2 };
|
|
272
|
+
vis.setCameraState(newState);
|
|
273
|
+
expect(renderer._setCameraStateCalls).to.have.length(1);
|
|
274
|
+
expect(renderer._setCameraStateCalls[0]).to.equal(newState);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('CrystVis#onCameraChange', function () {
|
|
280
|
+
|
|
281
|
+
it('registers a callback that fires when the renderer emits a camera change', function () {
|
|
282
|
+
const { vis, renderer } = makeMockVis();
|
|
283
|
+
const received = [];
|
|
284
|
+
vis.onCameraChange(state => received.push(state));
|
|
285
|
+
renderer._fireCameraChange();
|
|
286
|
+
expect(received).to.have.length(1);
|
|
287
|
+
expect(received[0]).to.have.keys(['position', 'target', 'zoom']);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('supports multiple subscribers', function () {
|
|
291
|
+
const { vis, renderer } = makeMockVis();
|
|
292
|
+
let countA = 0, countB = 0;
|
|
293
|
+
vis.onCameraChange(() => countA++);
|
|
294
|
+
vis.onCameraChange(() => countB++);
|
|
295
|
+
renderer._fireCameraChange();
|
|
296
|
+
expect(countA).to.equal(1);
|
|
297
|
+
expect(countB).to.equal(1);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('returned unsubscribe function stops future callbacks', function () {
|
|
301
|
+
const { vis, renderer } = makeMockVis();
|
|
302
|
+
const received = [];
|
|
303
|
+
const unsub = vis.onCameraChange(state => received.push(state));
|
|
304
|
+
renderer._fireCameraChange(); // fires once
|
|
305
|
+
unsub();
|
|
306
|
+
renderer._fireCameraChange(); // should NOT reach the callback
|
|
307
|
+
expect(received).to.have.length(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Tests: model source / parameters / metadata (§2, §3)
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
describe('CrystVis#getModelSource / getModelParameters / getModelMeta', function () {
|
|
317
|
+
|
|
318
|
+
it('getModelSource() returns null for unknown model', function () {
|
|
319
|
+
const { vis } = makeMockVis();
|
|
320
|
+
expect(vis.getModelSource('missing')).to.be.null;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('getModelSource() returns a cloned copy of stored source', function () {
|
|
324
|
+
const { vis } = makeMockVis();
|
|
325
|
+
vis._model_sources['m'] = { text: 'data...', extension: 'cif' };
|
|
326
|
+
const src = vis.getModelSource('m');
|
|
327
|
+
expect(src).to.deep.equal({ text: 'data...', extension: 'cif' });
|
|
328
|
+
// Mutating the returned copy must not affect the store
|
|
329
|
+
src.text = 'modified';
|
|
330
|
+
expect(vis._model_sources['m'].text).to.equal('data...');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('getModelParameters() returns null for unknown model', function () {
|
|
334
|
+
const { vis } = makeMockVis();
|
|
335
|
+
expect(vis.getModelParameters('missing')).to.be.null;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('getModelParameters() returns a cloned copy of stored parameters', function () {
|
|
339
|
+
const { vis } = makeMockVis();
|
|
340
|
+
vis._model_parameters['m'] = { supercell: [2, 2, 1], molecularCrystal: true };
|
|
341
|
+
const params = vis.getModelParameters('m');
|
|
342
|
+
expect(params).to.deep.equal({ supercell: [2, 2, 1], molecularCrystal: true });
|
|
343
|
+
params.supercell[0] = 99;
|
|
344
|
+
expect(vis._model_parameters['m'].supercell[0]).to.equal(2);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('getModelMeta() returns null for unknown model', function () {
|
|
348
|
+
const { vis } = makeMockVis();
|
|
349
|
+
expect(vis.getModelMeta('missing')).to.be.null;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('getModelMeta() returns a cloned copy of stored metadata', function () {
|
|
353
|
+
const { vis } = makeMockVis();
|
|
354
|
+
vis._model_meta['m'] = { prefix: 'cif', originalName: 'struct' };
|
|
355
|
+
const meta = vis.getModelMeta('m');
|
|
356
|
+
expect(meta).to.deep.equal({ prefix: 'cif', originalName: 'struct' });
|
|
357
|
+
meta.prefix = 'changed';
|
|
358
|
+
expect(vis._model_meta['m'].prefix).to.equal('cif');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Tests: lifecycle events (§4)
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
describe('CrystVis#onModelListChange', function () {
|
|
368
|
+
|
|
369
|
+
it('fires with the current model-name list', function () {
|
|
370
|
+
const { vis } = makeMockVis();
|
|
371
|
+
vis._models = { a: {}, b: {} };
|
|
372
|
+
const received = [];
|
|
373
|
+
vis.onModelListChange(names => received.push(names));
|
|
374
|
+
vis._emitModelListChange();
|
|
375
|
+
expect(received).to.have.length(1);
|
|
376
|
+
expect(received[0].sort()).to.deep.equal(['a', 'b']);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('fires when deleteModel is called', function () {
|
|
380
|
+
const { vis } = makeMockVis();
|
|
381
|
+
// Inject a stub model that deleteModel won't choke on
|
|
382
|
+
vis._models = { m: {} };
|
|
383
|
+
// displayModel() (called inside deleteModel when current) calls renderer.clear()
|
|
384
|
+
// and _emitDisplayChange – fine with our mock
|
|
385
|
+
const received = [];
|
|
386
|
+
vis.onModelListChange(names => received.push(names.slice()));
|
|
387
|
+
vis.deleteModel('m');
|
|
388
|
+
expect(received).to.have.length(1);
|
|
389
|
+
expect(received[0]).to.deep.equal([]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('returned unsubscribe function prevents future callbacks', function () {
|
|
393
|
+
const { vis } = makeMockVis();
|
|
394
|
+
vis._models = { a: {} };
|
|
395
|
+
const received = [];
|
|
396
|
+
const unsub = vis.onModelListChange(names => received.push(names));
|
|
397
|
+
vis._emitModelListChange();
|
|
398
|
+
unsub();
|
|
399
|
+
vis._emitModelListChange();
|
|
400
|
+
expect(received).to.have.length(1);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('CrystVis#onDisplayChange', function () {
|
|
406
|
+
|
|
407
|
+
it('fires with null when displayModel() is called with no args', function () {
|
|
408
|
+
const { vis } = makeMockVis();
|
|
409
|
+
const received = [];
|
|
410
|
+
vis.onDisplayChange(name => received.push(name));
|
|
411
|
+
vis.displayModel(); // clear
|
|
412
|
+
expect(received).to.have.length(1);
|
|
413
|
+
expect(received[0]).to.be.null;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('returned unsubscribe function prevents future callbacks', function () {
|
|
417
|
+
const { vis } = makeMockVis();
|
|
418
|
+
const received = [];
|
|
419
|
+
const unsub = vis.onDisplayChange(name => received.push(name));
|
|
420
|
+
vis.displayModel();
|
|
421
|
+
unsub();
|
|
422
|
+
vis.displayModel();
|
|
423
|
+
expect(received).to.have.length(1);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Tests: unloadAll (§5)
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
describe('CrystVis#unloadAll', function () {
|
|
433
|
+
|
|
434
|
+
it('clears all model stores', function () {
|
|
435
|
+
const { vis } = makeMockVis();
|
|
436
|
+
vis._models = { a: {}, b: {} };
|
|
437
|
+
vis._model_sources = { a: { text: 'x', extension: 'cif' }, b: { text: 'y', extension: 'xyz' } };
|
|
438
|
+
vis._model_parameters = { a: {}, b: {} };
|
|
439
|
+
vis._model_meta = { a: {}, b: {} };
|
|
440
|
+
|
|
441
|
+
vis.unloadAll();
|
|
442
|
+
|
|
443
|
+
expect(Object.keys(vis._models)).to.have.length(0);
|
|
444
|
+
expect(Object.keys(vis._model_sources)).to.have.length(0);
|
|
445
|
+
expect(Object.keys(vis._model_parameters)).to.have.length(0);
|
|
446
|
+
expect(Object.keys(vis._model_meta)).to.have.length(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('emits onModelListChange with an empty list', function () {
|
|
450
|
+
const { vis } = makeMockVis();
|
|
451
|
+
vis._models = { a: {} };
|
|
452
|
+
const received = [];
|
|
453
|
+
vis.onModelListChange(names => received.push(names.slice()));
|
|
454
|
+
vis.unloadAll();
|
|
455
|
+
expect(received).to.have.length(1);
|
|
456
|
+
expect(received[0]).to.deep.equal([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('emits onDisplayChange with null', function () {
|
|
460
|
+
const { vis } = makeMockVis();
|
|
461
|
+
vis._models = { a: {} };
|
|
462
|
+
const received = [];
|
|
463
|
+
vis.onDisplayChange(name => received.push(name));
|
|
464
|
+
vis.unloadAll();
|
|
465
|
+
expect(received).to.have.length(1);
|
|
466
|
+
expect(received[0]).to.be.null;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('throws after disposal', function () {
|
|
470
|
+
const { vis } = makeMockVis();
|
|
471
|
+
vis.dispose();
|
|
472
|
+
expect(() => vis.unloadAll()).to.throw(/cannot call unloadAll\(\) on a disposed instance/);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
});
|
package/.eslintrc.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
|
2
|
-
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
|
3
|
-
|
|
4
|
-
name: Node.js CI
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
branches: [ master ]
|
|
9
|
-
pull_request:
|
|
10
|
-
branches: [ master ]
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
build:
|
|
14
|
-
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
|
|
17
|
-
strategy:
|
|
18
|
-
matrix:
|
|
19
|
-
node-version: [20.x, 22.x, 24.x]
|
|
20
|
-
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
21
|
-
|
|
22
|
-
steps:
|
|
23
|
-
- uses: actions/checkout@v4
|
|
24
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
25
|
-
uses: actions/setup-node@v4
|
|
26
|
-
with:
|
|
27
|
-
node-version: ${{ matrix.node-version }}
|
|
28
|
-
- run: npm ci
|
|
29
|
-
- run: npm run build --if-present
|
|
30
|
-
- run: npm test
|