matterviz 0.3.4 → 0.3.6

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.
Files changed (852) hide show
  1. package/.vscode/launch.json +13 -0
  2. package/.vscodeignore +7 -0
  3. package/dist/assets/STLExporter-BpTH3YHE.js +8 -0
  4. package/dist/assets/browser-DdDecX_W.js +1 -0
  5. package/dist/assets/export-qgn-H9y6.js +2 -0
  6. package/dist/assets/main-DiKYzti2.css +1 -0
  7. package/dist/assets/moyo_wasm_bg-0ocwg7xY.wasm +0 -0
  8. package/dist/extension.js +31293 -0
  9. package/dist/src/lib/FilePicker.svelte +360 -0
  10. package/dist/src/lib/MillerIndexInput.svelte +66 -0
  11. package/dist/src/lib/api/mp.ts +26 -0
  12. package/dist/src/lib/api/optimade.ts +204 -0
  13. package/dist/src/lib/app.css +247 -0
  14. package/dist/src/lib/brillouin/BrillouinZone.svelte +549 -0
  15. package/dist/src/lib/brillouin/BrillouinZoneControls.svelte +144 -0
  16. package/dist/src/lib/brillouin/BrillouinZoneExportPane.svelte +146 -0
  17. package/dist/src/lib/brillouin/BrillouinZoneInfoPane.svelte +146 -0
  18. package/dist/src/lib/brillouin/BrillouinZoneScene.svelte +476 -0
  19. package/dist/src/lib/brillouin/BrillouinZoneTooltip.svelte +92 -0
  20. package/dist/src/lib/brillouin/compute.ts +529 -0
  21. package/dist/src/lib/brillouin/index.ts +8 -0
  22. package/dist/src/lib/brillouin/types.ts +51 -0
  23. package/dist/src/lib/chempot-diagram/ChemPotDiagram.svelte +327 -0
  24. package/dist/src/lib/chempot-diagram/ChemPotDiagram2D.svelte +846 -0
  25. package/dist/src/lib/chempot-diagram/ChemPotDiagram3D.svelte +3193 -0
  26. package/dist/src/lib/chempot-diagram/async-compute.svelte.ts +94 -0
  27. package/dist/src/lib/chempot-diagram/chempot-worker.ts +11 -0
  28. package/dist/src/lib/chempot-diagram/color.ts +42 -0
  29. package/dist/src/lib/chempot-diagram/compute.ts +1014 -0
  30. package/dist/src/lib/chempot-diagram/index.ts +6 -0
  31. package/dist/src/lib/chempot-diagram/pointer.ts +56 -0
  32. package/dist/src/lib/chempot-diagram/temperature.ts +77 -0
  33. package/dist/src/lib/chempot-diagram/types.ts +130 -0
  34. package/dist/src/lib/colors/index.ts +249 -0
  35. package/dist/src/lib/composition/BarChart.svelte +297 -0
  36. package/dist/src/lib/composition/BubbleChart.svelte +218 -0
  37. package/dist/src/lib/composition/Composition.svelte +165 -0
  38. package/dist/src/lib/composition/Formula.svelte +268 -0
  39. package/dist/src/lib/composition/FormulaFilter.svelte +1257 -0
  40. package/dist/src/lib/composition/PieChart.svelte +323 -0
  41. package/dist/src/lib/composition/format.ts +155 -0
  42. package/dist/src/lib/composition/index.ts +37 -0
  43. package/dist/src/lib/composition/parse.ts +605 -0
  44. package/dist/src/lib/constants.ts +134 -0
  45. package/dist/src/lib/controls.ts +42 -0
  46. package/dist/src/lib/convex-hull/ConvexHull.svelte +157 -0
  47. package/dist/src/lib/convex-hull/ConvexHull2D.svelte +825 -0
  48. package/dist/src/lib/convex-hull/ConvexHull3D.svelte +1801 -0
  49. package/dist/src/lib/convex-hull/ConvexHull4D.svelte +1398 -0
  50. package/dist/src/lib/convex-hull/ConvexHullControls.svelte +535 -0
  51. package/dist/src/lib/convex-hull/ConvexHullInfoPane.svelte +125 -0
  52. package/dist/src/lib/convex-hull/ConvexHullStats.svelte +929 -0
  53. package/dist/src/lib/convex-hull/ConvexHullTooltip.svelte +131 -0
  54. package/dist/src/lib/convex-hull/GasPressureControls.svelte +247 -0
  55. package/dist/src/lib/convex-hull/StructurePopup.svelte +151 -0
  56. package/dist/src/lib/convex-hull/TemperatureSlider.svelte +140 -0
  57. package/dist/src/lib/convex-hull/barycentric-coords.ts +246 -0
  58. package/dist/src/lib/convex-hull/demo-temperature.ts +63 -0
  59. package/dist/src/lib/convex-hull/gas-thermodynamics.ts +405 -0
  60. package/dist/src/lib/convex-hull/helpers.ts +932 -0
  61. package/dist/src/lib/convex-hull/index.ts +202 -0
  62. package/dist/src/lib/convex-hull/thermodynamics.ts +2192 -0
  63. package/dist/src/lib/convex-hull/types.ts +267 -0
  64. package/dist/src/lib/coordination/CoordinationBarPlot.svelte +311 -0
  65. package/dist/src/lib/coordination/calc-coordination.ts +93 -0
  66. package/dist/src/lib/coordination/index.ts +9 -0
  67. package/dist/src/lib/effects.svelte.ts +48 -0
  68. package/dist/src/lib/element/BohrAtom.svelte +147 -0
  69. package/dist/src/lib/element/ElementHeading.svelte +26 -0
  70. package/dist/src/lib/element/ElementPhoto.svelte +57 -0
  71. package/dist/src/lib/element/ElementStats.svelte +80 -0
  72. package/dist/src/lib/element/ElementTile.svelte +484 -0
  73. package/dist/src/lib/element/data.json.gz.d.ts +4 -0
  74. package/dist/src/lib/element/data.ts +14 -0
  75. package/dist/src/lib/element/index.ts +8 -0
  76. package/dist/src/lib/element/types.ts +62 -0
  77. package/dist/src/lib/feedback/ClickFeedback.svelte +58 -0
  78. package/dist/src/lib/feedback/DragOverlay.svelte +42 -0
  79. package/dist/src/lib/feedback/index.ts +4 -0
  80. package/dist/src/lib/fermi-surface/FermiSlice.svelte +189 -0
  81. package/dist/src/lib/fermi-surface/FermiSurface.svelte +600 -0
  82. package/dist/src/lib/fermi-surface/FermiSurfaceControls.svelte +448 -0
  83. package/dist/src/lib/fermi-surface/FermiSurfaceScene.svelte +794 -0
  84. package/dist/src/lib/fermi-surface/FermiSurfaceTooltip.svelte +111 -0
  85. package/dist/src/lib/fermi-surface/compute.ts +728 -0
  86. package/dist/src/lib/fermi-surface/constants.ts +32 -0
  87. package/dist/src/lib/fermi-surface/export.ts +64 -0
  88. package/dist/src/lib/fermi-surface/index.ts +14 -0
  89. package/dist/src/lib/fermi-surface/marching-cubes.ts +3 -0
  90. package/dist/src/lib/fermi-surface/parse.ts +574 -0
  91. package/dist/src/lib/fermi-surface/symmetry.ts +56 -0
  92. package/dist/src/lib/fermi-surface/types.ts +159 -0
  93. package/dist/src/lib/heatmap-matrix/HeatmapMatrix.svelte +1545 -0
  94. package/dist/src/lib/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
  95. package/dist/src/lib/heatmap-matrix/index.ts +167 -0
  96. package/dist/src/lib/heatmap-matrix/shared.ts +7 -0
  97. package/dist/src/lib/icons.ts +650 -0
  98. package/dist/src/lib/index.ts +61 -0
  99. package/dist/src/lib/io/decompress.ts +92 -0
  100. package/dist/src/lib/io/export.ts +385 -0
  101. package/dist/src/lib/io/fetch.ts +46 -0
  102. package/dist/src/lib/io/file-drop.ts +51 -0
  103. package/dist/src/lib/io/index.ts +7 -0
  104. package/dist/src/lib/io/is-binary.ts +24 -0
  105. package/dist/src/lib/io/types.ts +8 -0
  106. package/dist/src/lib/io/url-drop.ts +141 -0
  107. package/dist/src/lib/isosurface/Isosurface.svelte +285 -0
  108. package/dist/src/lib/isosurface/IsosurfaceControls.svelte +277 -0
  109. package/dist/src/lib/isosurface/index.ts +7 -0
  110. package/dist/src/lib/isosurface/parse.ts +656 -0
  111. package/dist/src/lib/isosurface/slice.ts +175 -0
  112. package/dist/src/lib/isosurface/types.ts +309 -0
  113. package/dist/src/lib/labels.ts +320 -0
  114. package/dist/src/lib/layout/FullscreenToggle.svelte +50 -0
  115. package/dist/src/lib/layout/InfoCard.svelte +120 -0
  116. package/dist/src/lib/layout/InfoTag.svelte +185 -0
  117. package/dist/src/lib/layout/PropertyFilter.svelte +246 -0
  118. package/dist/src/lib/layout/SettingsSection.svelte +148 -0
  119. package/dist/src/lib/layout/SubpageGrid.svelte +82 -0
  120. package/dist/src/lib/layout/fullscreen.ts +65 -0
  121. package/dist/src/lib/layout/index.ts +11 -0
  122. package/dist/src/lib/layout/json-tree/JsonNode.svelte +548 -0
  123. package/dist/src/lib/layout/json-tree/JsonTree.svelte +1230 -0
  124. package/dist/src/lib/layout/json-tree/JsonValue.svelte +334 -0
  125. package/dist/src/lib/layout/json-tree/index.ts +3 -0
  126. package/dist/src/lib/layout/json-tree/types.ts +126 -0
  127. package/dist/src/lib/layout/json-tree/utils.ts +682 -0
  128. package/dist/src/lib/marching-cubes.ts +614 -0
  129. package/dist/src/lib/math.ts +1081 -0
  130. package/dist/src/lib/overlays/ContextMenu.svelte +162 -0
  131. package/dist/src/lib/overlays/CopyButton.svelte +45 -0
  132. package/dist/src/lib/overlays/DragControlTab.svelte +98 -0
  133. package/dist/src/lib/overlays/DraggablePane.svelte +487 -0
  134. package/dist/src/lib/overlays/InfoPaneCards.svelte +149 -0
  135. package/dist/src/lib/overlays/index.ts +3 -0
  136. package/dist/src/lib/periodic-table/PeriodicTable.svelte +469 -0
  137. package/dist/src/lib/periodic-table/PeriodicTableControls.svelte +557 -0
  138. package/dist/src/lib/periodic-table/PropertySelect.svelte +37 -0
  139. package/dist/src/lib/periodic-table/index.ts +12 -0
  140. package/dist/src/lib/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +1086 -0
  141. package/dist/src/lib/phase-diagram/PhaseDiagramControls.svelte +444 -0
  142. package/dist/src/lib/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
  143. package/dist/src/lib/phase-diagram/PhaseDiagramExportPane.svelte +184 -0
  144. package/dist/src/lib/phase-diagram/PhaseDiagramTooltip.svelte +391 -0
  145. package/dist/src/lib/phase-diagram/TdbInfoPanel.svelte +203 -0
  146. package/dist/src/lib/phase-diagram/build-diagram.ts +186 -0
  147. package/dist/src/lib/phase-diagram/colors.ts +58 -0
  148. package/dist/src/lib/phase-diagram/diagram-input.ts +40 -0
  149. package/dist/src/lib/phase-diagram/index.ts +13 -0
  150. package/dist/src/lib/phase-diagram/parse.ts +348 -0
  151. package/dist/src/lib/phase-diagram/svg-to-diagram.ts +1023 -0
  152. package/dist/src/lib/phase-diagram/types.ts +144 -0
  153. package/dist/src/lib/phase-diagram/utils.ts +775 -0
  154. package/dist/src/lib/plot/AxisLabel.svelte +51 -0
  155. package/dist/src/lib/plot/BarPlot.svelte +2113 -0
  156. package/dist/src/lib/plot/BarPlotControls.svelte +66 -0
  157. package/dist/src/lib/plot/BinnedScatterPlot.svelte +1114 -0
  158. package/dist/src/lib/plot/ColorBar.svelte +721 -0
  159. package/dist/src/lib/plot/ColorScaleSelect.svelte +54 -0
  160. package/dist/src/lib/plot/ElementScatter.svelte +63 -0
  161. package/dist/src/lib/plot/FillArea.svelte +223 -0
  162. package/dist/src/lib/plot/Histogram.svelte +1558 -0
  163. package/dist/src/lib/plot/HistogramControls.svelte +212 -0
  164. package/dist/src/lib/plot/InteractiveAxisLabel.svelte +96 -0
  165. package/dist/src/lib/plot/Line.svelte +84 -0
  166. package/dist/src/lib/plot/PlotAxis.svelte +169 -0
  167. package/dist/src/lib/plot/PlotControls.svelte +537 -0
  168. package/dist/src/lib/plot/PlotLegend.svelte +569 -0
  169. package/dist/src/lib/plot/PlotTooltip.svelte +67 -0
  170. package/dist/src/lib/plot/PortalSelect.svelte +253 -0
  171. package/dist/src/lib/plot/ReferenceLine3D.svelte +156 -0
  172. package/dist/src/lib/plot/ReferencePlane.svelte +175 -0
  173. package/dist/src/lib/plot/ScatterPlot.svelte +2778 -0
  174. package/dist/src/lib/plot/ScatterPlot3D.svelte +529 -0
  175. package/dist/src/lib/plot/ScatterPlot3DControls.svelte +437 -0
  176. package/dist/src/lib/plot/ScatterPlot3DScene.svelte +912 -0
  177. package/dist/src/lib/plot/ScatterPlotControls.svelte +306 -0
  178. package/dist/src/lib/plot/ScatterPoint.svelte +182 -0
  179. package/dist/src/lib/plot/SpacegroupBarPlot.svelte +293 -0
  180. package/dist/src/lib/plot/Surface3D.svelte +197 -0
  181. package/dist/src/lib/plot/ZeroLines.svelte +97 -0
  182. package/dist/src/lib/plot/ZoomRect.svelte +23 -0
  183. package/dist/src/lib/plot/adaptive-density.ts +316 -0
  184. package/dist/src/lib/plot/auto-place.ts +184 -0
  185. package/dist/src/lib/plot/axis-utils.ts +122 -0
  186. package/dist/src/lib/plot/binned-scatter-types.ts +83 -0
  187. package/dist/src/lib/plot/data-cleaning.ts +1069 -0
  188. package/dist/src/lib/plot/data-transform.ts +69 -0
  189. package/dist/src/lib/plot/defaults.ts +9 -0
  190. package/dist/src/lib/plot/fill-utils.ts +494 -0
  191. package/dist/src/lib/plot/hover-lock.svelte.ts +60 -0
  192. package/dist/src/lib/plot/index.ts +53 -0
  193. package/dist/src/lib/plot/interactions.ts +119 -0
  194. package/dist/src/lib/plot/layout.ts +425 -0
  195. package/dist/src/lib/plot/reference-line.ts +426 -0
  196. package/dist/src/lib/plot/scales.ts +654 -0
  197. package/dist/src/lib/plot/svg.ts +23 -0
  198. package/dist/src/lib/plot/types.ts +1144 -0
  199. package/dist/src/lib/plot/utils/label-placement.ts +541 -0
  200. package/dist/src/lib/plot/utils/series-visibility.ts +140 -0
  201. package/dist/src/lib/plot/utils.ts +11 -0
  202. package/dist/src/lib/rdf/RdfPlot.svelte +247 -0
  203. package/dist/src/lib/rdf/calc-rdf.ts +167 -0
  204. package/dist/src/lib/rdf/index.ts +27 -0
  205. package/dist/src/lib/sanitize.ts +126 -0
  206. package/dist/src/lib/settings.ts +1479 -0
  207. package/dist/src/lib/spectral/Bands.svelte +1040 -0
  208. package/dist/src/lib/spectral/BandsAndDos.svelte +134 -0
  209. package/dist/src/lib/spectral/BrillouinBandsDos.svelte +252 -0
  210. package/dist/src/lib/spectral/Dos.svelte +697 -0
  211. package/dist/src/lib/spectral/helpers.ts +1381 -0
  212. package/dist/src/lib/spectral/index.ts +8 -0
  213. package/dist/src/lib/spectral/types.ts +112 -0
  214. package/dist/src/lib/state.svelte.ts +64 -0
  215. package/dist/src/lib/structure/Arrow.svelte +72 -0
  216. package/dist/src/lib/structure/AtomLegend.svelte +815 -0
  217. package/dist/src/lib/structure/Bond.svelte +140 -0
  218. package/dist/src/lib/structure/CanvasTooltip.svelte +33 -0
  219. package/dist/src/lib/structure/CellSelect.svelte +349 -0
  220. package/dist/src/lib/structure/Cylinder.svelte +45 -0
  221. package/dist/src/lib/structure/Lattice.svelte +196 -0
  222. package/dist/src/lib/structure/Structure.svelte +2248 -0
  223. package/dist/src/lib/structure/StructureControls.svelte +1273 -0
  224. package/dist/src/lib/structure/StructureExportPane.svelte +252 -0
  225. package/dist/src/lib/structure/StructureInfoPane.svelte +737 -0
  226. package/dist/src/lib/structure/StructureScene.svelte +2255 -0
  227. package/dist/src/lib/structure/atom-properties.ts +316 -0
  228. package/dist/src/lib/structure/bond-order-perception.ts +447 -0
  229. package/dist/src/lib/structure/bonding.ts +944 -0
  230. package/dist/src/lib/structure/export.ts +861 -0
  231. package/dist/src/lib/structure/index.ts +291 -0
  232. package/dist/src/lib/structure/label-placement.ts +130 -0
  233. package/dist/src/lib/structure/measure.ts +45 -0
  234. package/dist/src/lib/structure/parse.ts +1705 -0
  235. package/dist/src/lib/structure/partial-occupancy.ts +183 -0
  236. package/dist/src/lib/structure/pbc.ts +164 -0
  237. package/dist/src/lib/structure/supercell.ts +226 -0
  238. package/dist/src/lib/structure/validation.ts +11 -0
  239. package/dist/src/lib/symmetry/SymmetryStats.svelte +226 -0
  240. package/dist/src/lib/symmetry/WyckoffTable.svelte +120 -0
  241. package/dist/src/lib/symmetry/cell-transform.ts +118 -0
  242. package/dist/src/lib/symmetry/index.ts +348 -0
  243. package/dist/src/lib/symmetry/spacegroups.ts +404 -0
  244. package/dist/src/lib/table/HeatmapTable.svelte +1833 -0
  245. package/dist/src/lib/table/ToggleMenu.svelte +385 -0
  246. package/dist/src/lib/table/index.ts +139 -0
  247. package/dist/src/lib/theme/ThemeControl.svelte +53 -0
  248. package/dist/src/lib/theme/index.ts +107 -0
  249. package/dist/src/lib/theme/themes.mjs +297 -0
  250. package/dist/src/lib/time.ts +71 -0
  251. package/dist/src/lib/tooltip/TooltipContent.svelte +58 -0
  252. package/dist/src/lib/tooltip/index.ts +2 -0
  253. package/dist/src/lib/tooltip/types.ts +13 -0
  254. package/dist/src/lib/trajectory/Trajectory.svelte +1545 -0
  255. package/dist/src/lib/trajectory/TrajectoryError.svelte +128 -0
  256. package/dist/src/lib/trajectory/TrajectoryExportPane.svelte +357 -0
  257. package/dist/src/lib/trajectory/TrajectoryInfoPane.svelte +313 -0
  258. package/dist/src/lib/trajectory/constants.ts +7 -0
  259. package/dist/src/lib/trajectory/extract.ts +196 -0
  260. package/dist/src/lib/trajectory/format-detect.ts +96 -0
  261. package/dist/src/lib/trajectory/frame-reader.ts +456 -0
  262. package/dist/src/lib/trajectory/helpers.ts +217 -0
  263. package/dist/src/lib/trajectory/index.ts +218 -0
  264. package/dist/src/lib/trajectory/parse/ase.ts +109 -0
  265. package/dist/src/lib/trajectory/parse/hdf5.ts +173 -0
  266. package/dist/src/lib/trajectory/parse/index.ts +411 -0
  267. package/dist/src/lib/trajectory/parse/lammps.ts +215 -0
  268. package/dist/src/lib/trajectory/parse/vasp.ts +102 -0
  269. package/dist/src/lib/trajectory/parse/xyz.ts +143 -0
  270. package/dist/src/lib/trajectory/plotting.ts +599 -0
  271. package/dist/src/lib/trajectory/types.ts +13 -0
  272. package/dist/src/lib/utils.ts +56 -0
  273. package/dist/src/lib/xrd/XrdPlot.svelte +615 -0
  274. package/dist/src/lib/xrd/broadening.ts +130 -0
  275. package/dist/src/lib/xrd/calc-xrd.ts +397 -0
  276. package/dist/src/lib/xrd/index.ts +38 -0
  277. package/dist/src/lib/xrd/parse.ts +858 -0
  278. package/dist/webview.js +29421 -0
  279. package/icon.png +0 -0
  280. package/license +1 -1
  281. package/matterviz-0.3.2.vsix +0 -0
  282. package/matterviz-0.3.4.vsix +0 -0
  283. package/matterviz-0.3.5.vsix +0 -0
  284. package/package.json +1461 -231
  285. package/readme.md +171 -98
  286. package/scripts/sync-config.ts +101 -0
  287. package/src/declarations.d.ts +2 -0
  288. package/src/extension.ts +972 -0
  289. package/src/node-io.ts +65 -0
  290. package/src/types.ts +17 -0
  291. package/src/webview/JsonBrowser.svelte +1079 -0
  292. package/src/webview/PlotPanel.svelte +346 -0
  293. package/src/webview/detect.ts +444 -0
  294. package/src/webview/main.ts +764 -0
  295. package/src/webview/plot-utils.ts +250 -0
  296. package/test-fixtures/all-viz-types.json.gz +0 -0
  297. package/test-fixtures/plot-demo-data.json.gz +0 -0
  298. package/tests/detect.test.ts +604 -0
  299. package/tests/extension.test.ts +2041 -0
  300. package/tests/node-io.test.ts +39 -0
  301. package/tests/plot-utils.test.ts +302 -0
  302. package/tests/vite-plugin-json-gz.test.ts +114 -0
  303. package/tests/vscode-mock.ts +18 -0
  304. package/tests/webview.test.ts +231 -0
  305. package/tsconfig.json +20 -0
  306. package/vite-plugin-json-gz.ts +29 -0
  307. package/vite.config.ts +34 -0
  308. package/vite.extension.config.ts +34 -0
  309. package/dist/EmptyState.svelte.d.ts +0 -9
  310. package/dist/FilePicker.svelte +0 -360
  311. package/dist/FilePicker.svelte.d.ts +0 -17
  312. package/dist/Icon.svelte.d.ts +0 -13
  313. package/dist/MillerIndexInput.svelte +0 -66
  314. package/dist/MillerIndexInput.svelte.d.ts +0 -7
  315. package/dist/api/mp.d.ts +0 -6
  316. package/dist/api/mp.js +0 -22
  317. package/dist/api/optimade.d.ts +0 -45
  318. package/dist/api/optimade.js +0 -135
  319. package/dist/app.css +0 -240
  320. package/dist/brillouin/BrillouinZone.svelte +0 -543
  321. package/dist/brillouin/BrillouinZone.svelte.d.ts +0 -83
  322. package/dist/brillouin/BrillouinZoneControls.svelte +0 -144
  323. package/dist/brillouin/BrillouinZoneControls.svelte.d.ts +0 -17
  324. package/dist/brillouin/BrillouinZoneExportPane.svelte +0 -148
  325. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +0 -15
  326. package/dist/brillouin/BrillouinZoneInfoPane.svelte +0 -146
  327. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +0 -13
  328. package/dist/brillouin/BrillouinZoneScene.svelte +0 -476
  329. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +0 -48
  330. package/dist/brillouin/BrillouinZoneTooltip.svelte +0 -92
  331. package/dist/brillouin/BrillouinZoneTooltip.svelte.d.ts +0 -8
  332. package/dist/brillouin/compute.d.ts +0 -17
  333. package/dist/brillouin/compute.js +0 -422
  334. package/dist/brillouin/index.d.ts +0 -8
  335. package/dist/brillouin/index.js +0 -8
  336. package/dist/brillouin/types.d.ts +0 -48
  337. package/dist/brillouin/types.js +0 -1
  338. package/dist/chempot-diagram/ChemPotDiagram.svelte +0 -327
  339. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +0 -13
  340. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +0 -847
  341. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +0 -16
  342. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +0 -3194
  343. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +0 -16
  344. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +0 -7
  345. package/dist/chempot-diagram/async-compute.svelte.d.ts +0 -3
  346. package/dist/chempot-diagram/async-compute.svelte.js +0 -77
  347. package/dist/chempot-diagram/chempot-worker.d.ts +0 -1
  348. package/dist/chempot-diagram/chempot-worker.js +0 -11
  349. package/dist/chempot-diagram/color.d.ts +0 -10
  350. package/dist/chempot-diagram/color.js +0 -32
  351. package/dist/chempot-diagram/compute.d.ts +0 -48
  352. package/dist/chempot-diagram/compute.js +0 -812
  353. package/dist/chempot-diagram/index.d.ts +0 -6
  354. package/dist/chempot-diagram/index.js +0 -6
  355. package/dist/chempot-diagram/pointer.d.ts +0 -16
  356. package/dist/chempot-diagram/pointer.js +0 -40
  357. package/dist/chempot-diagram/temperature.d.ts +0 -15
  358. package/dist/chempot-diagram/temperature.js +0 -36
  359. package/dist/chempot-diagram/types.d.ts +0 -86
  360. package/dist/chempot-diagram/types.js +0 -28
  361. package/dist/colors/index.d.ts +0 -47
  362. package/dist/colors/index.js +0 -203
  363. package/dist/composition/BarChart.svelte +0 -297
  364. package/dist/composition/BarChart.svelte.d.ts +0 -39
  365. package/dist/composition/BubbleChart.svelte +0 -218
  366. package/dist/composition/BubbleChart.svelte.d.ts +0 -28
  367. package/dist/composition/Composition.svelte +0 -164
  368. package/dist/composition/Composition.svelte.d.ts +0 -15
  369. package/dist/composition/Formula.svelte +0 -265
  370. package/dist/composition/Formula.svelte.d.ts +0 -19
  371. package/dist/composition/FormulaFilter.svelte +0 -1259
  372. package/dist/composition/FormulaFilter.svelte.d.ts +0 -51
  373. package/dist/composition/PieChart.svelte +0 -323
  374. package/dist/composition/PieChart.svelte.d.ts +0 -37
  375. package/dist/composition/format.d.ts +0 -15
  376. package/dist/composition/format.js +0 -109
  377. package/dist/composition/index.d.ts +0 -20
  378. package/dist/composition/index.js +0 -14
  379. package/dist/composition/parse.d.ts +0 -55
  380. package/dist/composition/parse.js +0 -459
  381. package/dist/constants.d.ts +0 -29
  382. package/dist/constants.js +0 -105
  383. package/dist/controls.d.ts +0 -14
  384. package/dist/controls.js +0 -30
  385. package/dist/convex-hull/ConvexHull.svelte +0 -157
  386. package/dist/convex-hull/ConvexHull.svelte.d.ts +0 -13
  387. package/dist/convex-hull/ConvexHull2D.svelte +0 -813
  388. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +0 -11
  389. package/dist/convex-hull/ConvexHull3D.svelte +0 -1788
  390. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +0 -8
  391. package/dist/convex-hull/ConvexHull4D.svelte +0 -1374
  392. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +0 -8
  393. package/dist/convex-hull/ConvexHullControls.svelte +0 -546
  394. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +0 -48
  395. package/dist/convex-hull/ConvexHullInfoPane.svelte +0 -115
  396. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +0 -18
  397. package/dist/convex-hull/ConvexHullStats.svelte +0 -905
  398. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +0 -15
  399. package/dist/convex-hull/ConvexHullTooltip.svelte +0 -131
  400. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +0 -33
  401. package/dist/convex-hull/GasPressureControls.svelte +0 -247
  402. package/dist/convex-hull/GasPressureControls.svelte.d.ts +0 -11
  403. package/dist/convex-hull/StructurePopup.svelte +0 -116
  404. package/dist/convex-hull/StructurePopup.svelte.d.ts +0 -18
  405. package/dist/convex-hull/TemperatureSlider.svelte +0 -137
  406. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +0 -8
  407. package/dist/convex-hull/barycentric-coords.d.ts +0 -18
  408. package/dist/convex-hull/barycentric-coords.js +0 -182
  409. package/dist/convex-hull/demo-temperature.d.ts +0 -6
  410. package/dist/convex-hull/demo-temperature.js +0 -40
  411. package/dist/convex-hull/gas-thermodynamics.d.ts +0 -16
  412. package/dist/convex-hull/gas-thermodynamics.js +0 -316
  413. package/dist/convex-hull/helpers.d.ts +0 -103
  414. package/dist/convex-hull/helpers.js +0 -671
  415. package/dist/convex-hull/index.d.ts +0 -118
  416. package/dist/convex-hull/index.js +0 -57
  417. package/dist/convex-hull/thermodynamics.d.ts +0 -66
  418. package/dist/convex-hull/thermodynamics.js +0 -1752
  419. package/dist/convex-hull/types.d.ts +0 -162
  420. package/dist/convex-hull/types.js +0 -36
  421. package/dist/coordination/CoordinationBarPlot.svelte +0 -311
  422. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +0 -30
  423. package/dist/coordination/calc-coordination.d.ts +0 -15
  424. package/dist/coordination/calc-coordination.js +0 -63
  425. package/dist/coordination/index.d.ts +0 -8
  426. package/dist/coordination/index.js +0 -7
  427. package/dist/element/BohrAtom.svelte +0 -149
  428. package/dist/element/BohrAtom.svelte.d.ts +0 -20
  429. package/dist/element/ElementHeading.svelte +0 -26
  430. package/dist/element/ElementHeading.svelte.d.ts +0 -8
  431. package/dist/element/ElementPhoto.svelte +0 -57
  432. package/dist/element/ElementPhoto.svelte.d.ts +0 -9
  433. package/dist/element/ElementStats.svelte +0 -80
  434. package/dist/element/ElementStats.svelte.d.ts +0 -8
  435. package/dist/element/ElementTile.svelte +0 -484
  436. package/dist/element/ElementTile.svelte.d.ts +0 -29
  437. package/dist/element/Nucleus.svelte.d.ts +0 -17
  438. package/dist/element/data.d.ts +0 -3
  439. package/dist/element/data.js +0 -2
  440. package/dist/element/data.json.gz.d.ts +0 -2
  441. package/dist/element/index.d.ts +0 -8
  442. package/dist/element/index.js +0 -8
  443. package/dist/element/types.d.ts +0 -57
  444. package/dist/element/types.js +0 -1
  445. package/dist/feedback/ClickFeedback.svelte +0 -58
  446. package/dist/feedback/ClickFeedback.svelte.d.ts +0 -12
  447. package/dist/feedback/DragOverlay.svelte +0 -42
  448. package/dist/feedback/DragOverlay.svelte.d.ts +0 -7
  449. package/dist/feedback/Spinner.svelte.d.ts +0 -7
  450. package/dist/feedback/StatusMessage.svelte.d.ts +0 -9
  451. package/dist/feedback/index.d.ts +0 -4
  452. package/dist/feedback/index.js +0 -4
  453. package/dist/fermi-surface/FermiSlice.svelte +0 -189
  454. package/dist/fermi-surface/FermiSlice.svelte.d.ts +0 -24
  455. package/dist/fermi-surface/FermiSurface.svelte +0 -597
  456. package/dist/fermi-surface/FermiSurface.svelte.d.ts +0 -83
  457. package/dist/fermi-surface/FermiSurfaceControls.svelte +0 -452
  458. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +0 -35
  459. package/dist/fermi-surface/FermiSurfaceScene.svelte +0 -792
  460. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +0 -50
  461. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +0 -111
  462. package/dist/fermi-surface/FermiSurfaceTooltip.svelte.d.ts +0 -8
  463. package/dist/fermi-surface/compute.d.ts +0 -5
  464. package/dist/fermi-surface/compute.js +0 -538
  465. package/dist/fermi-surface/constants.d.ts +0 -9
  466. package/dist/fermi-surface/constants.js +0 -27
  467. package/dist/fermi-surface/export.d.ts +0 -5
  468. package/dist/fermi-surface/export.js +0 -63
  469. package/dist/fermi-surface/index.d.ts +0 -12
  470. package/dist/fermi-surface/index.js +0 -13
  471. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  472. package/dist/fermi-surface/marching-cubes.js +0 -2
  473. package/dist/fermi-surface/parse.d.ts +0 -2
  474. package/dist/fermi-surface/parse.js +0 -495
  475. package/dist/fermi-surface/symmetry.d.ts +0 -3
  476. package/dist/fermi-surface/symmetry.js +0 -46
  477. package/dist/fermi-surface/types.d.ts +0 -113
  478. package/dist/fermi-surface/types.js +0 -4
  479. package/dist/heatmap-matrix/HeatmapMatrix.svelte +0 -1527
  480. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +0 -110
  481. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +0 -225
  482. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +0 -30
  483. package/dist/heatmap-matrix/index.d.ts +0 -53
  484. package/dist/heatmap-matrix/index.js +0 -100
  485. package/dist/heatmap-matrix/shared.d.ts +0 -2
  486. package/dist/heatmap-matrix/shared.js +0 -4
  487. package/dist/icons.d.ts +0 -569
  488. package/dist/icons.js +0 -648
  489. package/dist/index.d.ts +0 -39
  490. package/dist/index.js +0 -39
  491. package/dist/io/decompress.d.ts +0 -10
  492. package/dist/io/decompress.js +0 -69
  493. package/dist/io/export.d.ts +0 -16
  494. package/dist/io/export.js +0 -312
  495. package/dist/io/fetch.d.ts +0 -5
  496. package/dist/io/fetch.js +0 -39
  497. package/dist/io/file-drop.d.ts +0 -7
  498. package/dist/io/file-drop.js +0 -43
  499. package/dist/io/index.d.ts +0 -7
  500. package/dist/io/index.js +0 -7
  501. package/dist/io/is-binary.d.ts +0 -1
  502. package/dist/io/is-binary.js +0 -5
  503. package/dist/io/types.d.ts +0 -8
  504. package/dist/io/types.js +0 -1
  505. package/dist/io/url-drop.d.ts +0 -2
  506. package/dist/io/url-drop.js +0 -117
  507. package/dist/isosurface/Isosurface.svelte +0 -285
  508. package/dist/isosurface/Isosurface.svelte.d.ts +0 -8
  509. package/dist/isosurface/IsosurfaceControls.svelte +0 -291
  510. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +0 -9
  511. package/dist/isosurface/index.d.ts +0 -5
  512. package/dist/isosurface/index.js +0 -6
  513. package/dist/isosurface/parse.d.ts +0 -6
  514. package/dist/isosurface/parse.js +0 -553
  515. package/dist/isosurface/slice.d.ts +0 -11
  516. package/dist/isosurface/slice.js +0 -140
  517. package/dist/isosurface/types.d.ts +0 -56
  518. package/dist/isosurface/types.js +0 -227
  519. package/dist/labels.d.ts +0 -53
  520. package/dist/labels.js +0 -274
  521. package/dist/layout/FullscreenToggle.svelte +0 -50
  522. package/dist/layout/FullscreenToggle.svelte.d.ts +0 -7
  523. package/dist/layout/InfoCard.svelte +0 -120
  524. package/dist/layout/InfoCard.svelte.d.ts +0 -21
  525. package/dist/layout/InfoTag.svelte +0 -183
  526. package/dist/layout/InfoTag.svelte.d.ts +0 -19
  527. package/dist/layout/PropertyFilter.svelte +0 -244
  528. package/dist/layout/PropertyFilter.svelte.d.ts +0 -24
  529. package/dist/layout/SettingsSection.svelte +0 -148
  530. package/dist/layout/SettingsSection.svelte.d.ts +0 -17
  531. package/dist/layout/SubpageGrid.svelte +0 -82
  532. package/dist/layout/SubpageGrid.svelte.d.ts +0 -14
  533. package/dist/layout/fullscreen.d.ts +0 -9
  534. package/dist/layout/fullscreen.js +0 -53
  535. package/dist/layout/index.d.ts +0 -10
  536. package/dist/layout/index.js +0 -8
  537. package/dist/layout/json-tree/JsonNode.svelte +0 -547
  538. package/dist/layout/json-tree/JsonNode.svelte.d.ts +0 -11
  539. package/dist/layout/json-tree/JsonTree.svelte +0 -1222
  540. package/dist/layout/json-tree/JsonTree.svelte.d.ts +0 -6
  541. package/dist/layout/json-tree/JsonValue.svelte +0 -334
  542. package/dist/layout/json-tree/JsonValue.svelte.d.ts +0 -9
  543. package/dist/layout/json-tree/index.d.ts +0 -3
  544. package/dist/layout/json-tree/index.js +0 -3
  545. package/dist/layout/json-tree/types.d.ts +0 -73
  546. package/dist/layout/json-tree/types.js +0 -3
  547. package/dist/layout/json-tree/utils.d.ts +0 -29
  548. package/dist/layout/json-tree/utils.js +0 -648
  549. package/dist/marching-cubes.d.ts +0 -14
  550. package/dist/marching-cubes.js +0 -542
  551. package/dist/math.d.ts +0 -91
  552. package/dist/math.js +0 -896
  553. package/dist/overlays/ContextMenu.svelte +0 -162
  554. package/dist/overlays/ContextMenu.svelte.d.ts +0 -25
  555. package/dist/overlays/DraggablePane.svelte +0 -564
  556. package/dist/overlays/DraggablePane.svelte.d.ts +0 -36
  557. package/dist/overlays/index.d.ts +0 -2
  558. package/dist/overlays/index.js +0 -2
  559. package/dist/periodic-table/PeriodicTable.svelte +0 -469
  560. package/dist/periodic-table/PeriodicTable.svelte.d.ts +0 -55
  561. package/dist/periodic-table/PeriodicTableControls.svelte +0 -557
  562. package/dist/periodic-table/PeriodicTableControls.svelte.d.ts +0 -24
  563. package/dist/periodic-table/PropertySelect.svelte +0 -37
  564. package/dist/periodic-table/PropertySelect.svelte.d.ts +0 -13
  565. package/dist/periodic-table/TableInset.svelte.d.ts +0 -9
  566. package/dist/periodic-table/index.d.ts +0 -10
  567. package/dist/periodic-table/index.js +0 -4
  568. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +0 -1086
  569. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +0 -44
  570. package/dist/phase-diagram/PhaseDiagramControls.svelte +0 -451
  571. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +0 -30
  572. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +0 -126
  573. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +0 -15
  574. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +0 -192
  575. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +0 -19
  576. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +0 -392
  577. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +0 -16
  578. package/dist/phase-diagram/TdbInfoPanel.svelte +0 -203
  579. package/dist/phase-diagram/TdbInfoPanel.svelte.d.ts +0 -12
  580. package/dist/phase-diagram/build-diagram.d.ts +0 -11
  581. package/dist/phase-diagram/build-diagram.js +0 -167
  582. package/dist/phase-diagram/colors.d.ts +0 -35
  583. package/dist/phase-diagram/colors.js +0 -51
  584. package/dist/phase-diagram/diagram-input.d.ts +0 -33
  585. package/dist/phase-diagram/diagram-input.js +0 -3
  586. package/dist/phase-diagram/index.d.ts +0 -13
  587. package/dist/phase-diagram/index.js +0 -13
  588. package/dist/phase-diagram/parse.d.ts +0 -55
  589. package/dist/phase-diagram/parse.js +0 -276
  590. package/dist/phase-diagram/svg-to-diagram.d.ts +0 -2
  591. package/dist/phase-diagram/svg-to-diagram.js +0 -869
  592. package/dist/phase-diagram/types.d.ts +0 -99
  593. package/dist/phase-diagram/types.js +0 -1
  594. package/dist/phase-diagram/utils.d.ts +0 -118
  595. package/dist/phase-diagram/utils.js +0 -606
  596. package/dist/plot/AxisLabel.svelte +0 -51
  597. package/dist/plot/AxisLabel.svelte.d.ts +0 -16
  598. package/dist/plot/BarPlot.svelte +0 -2256
  599. package/dist/plot/BarPlot.svelte.d.ts +0 -82
  600. package/dist/plot/BarPlotControls.svelte +0 -66
  601. package/dist/plot/BarPlotControls.svelte.d.ts +0 -18
  602. package/dist/plot/ColorBar.svelte +0 -719
  603. package/dist/plot/ColorBar.svelte.d.ts +0 -31
  604. package/dist/plot/ColorScaleSelect.svelte +0 -54
  605. package/dist/plot/ColorScaleSelect.svelte.d.ts +0 -15
  606. package/dist/plot/ElementScatter.svelte +0 -63
  607. package/dist/plot/ElementScatter.svelte.d.ts +0 -14
  608. package/dist/plot/FillArea.svelte +0 -226
  609. package/dist/plot/FillArea.svelte.d.ts +0 -20
  610. package/dist/plot/Histogram.svelte +0 -1654
  611. package/dist/plot/Histogram.svelte.d.ts +0 -50
  612. package/dist/plot/HistogramControls.svelte +0 -212
  613. package/dist/plot/HistogramControls.svelte.d.ts +0 -22
  614. package/dist/plot/InteractiveAxisLabel.svelte +0 -94
  615. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +0 -14
  616. package/dist/plot/Line.svelte +0 -85
  617. package/dist/plot/Line.svelte.d.ts +0 -15
  618. package/dist/plot/PlotControls.svelte +0 -537
  619. package/dist/plot/PlotControls.svelte.d.ts +0 -4
  620. package/dist/plot/PlotLegend.svelte +0 -498
  621. package/dist/plot/PlotLegend.svelte.d.ts +0 -25
  622. package/dist/plot/PlotTooltip.svelte +0 -67
  623. package/dist/plot/PlotTooltip.svelte.d.ts +0 -17
  624. package/dist/plot/PortalSelect.svelte +0 -253
  625. package/dist/plot/PortalSelect.svelte.d.ts +0 -16
  626. package/dist/plot/ReferenceLine.svelte.d.ts +0 -20
  627. package/dist/plot/ReferenceLine3D.svelte +0 -154
  628. package/dist/plot/ReferenceLine3D.svelte.d.ts +0 -14
  629. package/dist/plot/ReferencePlane.svelte +0 -178
  630. package/dist/plot/ReferencePlane.svelte.d.ts +0 -14
  631. package/dist/plot/ScatterPlot.svelte +0 -2831
  632. package/dist/plot/ScatterPlot.svelte.d.ts +0 -92
  633. package/dist/plot/ScatterPlot3D.svelte +0 -499
  634. package/dist/plot/ScatterPlot3D.svelte.d.ts +0 -94
  635. package/dist/plot/ScatterPlot3DControls.svelte +0 -437
  636. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +0 -20
  637. package/dist/plot/ScatterPlot3DScene.svelte +0 -912
  638. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +0 -74
  639. package/dist/plot/ScatterPlotControls.svelte +0 -307
  640. package/dist/plot/ScatterPlotControls.svelte.d.ts +0 -17
  641. package/dist/plot/ScatterPoint.svelte +0 -185
  642. package/dist/plot/ScatterPoint.svelte.d.ts +0 -19
  643. package/dist/plot/SpacegroupBarPlot.svelte +0 -292
  644. package/dist/plot/SpacegroupBarPlot.svelte.d.ts +0 -9
  645. package/dist/plot/Surface3D.svelte +0 -200
  646. package/dist/plot/Surface3D.svelte.d.ts +0 -13
  647. package/dist/plot/ZeroLines.svelte +0 -96
  648. package/dist/plot/ZeroLines.svelte.d.ts +0 -32
  649. package/dist/plot/ZoomRect.svelte +0 -23
  650. package/dist/plot/ZoomRect.svelte.d.ts +0 -8
  651. package/dist/plot/axis-utils.d.ts +0 -19
  652. package/dist/plot/axis-utils.js +0 -80
  653. package/dist/plot/data-cleaning.d.ts +0 -37
  654. package/dist/plot/data-cleaning.js +0 -855
  655. package/dist/plot/data-transform.d.ts +0 -16
  656. package/dist/plot/data-transform.js +0 -45
  657. package/dist/plot/defaults.d.ts +0 -19
  658. package/dist/plot/defaults.js +0 -9
  659. package/dist/plot/fill-utils.d.ts +0 -51
  660. package/dist/plot/fill-utils.js +0 -337
  661. package/dist/plot/hover-lock.svelte.d.ts +0 -14
  662. package/dist/plot/hover-lock.svelte.js +0 -46
  663. package/dist/plot/index.d.ts +0 -43
  664. package/dist/plot/index.js +0 -37
  665. package/dist/plot/interactions.d.ts +0 -12
  666. package/dist/plot/interactions.js +0 -100
  667. package/dist/plot/layout.d.ts +0 -60
  668. package/dist/plot/layout.js +0 -230
  669. package/dist/plot/reference-line.d.ts +0 -60
  670. package/dist/plot/reference-line.js +0 -316
  671. package/dist/plot/scales.d.ts +0 -48
  672. package/dist/plot/scales.js +0 -484
  673. package/dist/plot/svg.d.ts +0 -1
  674. package/dist/plot/svg.js +0 -11
  675. package/dist/plot/types.d.ts +0 -863
  676. package/dist/plot/types.js +0 -103
  677. package/dist/plot/utils/label-placement.d.ts +0 -47
  678. package/dist/plot/utils/label-placement.js +0 -256
  679. package/dist/plot/utils/series-visibility.d.ts +0 -9
  680. package/dist/plot/utils/series-visibility.js +0 -67
  681. package/dist/plot/utils.d.ts +0 -1
  682. package/dist/plot/utils.js +0 -14
  683. package/dist/rdf/RdfPlot.svelte +0 -247
  684. package/dist/rdf/RdfPlot.svelte.d.ts +0 -27
  685. package/dist/rdf/calc-rdf.d.ts +0 -4
  686. package/dist/rdf/calc-rdf.js +0 -111
  687. package/dist/rdf/index.d.ts +0 -23
  688. package/dist/rdf/index.js +0 -2
  689. package/dist/sanitize.d.ts +0 -4
  690. package/dist/sanitize.js +0 -107
  691. package/dist/settings.d.ts +0 -253
  692. package/dist/settings.js +0 -1123
  693. package/dist/spectral/Bands.svelte +0 -1040
  694. package/dist/spectral/Bands.svelte.d.ts +0 -40
  695. package/dist/spectral/BandsAndDos.svelte +0 -128
  696. package/dist/spectral/BandsAndDos.svelte.d.ts +0 -18
  697. package/dist/spectral/BrillouinBandsDos.svelte +0 -248
  698. package/dist/spectral/BrillouinBandsDos.svelte.d.ts +0 -20
  699. package/dist/spectral/Dos.svelte +0 -697
  700. package/dist/spectral/Dos.svelte.d.ts +0 -29
  701. package/dist/spectral/helpers.d.ts +0 -117
  702. package/dist/spectral/helpers.js +0 -1023
  703. package/dist/spectral/index.d.ts +0 -6
  704. package/dist/spectral/index.js +0 -7
  705. package/dist/spectral/types.d.ts +0 -84
  706. package/dist/spectral/types.js +0 -2
  707. package/dist/state.svelte.d.ts +0 -25
  708. package/dist/state.svelte.js +0 -45
  709. package/dist/structure/Arrow.svelte +0 -72
  710. package/dist/structure/Arrow.svelte.d.ts +0 -15
  711. package/dist/structure/AtomLegend.svelte +0 -798
  712. package/dist/structure/AtomLegend.svelte.d.ts +0 -34
  713. package/dist/structure/Bond.svelte +0 -140
  714. package/dist/structure/Bond.svelte.d.ts +0 -9
  715. package/dist/structure/CanvasTooltip.svelte +0 -33
  716. package/dist/structure/CanvasTooltip.svelte.d.ts +0 -12
  717. package/dist/structure/CellSelect.svelte +0 -351
  718. package/dist/structure/CellSelect.svelte.d.ts +0 -13
  719. package/dist/structure/Cylinder.svelte +0 -45
  720. package/dist/structure/Cylinder.svelte.d.ts +0 -10
  721. package/dist/structure/Lattice.svelte +0 -196
  722. package/dist/structure/Lattice.svelte.d.ts +0 -17
  723. package/dist/structure/Structure.svelte +0 -1857
  724. package/dist/structure/Structure.svelte.d.ts +0 -83
  725. package/dist/structure/StructureControls.svelte +0 -1184
  726. package/dist/structure/StructureControls.svelte.d.ts +0 -31
  727. package/dist/structure/StructureExportPane.svelte +0 -251
  728. package/dist/structure/StructureExportPane.svelte.d.ts +0 -17
  729. package/dist/structure/StructureInfoPane.svelte +0 -434
  730. package/dist/structure/StructureInfoPane.svelte.d.ts +0 -18
  731. package/dist/structure/StructureScene.svelte +0 -1574
  732. package/dist/structure/StructureScene.svelte.d.ts +0 -104
  733. package/dist/structure/atom-properties.d.ts +0 -37
  734. package/dist/structure/atom-properties.js +0 -198
  735. package/dist/structure/bonding.d.ts +0 -33
  736. package/dist/structure/bonding.js +0 -304
  737. package/dist/structure/export.d.ts +0 -20
  738. package/dist/structure/export.js +0 -725
  739. package/dist/structure/ferrox-wasm-types.d.ts +0 -46
  740. package/dist/structure/ferrox-wasm-types.js +0 -18
  741. package/dist/structure/ferrox-wasm.d.ts +0 -94
  742. package/dist/structure/ferrox-wasm.js +0 -249
  743. package/dist/structure/index.d.ts +0 -110
  744. package/dist/structure/index.js +0 -168
  745. package/dist/structure/measure.d.ts +0 -6
  746. package/dist/structure/measure.js +0 -29
  747. package/dist/structure/parse.d.ts +0 -65
  748. package/dist/structure/parse.js +0 -1374
  749. package/dist/structure/partial-occupancy.d.ts +0 -25
  750. package/dist/structure/partial-occupancy.js +0 -99
  751. package/dist/structure/pbc.d.ts +0 -9
  752. package/dist/structure/pbc.js +0 -123
  753. package/dist/structure/supercell.d.ts +0 -8
  754. package/dist/structure/supercell.js +0 -137
  755. package/dist/structure/validation.d.ts +0 -2
  756. package/dist/structure/validation.js +0 -10
  757. package/dist/symmetry/SymmetryStats.svelte +0 -226
  758. package/dist/symmetry/SymmetryStats.svelte.d.ts +0 -21
  759. package/dist/symmetry/WyckoffTable.svelte +0 -113
  760. package/dist/symmetry/WyckoffTable.svelte.d.ts +0 -11
  761. package/dist/symmetry/cell-transform.d.ts +0 -12
  762. package/dist/symmetry/cell-transform.js +0 -77
  763. package/dist/symmetry/index.d.ts +0 -43
  764. package/dist/symmetry/index.js +0 -229
  765. package/dist/symmetry/spacegroups.d.ts +0 -9
  766. package/dist/symmetry/spacegroups.js +0 -394
  767. package/dist/table/HeatmapTable.svelte +0 -1854
  768. package/dist/table/HeatmapTable.svelte.d.ts +0 -49
  769. package/dist/table/ToggleMenu.svelte +0 -376
  770. package/dist/table/ToggleMenu.svelte.d.ts +0 -11
  771. package/dist/table/index.d.ts +0 -74
  772. package/dist/table/index.js +0 -38
  773. package/dist/theme/ThemeControl.svelte +0 -53
  774. package/dist/theme/ThemeControl.svelte.d.ts +0 -9
  775. package/dist/theme/index.d.ts +0 -29
  776. package/dist/theme/index.js +0 -79
  777. package/dist/theme/themes.mjs +0 -285
  778. package/dist/time.d.ts +0 -4
  779. package/dist/time.js +0 -70
  780. package/dist/tooltip/TooltipContent.svelte +0 -58
  781. package/dist/tooltip/TooltipContent.svelte.d.ts +0 -31
  782. package/dist/tooltip/index.d.ts +0 -2
  783. package/dist/tooltip/index.js +0 -2
  784. package/dist/tooltip/types.d.ts +0 -8
  785. package/dist/tooltip/types.js +0 -1
  786. package/dist/trajectory/Trajectory.svelte +0 -1517
  787. package/dist/trajectory/Trajectory.svelte.d.ts +0 -77
  788. package/dist/trajectory/TrajectoryError.svelte +0 -128
  789. package/dist/trajectory/TrajectoryError.svelte.d.ts +0 -13
  790. package/dist/trajectory/TrajectoryExportPane.svelte +0 -357
  791. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +0 -17
  792. package/dist/trajectory/TrajectoryInfoPane.svelte +0 -387
  793. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +0 -17
  794. package/dist/trajectory/constants.d.ts +0 -6
  795. package/dist/trajectory/constants.js +0 -7
  796. package/dist/trajectory/extract.d.ts +0 -5
  797. package/dist/trajectory/extract.js +0 -162
  798. package/dist/trajectory/format-detect.d.ts +0 -9
  799. package/dist/trajectory/format-detect.js +0 -76
  800. package/dist/trajectory/frame-reader.d.ts +0 -17
  801. package/dist/trajectory/frame-reader.js +0 -332
  802. package/dist/trajectory/helpers.d.ts +0 -14
  803. package/dist/trajectory/helpers.js +0 -172
  804. package/dist/trajectory/index.d.ts +0 -63
  805. package/dist/trajectory/index.js +0 -126
  806. package/dist/trajectory/parse/ase.d.ts +0 -2
  807. package/dist/trajectory/parse/ase.js +0 -77
  808. package/dist/trajectory/parse/hdf5.d.ts +0 -2
  809. package/dist/trajectory/parse/hdf5.js +0 -129
  810. package/dist/trajectory/parse/index.d.ts +0 -12
  811. package/dist/trajectory/parse/index.js +0 -299
  812. package/dist/trajectory/parse/lammps.d.ts +0 -5
  813. package/dist/trajectory/parse/lammps.js +0 -179
  814. package/dist/trajectory/parse/vasp.d.ts +0 -2
  815. package/dist/trajectory/parse/vasp.js +0 -68
  816. package/dist/trajectory/parse/xyz.d.ts +0 -2
  817. package/dist/trajectory/parse/xyz.js +0 -110
  818. package/dist/trajectory/plotting.d.ts +0 -28
  819. package/dist/trajectory/plotting.js +0 -423
  820. package/dist/trajectory/types.d.ts +0 -11
  821. package/dist/trajectory/types.js +0 -1
  822. package/dist/utils.d.ts +0 -5
  823. package/dist/utils.js +0 -36
  824. package/dist/xrd/XrdPlot.svelte +0 -615
  825. package/dist/xrd/XrdPlot.svelte.d.ts +0 -28
  826. package/dist/xrd/broadening.d.ts +0 -20
  827. package/dist/xrd/broadening.js +0 -97
  828. package/dist/xrd/calc-xrd.d.ts +0 -37
  829. package/dist/xrd/calc-xrd.js +0 -337
  830. package/dist/xrd/index.d.ts +0 -37
  831. package/dist/xrd/index.js +0 -4
  832. package/dist/xrd/parse.d.ts +0 -13
  833. package/dist/xrd/parse.js +0 -749
  834. /package/dist/{EmptyState.svelte → src/lib/EmptyState.svelte} +0 -0
  835. /package/dist/{Icon.svelte → src/lib/Icon.svelte} +0 -0
  836. /package/dist/{chempot-diagram → src/lib/chempot-diagram}/ChemPotScene3D.svelte +0 -0
  837. /package/dist/{colors → src/lib/colors}/alloy-colors.json +0 -0
  838. /package/dist/{colors → src/lib/colors}/dark-mode-colors.json +0 -0
  839. /package/dist/{colors → src/lib/colors}/jmol-colors.json +0 -0
  840. /package/dist/{colors → src/lib/colors}/muted-colors.json +0 -0
  841. /package/dist/{colors → src/lib/colors}/pastel-colors.json +0 -0
  842. /package/dist/{colors → src/lib/colors}/vesta-colors.json +0 -0
  843. /package/dist/{element → src/lib/element}/Nucleus.svelte +0 -0
  844. /package/dist/{element → src/lib/element}/data.json +0 -0
  845. /package/dist/{element → src/lib/element}/data.json.gz +0 -0
  846. /package/dist/{element → src/lib/element}/data.schema.json +0 -0
  847. /package/dist/{element-image-urls.json → src/lib/element-image-urls.json} +0 -0
  848. /package/dist/{feedback → src/lib/feedback}/Spinner.svelte +0 -0
  849. /package/dist/{feedback → src/lib/feedback}/StatusMessage.svelte +0 -0
  850. /package/dist/{periodic-table → src/lib/periodic-table}/TableInset.svelte +0 -0
  851. /package/dist/{plot → src/lib/plot}/ReferenceLine.svelte +0 -0
  852. /package/dist/{xrd → src/lib/xrd}/atomic_scattering_params.json +0 -0
@@ -0,0 +1,2041 @@
1
+ import {
2
+ STRUCTURE_EXTENSIONS,
3
+ TRAJ_EXTENSIONS,
4
+ TRAJ_KEYWORDS,
5
+ VASP_VOLUMETRIC_REGEX,
6
+ } from '$lib/constants'
7
+ import type { ThemeName } from '$lib/theme/index'
8
+ import { is_trajectory_file } from '$lib/trajectory/parse'
9
+ import { Buffer } from 'node:buffer'
10
+ import { beforeEach, describe, expect, test, vi } from 'vitest'
11
+ import type { ExtensionContext, Tab, TextEditor, Uri } from 'vscode'
12
+ import pkg_json from '../package.json' with { type: 'json' }
13
+ import {
14
+ activate,
15
+ create_html,
16
+ get_defaults,
17
+ get_file,
18
+ get_theme,
19
+ handle_msg,
20
+ MessageData,
21
+ read_file,
22
+ render,
23
+ should_auto_render,
24
+ } from '../src/extension'
25
+ import { VOLUMETRIC_VASP_RE } from '../src/types'
26
+ import type { FileData } from '../src/webview/main'
27
+
28
+ // Mock modules
29
+ const mock_fs = vi.hoisted(() => ({
30
+ readFileSync: vi.fn().mockReturnValue(`mock content`),
31
+ existsSync: vi.fn().mockReturnValue(true),
32
+ readdirSync: vi.fn().mockReturnValue([]),
33
+ }))
34
+
35
+ vi.mock(`node:fs`, () => mock_fs)
36
+ vi.mock(`node:path`, async (importOriginal) => {
37
+ const actual = (await importOriginal()) as Record<string, unknown>
38
+ return {
39
+ ...actual,
40
+ basename: vi.fn((p: string) => p.split(`/`).pop() || ``),
41
+ dirname: vi.fn((p: string) => p.split(`/`).slice(0, -1).join(`/`) || `/`),
42
+ join: vi.fn((...paths: string[]) => paths.join(`/`)),
43
+ isAbsolute: vi.fn((p: string) => p.startsWith(`/`)),
44
+ }
45
+ })
46
+
47
+ const msg_args = {
48
+ // generic placeholder arguments for all messages
49
+ filename: `filename`,
50
+ request_id: `request_id`,
51
+ file_path: `file_path`,
52
+ frame_index: 0,
53
+ } as const
54
+
55
+ const mock_vscode = vi.hoisted(() => ({
56
+ window: {
57
+ showInformationMessage: vi.fn(),
58
+ showErrorMessage: vi.fn(),
59
+ showTextDocument: vi.fn(),
60
+ showSaveDialog: vi.fn(),
61
+ createWebviewPanel: vi.fn(),
62
+ activeTextEditor: null as TextEditor | null,
63
+ tabGroups: { activeTabGroup: { activeTab: null as Tab | null } },
64
+ registerCustomEditorProvider: vi.fn(),
65
+ activeColorTheme: { kind: 1 }, // Light theme by default
66
+ onDidChangeActiveColorTheme: vi.fn(() => ({ dispose: vi.fn() })),
67
+ onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
68
+ },
69
+ workspace: {
70
+ getConfiguration: vi.fn(() => ({
71
+ get: vi.fn((_key: string, default_val: string) => default_val),
72
+ })),
73
+ onDidChangeConfiguration: vi.fn(() => ({ dispose: vi.fn() })),
74
+ openTextDocument: vi.fn(),
75
+ onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
76
+ onDidCreateFiles: vi.fn(() => ({ dispose: vi.fn() })),
77
+ onDidRenameFiles: vi.fn(() => ({ dispose: vi.fn() })),
78
+ createFileSystemWatcher: vi.fn(() => ({
79
+ onDidChange: vi.fn(),
80
+ onDidDelete: vi.fn(),
81
+ dispose: vi.fn(),
82
+ })),
83
+ fs: { stat: vi.fn(), readFile: vi.fn(), writeFile: vi.fn() },
84
+ },
85
+ commands: { registerCommand: vi.fn(), executeCommand: vi.fn() },
86
+ Uri: {
87
+ file: vi.fn((p: string) => ({ fsPath: p })),
88
+ joinPath: vi.fn((_base: unknown, ...paths: string[]) => ({
89
+ fsPath: paths.join(`/`),
90
+ })),
91
+ parse: vi.fn((url: string) => ({ toString: () => url })),
92
+ },
93
+ ViewColumn: { Active: 1, Beside: 2 },
94
+ ColorThemeKind: { Light: 1, Dark: 2, HighContrast: 3, HighContrastLight: 4 },
95
+ UIKind: { Desktop: 1, Web: 2 },
96
+ RelativePattern: class {
97
+ constructor(
98
+ public base: unknown,
99
+ public pattern: string,
100
+ ) {}
101
+ },
102
+ version: `1.99.0`,
103
+ env: {
104
+ appName: `VSCode`,
105
+ remoteName: undefined,
106
+ uiKind: 1, // Desktop
107
+ clipboard: {
108
+ writeText: vi.fn(() => Promise.resolve()),
109
+ readText: vi.fn(() => Promise.resolve(``)),
110
+ },
111
+ openExternal: vi.fn(() => Promise.resolve(true)),
112
+ },
113
+ }))
114
+
115
+ vi.mock(`vscode`, () => mock_vscode)
116
+
117
+ describe(`MatterViz Extension`, () => {
118
+ let mock_file_system_watcher: {
119
+ onDidChange: ReturnType<typeof vi.fn>
120
+ onDidDelete: ReturnType<typeof vi.fn>
121
+ dispose: ReturnType<typeof vi.fn>
122
+ }
123
+ const supported_volumetric_filenames: [string, string][] = [
124
+ [`CHGCAR`, `VASP volumetric`],
125
+ [`AECCAR0`, `VASP volumetric`],
126
+ [`AECCAR1`, `VASP volumetric`],
127
+ [`AECCAR2`, `VASP volumetric`],
128
+ [`ELFCAR`, `VASP volumetric`],
129
+ [`LOCPOT`, `VASP volumetric`],
130
+ [`PARCHG`, `VASP volumetric`],
131
+ [`PARCHG.gz`, `VASP volumetric`],
132
+ [`PARCHG.BAND_1`, `VASP volumetric`],
133
+ [`run_PARCHG_001`, `VASP volumetric`],
134
+ [`density.cube`, `Gaussian cube`],
135
+ [`density.cube.gz`, `Gaussian cube`],
136
+ ]
137
+ const volumetric_auto_render_filenames: [string, boolean][] =
138
+ supported_volumetric_filenames.map(([filename]) => [filename, true] as [string, boolean])
139
+
140
+ test(`extensionKind should be configured as ["workspace"] to work locally and in remote SSH sessions`, () => {
141
+ // https://github.com/janosh/matterviz/issues/129#issuecomment-3193473225
142
+ expect(pkg_json.extensionKind).toEqual([`workspace`])
143
+ })
144
+
145
+ test(`extension volumetric regex stays in sync with app detection`, () => {
146
+ expect(VOLUMETRIC_VASP_RE.source).toBe(VASP_VOLUMETRIC_REGEX.source)
147
+ expect(VOLUMETRIC_VASP_RE.flags).toBe(VASP_VOLUMETRIC_REGEX.flags)
148
+ })
149
+
150
+ describe(`Custom Editor File Patterns`, () => {
151
+ const custom_editors = pkg_json.contributes.customEditors
152
+ const matterviz_editor = custom_editors.find(
153
+ (editor) => editor.viewType === `matterviz.viewer`,
154
+ )
155
+ const patterns = matterviz_editor?.selector.map((sel) => sel.filenamePattern) ?? []
156
+
157
+ // Tests if a filename matches any pattern (simplified glob matching).
158
+ const matches_any_pattern = (filename: string): boolean => {
159
+ for (const pattern of patterns) {
160
+ // Convert glob pattern to regex
161
+ const regex_str = pattern
162
+ .replace(/\./g, `\\.`) // escape dots
163
+ .replace(/\*/g, `.*`) // * → .*
164
+ .replace(/\{([^}]+)\}/g, (_, group) => `(${group.split(`,`).join(`|`)})`) // {a,b} → (a|b)
165
+ const regex = new RegExp(`^${regex_str}$`, `i`)
166
+ if (regex.test(filename)) return true
167
+ }
168
+ return false
169
+ }
170
+
171
+ test(`should have matterviz.viewer custom editor defined`, () => {
172
+ expect(matterviz_editor).toBeDefined()
173
+ expect(matterviz_editor?.displayName).toBe(`MatterViz Viewer`)
174
+ })
175
+
176
+ // Tests auto-synced with library constants to prevent regressions
177
+ test.each<[string, string]>([
178
+ // All trajectory extensions from library
179
+ ...TRAJ_EXTENSIONS.map(
180
+ (ext: string) => [`test${ext}`, `TRAJ_EXT ${ext}`] as [string, string],
181
+ ),
182
+ // All structure extensions from library (uncompressed, .gz, .bz2)
183
+ ...STRUCTURE_EXTENSIONS.flatMap(
184
+ (ext: string) =>
185
+ [
186
+ [`test${ext}`, `STRUCT_EXT ${ext}`],
187
+ [`test${ext}.gz`, `${ext}.gz`],
188
+ ] as [string, string][],
189
+ ),
190
+ // All trajectory keywords in filenames
191
+ ...TRAJ_KEYWORDS.map(
192
+ (kw: string) => [`${kw}_output.dat`, `TRAJ_KW ${kw}`] as [string, string],
193
+ ),
194
+ // VASP special filenames + additional VS Code-only formats
195
+ [`POSCAR`, `VASP`],
196
+ [`CONTCAR`, `VASP`],
197
+ [`XDATCAR`, `VASP`],
198
+ [`OUTCAR`, `VASP`],
199
+ [`simulation.h5`, `HDF5`],
200
+ [`data.hdf5`, `HDF5`],
201
+ [`dynamics.dcd`, `DCD`],
202
+ [`run.trr`, `TRR`],
203
+ [`molecule.xyz`, `XYZ`],
204
+ [`atoms.extxyz`, `extXYZ`],
205
+ ...supported_volumetric_filenames,
206
+ ])(`pattern matches "%s" (%s)`, (filename) => {
207
+ expect(matches_any_pattern(filename)).toBe(true)
208
+ })
209
+
210
+ test.each([
211
+ [`myCHGCARfile`],
212
+ [`prefixPARCHGsuffix`],
213
+ [`notes_ELFCARbackup`],
214
+ [`report-AECCARnotes`],
215
+ [`density.cube.bz2`],
216
+ [`structure.cif.bz2`],
217
+ ])(`pattern does not match unsupported near miss "%s"`, (filename) => {
218
+ expect(matches_any_pattern(filename)).toBe(false)
219
+ })
220
+ })
221
+
222
+ beforeEach(async () => {
223
+ vi.clearAllMocks()
224
+
225
+ // Import extension module and clear all watchers
226
+ const ext = await import(`../src/extension`)
227
+ ext.active_watchers.clear()
228
+ ext.active_frame_loaders.clear()
229
+ ext.auto_render_timers.clear()
230
+ ext.active_auto_render_panels.clear()
231
+
232
+ mock_fs.readFileSync.mockReturnValue(`mock content`)
233
+ mock_fs.existsSync.mockReturnValue(true)
234
+ mock_fs.readdirSync.mockReturnValue([])
235
+ mock_vscode.window.activeTextEditor = null
236
+
237
+ // Reset theme to default Light theme to avoid inter-test coupling
238
+ mock_vscode.window.activeColorTheme = { kind: 1 } // Light theme by default
239
+
240
+ // Set up file system watcher mock
241
+ mock_file_system_watcher = {
242
+ onDidChange: vi.fn(),
243
+ onDidDelete: vi.fn(),
244
+ dispose: vi.fn(),
245
+ }
246
+ mock_vscode.workspace.createFileSystemWatcher.mockReturnValue(mock_file_system_watcher)
247
+
248
+ // Set up default mock for vscode.workspace.fs.stat to return file stats
249
+ mock_vscode.workspace.fs.stat.mockResolvedValue({ size: 1000, type: 1 })
250
+ mock_vscode.workspace.fs.readFile.mockResolvedValue(
251
+ new Uint8Array(Buffer.from(`mock content`)),
252
+ )
253
+ })
254
+
255
+ // Test data consolidation
256
+ const mock_webview = {
257
+ cspSource: `vscode-webview:`,
258
+ asWebviewUri: vi.fn(
259
+ (uri: { fsPath: string }) =>
260
+ `vscode-webview://unit-test${encodeURIComponent(uri.fsPath)}`,
261
+ ),
262
+ onDidReceiveMessage: vi.fn(),
263
+ postMessage: vi.fn(),
264
+ html: ``,
265
+ }
266
+ const mock_context = {
267
+ extensionUri: { fsPath: `/test` },
268
+ subscriptions: [],
269
+ } as unknown as ExtensionContext
270
+
271
+ test.each([
272
+ [`test.gz`, true],
273
+ [`test.h5`, true],
274
+ [`test.traj`, true], // ASE binary files should be treated as compressed
275
+ [`test.hdf5`, true],
276
+ [`md_npt_300K.traj`, true], // Specific ASE ULM binary file
277
+ [`ase-LiMnO2-chgnet-relax.traj`, true], // Another ASE ULM binary file
278
+ [`test.cif`, false],
279
+ [`test.xyz`, false], // .xyz files are text format, not compressed binary
280
+ [`test.json`, false],
281
+ [``, false],
282
+ ])(`file reading: "%s" → compressed:%s`, async (filename, expected_compressed) => {
283
+ const result = await read_file(`/test/${filename}`)
284
+ expect(result.filename).toBe(filename)
285
+ expect(result.is_base64).toBe(expected_compressed)
286
+ // Assert payload differences instead of redundant API calls
287
+ if (expected_compressed) {
288
+ expect(result.content).toBe(Buffer.from(`mock content`).toString(`base64`))
289
+ } else {
290
+ expect(result.content).toBe(`mock content`)
291
+ }
292
+ })
293
+
294
+ test(`file reading: large file should return sentinel`, async () => {
295
+ const large_file_size = 2 * 1024 * 1024 * 1024 // 2GB, above MAX_VSCODE_FILE_SIZE (1GB)
296
+ const filename = `large-structure.cif`
297
+ const file_path = `/test/${filename}`
298
+
299
+ // Mock fs.stat to return a size above the threshold
300
+ mock_vscode.workspace.fs.stat.mockResolvedValue({
301
+ size: large_file_size,
302
+ type: 1,
303
+ })
304
+
305
+ const result = await read_file(file_path)
306
+
307
+ expect(result.filename).toBe(filename)
308
+ expect(result.content).toBe(`LARGE_FILE:${file_path}:${large_file_size}`)
309
+ expect(result.is_base64).toBe(false)
310
+ // readFile should not be called for large files
311
+ expect(mock_vscode.workspace.fs.readFile).not.toHaveBeenCalled()
312
+ })
313
+
314
+ test(`file reading: large binary file should return sentinel with base64 flag`, async () => {
315
+ const large_file_size = 2 * 1024 * 1024 * 1024 // 2GB, above MAX_VSCODE_FILE_SIZE (1GB)
316
+ const filename = `large-trajectory.traj`
317
+ const file_path = `/test/${filename}`
318
+
319
+ // Mock fs.stat to return a size above the threshold
320
+ mock_vscode.workspace.fs.stat.mockResolvedValue({
321
+ size: large_file_size,
322
+ type: 1,
323
+ })
324
+
325
+ const result = await read_file(file_path)
326
+
327
+ expect(result.filename).toBe(filename)
328
+ expect(result.content).toBe(`LARGE_FILE:${file_path}:${large_file_size}`)
329
+ expect(result.is_base64).toBe(true) // Binary files should have base64 flag
330
+ // readFile should not be called for large files
331
+ expect(mock_vscode.workspace.fs.readFile).not.toHaveBeenCalled()
332
+ })
333
+
334
+ test.each([
335
+ [`md_npt_300K.traj`, true, true], // ASE binary trajectory
336
+ [`ase-LiMnO2-chgnet-relax.traj`, true, true], // ASE binary trajectory
337
+ [`simulation_nvt_250K.traj`, true, true], // ASE binary trajectory
338
+ [`water_cluster_md.traj`, true, true], // ASE binary trajectory
339
+ [`optimization_relax.traj`, true, true], // ASE binary trajectory
340
+ [`regular_text.traj`, true, true], // .traj files are always binary
341
+ // filename-only based .xyz/.extxyz detection always assumes structure, requires file content to look for frames and recognize as trajectory
342
+ [`test.xyz`, false, false],
343
+ [`test.extxyz`, false, false],
344
+ [`test.cif`, false, false], // Not a trajectory file
345
+ ])(
346
+ `ASE trajectory file handling: "%s" → trajectory:%s, binary:%s`,
347
+ async (filename, is_trajectory, is_binary) => {
348
+ expect(is_trajectory_file(filename)).toBe(is_trajectory)
349
+ if (is_trajectory) {
350
+ const result = await read_file(`/test/${filename}`)
351
+ expect(result.is_base64).toBe(is_binary)
352
+ }
353
+ },
354
+ )
355
+
356
+ // Integration test for ASE trajectory file processing (simulates the exact failing scenario)
357
+ test(`ASE trajectory file end-to-end processing`, async () => {
358
+ const ase_filename = `ase-LiMnO2-chgnet-relax.traj`
359
+
360
+ // Step 1: Extension should detect this as a trajectory file
361
+ expect(is_trajectory_file(ase_filename)).toBe(true)
362
+
363
+ // Step 2: Extension should read this as binary (compressed)
364
+ const file_result = await read_file(`/test/${ase_filename}`)
365
+ expect(file_result.filename).toBe(ase_filename)
366
+ expect(file_result.is_base64).toBe(true)
367
+ expect(file_result.content).toBe(Buffer.from(`mock content`).toString(`base64`)) // base64 encoded binary data
368
+
369
+ // Step 3: Verify webview data structure matches expected format
370
+ const webview_data = {
371
+ type: `trajectory` as const,
372
+ data: file_result,
373
+ }
374
+
375
+ // Step 4: HTML generation should work with this data
376
+ const webview_data_with_theme = { ...webview_data, theme: `light` as const }
377
+ const html = create_html(mock_webview, mock_context, webview_data_with_theme)
378
+
379
+ expect(html).toContain(`<!DOCTYPE html>`)
380
+ expect(html).toContain(JSON.stringify(webview_data_with_theme))
381
+
382
+ // Step 5: Verify the exact data structure that would be sent to webview
383
+ const parsed_data = JSON.parse(/matterviz_data=(\{[\s\S]*?\});/.exec(html)?.[1] ?? `{}`)
384
+ expect(parsed_data.type).toBe(`trajectory`)
385
+ expect(parsed_data.data.filename).toBe(ase_filename)
386
+ expect(parsed_data.data.is_base64).toBe(true)
387
+ expect(parsed_data.data.content).toBe(Buffer.from(`mock content`).toString(`base64`))
388
+ expect(parsed_data.theme).toBe(`light`)
389
+ })
390
+
391
+ test.each([
392
+ [{ fsPath: `/test/file.cif` } as unknown as Uri, `file.cif`],
393
+ [{ fsPath: `/test/structure.xyz` } as unknown as Uri, `structure.xyz`],
394
+ ])(`get_file with URI`, async (uri, expected_filename) => {
395
+ const result = await get_file(uri)
396
+ expect(result.filename).toBe(expected_filename)
397
+ })
398
+
399
+ test(`get_file with active editor`, async () => {
400
+ mock_vscode.window.activeTextEditor = {
401
+ document: { fileName: `/test/active.cif`, getText: () => `active content` },
402
+ } as TextEditor
403
+ const result = await get_file()
404
+ expect(result.filename).toBe(`active.cif`)
405
+ expect(result.content).toBe(`active content`)
406
+ expect(result.is_base64).toBe(false)
407
+ })
408
+
409
+ test(`get_file with active tab`, async () => {
410
+ mock_vscode.window.tabGroups.activeTabGroup.activeTab = {
411
+ input: { uri: { fsPath: `/test/tab.cif` } },
412
+ } as unknown as Tab
413
+ const result = await get_file()
414
+ expect(result.filename).toBe(`tab.cif`)
415
+ })
416
+
417
+ test(`get_file throws when no file found`, async () => {
418
+ mock_vscode.window.tabGroups.activeTabGroup.activeTab = null
419
+ await expect(get_file()).rejects.toThrow(
420
+ `No file selected. MatterViz needs an active editor to know what to render.`,
421
+ )
422
+ })
423
+
424
+ test.each([
425
+ [`structure`, { filename: `test.cif`, content: `content`, is_base64: false }],
426
+ [`trajectory`, { filename: `test.traj`, content: `YmluYXJ5`, is_base64: true }],
427
+ [
428
+ `structure`,
429
+ {
430
+ filename: `test"quotes.cif`,
431
+ content: `content`,
432
+ is_base64: false,
433
+ },
434
+ ],
435
+ [`structure`, { filename: `test.cif`, content: ``, is_base64: false }],
436
+ [
437
+ `structure`,
438
+ {
439
+ filename: `test.cif`,
440
+ content: `<script>alert("xss")</script>`,
441
+ is_base64: false,
442
+ },
443
+ ],
444
+ [
445
+ `structure`,
446
+ {
447
+ filename: `large.cif`,
448
+ content: `x`.repeat(100_000),
449
+ is_base64: false,
450
+ },
451
+ ],
452
+ ] as const)(`HTML generation: %s files`, (type, data) => {
453
+ const webview_data = { type, data, theme: `light` } as const
454
+ const html = create_html(mock_webview, mock_context, webview_data)
455
+ expect(html).toContain(`<!DOCTYPE html>`)
456
+ expect(html).toContain(`Content-Security-Policy`)
457
+ expect(html).toContain(`default-src 'none'`)
458
+ expect(html).toContain(`script-src 'nonce-`)
459
+ expect(html).toMatch(/nonce="[a-zA-Z0-9]{8,32}"/)
460
+ expect(html).toContain(JSON.stringify(webview_data).replace(/<\//g, `<\\/`))
461
+ expect(html).toContain(`matterviz-app`)
462
+ })
463
+
464
+ test.each([
465
+ [{ command: `info`, text: `Test message` }, `showInformationMessage`],
466
+ [{ command: `error`, text: `Error message` }, `showErrorMessage`],
467
+ [{ command: `info`, text: `"><script>alert(1)</script>` }, `showInformationMessage`],
468
+ [{ command: `error`, text: `javascript:alert(1)` }, `showErrorMessage`],
469
+ ] as const)(`message handling: %s`, async (message, expected_method) => {
470
+ await handle_msg(message)
471
+ expect(
472
+ mock_vscode.window[expected_method as keyof typeof mock_vscode.window],
473
+ ).toHaveBeenCalledWith(message.text)
474
+ })
475
+
476
+ test.each([
477
+ [{ command: `saveAs`, content: `content`, filename: `test.cif` }, true, `Saved: save.cif`],
478
+ [
479
+ {
480
+ command: `saveAs`,
481
+ content: `<script>alert("XSS")</script>`,
482
+ filename: `test.cif`,
483
+ },
484
+ true,
485
+ `Saved: save.cif`,
486
+ ],
487
+ ] as const)(`saveAs success: %s`, async (message, should_succeed, expected_info) => {
488
+ mock_vscode.window.showSaveDialog.mockResolvedValue({ fsPath: `/test/save.cif` })
489
+ await handle_msg(message)
490
+ if (should_succeed) {
491
+ const enc = new TextEncoder()
492
+ expect(mock_vscode.workspace.fs.writeFile).toHaveBeenCalledWith(
493
+ { fsPath: `/test/save.cif` },
494
+ enc.encode(message.content),
495
+ )
496
+ expect(mock_vscode.window.showInformationMessage).toHaveBeenCalledWith(expected_info)
497
+ }
498
+ })
499
+
500
+ test.each([
501
+ [
502
+ `PNG image`,
503
+ `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==`,
504
+ `structure.png`,
505
+ true,
506
+ ],
507
+ [
508
+ `JPEG image`,
509
+ `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AP/Z`,
510
+ `plot.jpg`,
511
+ true,
512
+ ],
513
+ [
514
+ `PDF document`,
515
+ `data:application/pdf;base64,JVBERi0xLjQKJcOkw7zDtsO8DQoxIDAgb2JqDQo8PA0KL1R5cGUgL0NhdGFsb2cNCi9QYWdlcyAyIDAgUg0KPj4NCmVuZG9iag0KMiAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0tpZHMgWzMgMCBSXQ0KL0NvdW50IDENCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdDQo+Pg0KZW5kb2JqDQozIDAgb2JqDQo8PA0KL1R5cGUgL1BhZ2UNCi9QYXJlbnQgMiAwIFINCi9SZXNvdXJjZXMgPDwNCi9Gb250IDw8DQovRjEgNCAwIFINCj4+DQo+Pg0KL0NvbnRlbnRzIDUgMCBSDQo+Pg0KZW5kb2JqDQo0IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL0Jhc2VGb250IC9IZWx2ZXRpY2ENCi9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nDQo+Pg0KZW5kb2JqDQo1IDAgb2JqDQo8PA0KL0xlbmd0aCA0NA0KPj4NCnN0cmVhbQ0KQlQNCjEyIDAgVGQKL0YxIDEyIFRqDQooSGVsbG8gV29ybGQpIFRqDQpFVA0KZW5kc3RyZWFtDQplbmRvYmoNCnhyZWYNCjAgNg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDEwIDAwMDAwIG4NCjAwMDAwMDAwNzkgMDAwMDAgbg0KMDAwMDAwMDE3MyAwMDAwMCBuDQowMDAwMDAwMzAxIDAwMDAwIG4NCjAwMDAwMDAzODAgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9TaXplIDYNCi9Sb290IDEgMCBSDQo+Pg0Kc3RhcnR4cmVmDQo0OTINCiUlRU9G`,
516
+ `report.pdf`,
517
+ true,
518
+ ],
519
+ ])(`saveAs binary data: %s`, async (_description, data_url, filename, is_binary) => {
520
+ mock_vscode.window.showSaveDialog.mockResolvedValue({ fsPath: `/test/${filename}` })
521
+ await handle_msg({
522
+ command: `saveAs`,
523
+ content: data_url,
524
+ ...msg_args,
525
+ filename,
526
+ is_binary,
527
+ })
528
+ const base64_data = data_url.replace(/^data:[^;]+;base64,/, ``)
529
+ const expected_buffer = Uint8Array.from(Buffer.from(base64_data, `base64`))
530
+ expect(mock_vscode.workspace.fs.writeFile).toHaveBeenCalledWith(
531
+ { fsPath: `/test/${filename}` },
532
+ expected_buffer,
533
+ )
534
+ expect(mock_vscode.window.showInformationMessage).toHaveBeenCalledWith(
535
+ `Saved: ${filename}`,
536
+ )
537
+ })
538
+
539
+ test(`saveAs error handling`, async () => {
540
+ mock_vscode.window.showSaveDialog.mockResolvedValue({
541
+ fsPath: `/test/save.cif`,
542
+ })
543
+ mock_vscode.workspace.fs.writeFile.mockRejectedValue(new Error(`Write failed`))
544
+
545
+ await handle_msg({
546
+ command: `saveAs`,
547
+ content: `content`,
548
+ ...msg_args,
549
+ filename: `test.cif`,
550
+ })
551
+ expect(mock_vscode.window.showErrorMessage).toHaveBeenCalledWith(
552
+ `Failed to save text file: Write failed`,
553
+ )
554
+ })
555
+
556
+ test(`saveAs user cancellation`, async () => {
557
+ mock_vscode.window.showSaveDialog.mockResolvedValue(undefined)
558
+
559
+ await handle_msg({
560
+ command: `saveAs`,
561
+ content: `content`,
562
+ ...msg_args,
563
+ filename: `test.cif`,
564
+ })
565
+ expect(mock_vscode.workspace.fs.writeFile).not.toHaveBeenCalled()
566
+ })
567
+
568
+ test(`saveAs binary data validation: empty base64 data`, async () => {
569
+ mock_vscode.window.showSaveDialog.mockResolvedValue({ fsPath: `/test/test.png` })
570
+
571
+ await handle_msg({
572
+ command: `saveAs`,
573
+ content: `data:image/png;base64,`,
574
+ ...msg_args,
575
+ filename: `test.png`,
576
+ is_binary: true,
577
+ })
578
+
579
+ expect(mock_vscode.window.showErrorMessage).toHaveBeenCalledWith(
580
+ `Failed to save binary data: Invalid data URL: missing base64 data`,
581
+ )
582
+ expect(mock_vscode.workspace.fs.writeFile).not.toHaveBeenCalled()
583
+ })
584
+
585
+ test.each([[{ command: `info` }], [{ command: `saveAs` }], [{ command: `unknown` }]])(
586
+ `malformed message handling: %s`,
587
+ async (msg) => {
588
+ await expect(
589
+ handle_msg({ ...msg, ...msg_args } as unknown as MessageData),
590
+ ).resolves.toBeUndefined()
591
+ },
592
+ )
593
+
594
+ test(`render creates webview panel`, async () => {
595
+ const mock_panel = {
596
+ webview: { ...mock_webview },
597
+ onDidDispose: vi.fn(),
598
+ }
599
+ mock_vscode.window.createWebviewPanel.mockReturnValue(mock_panel)
600
+ mock_vscode.window.activeTextEditor = {
601
+ document: { fileName: `/test/active.cif`, getText: () => `content` },
602
+ } as TextEditor
603
+
604
+ await render(mock_context)
605
+ expect(mock_vscode.window.createWebviewPanel).toHaveBeenCalledWith(
606
+ `matterviz`,
607
+ `MatterViz - active.cif`,
608
+ mock_vscode.ViewColumn.Active,
609
+ expect.any(Object),
610
+ )
611
+ })
612
+
613
+ test(`render handles errors`, async () => {
614
+ mock_vscode.window.activeTextEditor = null
615
+ mock_vscode.window.tabGroups.activeTabGroup.activeTab = null
616
+ await render(mock_context)
617
+ expect(mock_vscode.window.showErrorMessage).toHaveBeenCalledWith(
618
+ `Failed: No file selected. MatterViz needs an active editor to know what to render.`,
619
+ )
620
+ })
621
+
622
+ test(`extension activation`, () => {
623
+ activate(mock_context)
624
+ expect(mock_vscode.commands.registerCommand).toHaveBeenCalledWith(
625
+ `matterviz.open`,
626
+ expect.any(Function),
627
+ )
628
+ expect(mock_vscode.commands.registerCommand).toHaveBeenCalledWith(
629
+ `matterviz.report_bug`,
630
+ expect.any(Function),
631
+ )
632
+ expect(mock_vscode.window.registerCustomEditorProvider).toHaveBeenCalledWith(
633
+ `matterviz.viewer`,
634
+ expect.any(Object),
635
+ expect.any(Object),
636
+ )
637
+ })
638
+
639
+ describe(`Bug Reporting`, () => {
640
+ let mock_opened_document: { content: string; language: string } | null = null
641
+ let report_bug_command: (() => Promise<void>) | null = null
642
+ let mock_env: {
643
+ appName: string
644
+ remoteName: string | undefined
645
+ uiKind: number
646
+ clipboard: {
647
+ writeText: ReturnType<typeof vi.fn>
648
+ readText: ReturnType<typeof vi.fn>
649
+ }
650
+ openExternal: ReturnType<typeof vi.fn>
651
+ }
652
+
653
+ beforeEach(async () => {
654
+ // Reset state
655
+ mock_opened_document = null
656
+ report_bug_command = null
657
+
658
+ // Mock clipboard API (must be set up before activation)
659
+ mock_env = {
660
+ appName: `Cursor`,
661
+ remoteName: undefined,
662
+ uiKind: 1, // Desktop
663
+ clipboard: {
664
+ writeText: vi.fn(() => Promise.resolve()),
665
+ readText: vi.fn(() => Promise.resolve(``)),
666
+ },
667
+ openExternal: vi.fn(() => Promise.resolve(true)),
668
+ }
669
+ mock_vscode.env = mock_env as unknown as typeof mock_vscode.env
670
+
671
+ // Capture the report_bug command during activation
672
+ const command_registry = new Map<string, () => Promise<void>>()
673
+ mock_vscode.commands.registerCommand = vi.fn(
674
+ (command_name: string, callback: () => Promise<void>) => {
675
+ command_registry.set(command_name, callback)
676
+ return { dispose: vi.fn() }
677
+ },
678
+ )
679
+
680
+ // Mock workspace.openTextDocument to capture the document content (BEFORE activation)
681
+ mock_vscode.workspace.openTextDocument = vi.fn(
682
+ (options: { content: string; language: string }) => {
683
+ mock_opened_document = {
684
+ content: options.content,
685
+ language: options.language,
686
+ }
687
+ return Promise.resolve({
688
+ uri: { fsPath: `/tmp/bug-report.md` },
689
+ getText: () => options.content,
690
+ })
691
+ },
692
+ )
693
+
694
+ // Mock window.showTextDocument (BEFORE activation)
695
+ mock_vscode.window.showTextDocument = vi.fn(() => Promise.resolve())
696
+
697
+ // Mock showInformationMessage to return action choices (BEFORE activation)
698
+ mock_vscode.window.showInformationMessage = vi.fn(() => Promise.resolve(undefined))
699
+
700
+ // Activate extension to register commands
701
+ activate(mock_context)
702
+
703
+ // Get the report_bug command
704
+ report_bug_command = command_registry.get(`matterviz.report_bug`) ?? null
705
+ })
706
+
707
+ test(`should generate bug report with environment information`, async () => {
708
+ expect(report_bug_command).not.toBeNull()
709
+ if (!report_bug_command) return
710
+
711
+ await report_bug_command()
712
+
713
+ expect(mock_opened_document).not.toBeNull()
714
+ expect(mock_opened_document?.language).toBe(`markdown`)
715
+
716
+ const content = mock_opened_document?.content ?? ``
717
+
718
+ // Check for main sections
719
+ expect(content).toContain(`### Environment`)
720
+ expect(content).toContain(`### System Resources`)
721
+ expect(content).toContain(`### Active Files & Extension State`)
722
+ expect(content).toContain(`### Console Logs`)
723
+
724
+ // Check environment details
725
+ expect(content).toContain(`- **Editor**: Cursor`)
726
+ expect(content).toContain(`- **MatterViz Version**: ${pkg_json.version}`)
727
+ expect(content).toContain(`- **UI Kind**: Desktop`)
728
+ expect(content).toContain(`- **Remote Session**: No (Local)`)
729
+
730
+ // Check system resources
731
+ expect(content).toContain(`- **Total Memory**:`)
732
+ expect(content).toContain(`- **Free Memory**:`)
733
+ expect(content).toContain(`- **Process RSS**:`)
734
+ expect(content).toContain(`- **Process Heap Used**:`)
735
+ expect(content).toContain(`- **Process Heap Total**:`)
736
+
737
+ // Check console logs instructions
738
+ expect(content).toContain(`**Please check for console errors/warnings:**`)
739
+ expect(content).toContain(`Toggle Developer Tools`)
740
+
741
+ // Check GitHub link
742
+ expect(content).toContain(`https://github.com/janosh/matterviz/issues`)
743
+
744
+ // Check timestamp
745
+ expect(content).toMatch(/\*\*Generated\*\*: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
746
+ })
747
+
748
+ test(`should detect remote session correctly`, async () => {
749
+ // Mock remote session
750
+ mock_env.remoteName = `ssh-remote`
751
+
752
+ expect(report_bug_command).not.toBeNull()
753
+ if (!report_bug_command) return
754
+
755
+ await report_bug_command()
756
+
757
+ const content = mock_opened_document?.content ?? ``
758
+ expect(content).toContain(`- **Remote Session**: Yes (ssh-remote)`)
759
+ })
760
+
761
+ test(`should include active files in report`, async () => {
762
+ // Start watching some files first
763
+ const file1_path = `/test/structure.cif`
764
+ const file2_path = `/test/trajectory.traj`
765
+
766
+ await handle_msg(
767
+ {
768
+ command: `startWatching`,
769
+ file_path: file1_path,
770
+ filename: `structure.cif`,
771
+ request_id: `req1`,
772
+ frame_index: 0,
773
+ } as MessageData,
774
+ mock_webview,
775
+ )
776
+
777
+ await handle_msg(
778
+ {
779
+ command: `startWatching`,
780
+ file_path: file2_path,
781
+ filename: `trajectory.traj`,
782
+ request_id: `req2`,
783
+ frame_index: 0,
784
+ } as MessageData,
785
+ mock_webview,
786
+ )
787
+
788
+ expect(report_bug_command).not.toBeNull()
789
+ if (!report_bug_command) return
790
+
791
+ await report_bug_command()
792
+
793
+ const content = mock_opened_document?.content ?? ``
794
+
795
+ // Should list both files with bold filenames (not headers)
796
+ expect(content).toContain(`**structure.cif**`)
797
+ expect(content).toContain(`**trajectory.traj**`)
798
+ expect(content).toContain(`- **Path**: \`${file1_path}\``)
799
+ expect(content).toContain(`- **Path**: \`${file2_path}\``)
800
+ expect(content).toContain(`- **Has Watcher**: true`)
801
+
802
+ // Check for combined section and extension state counters
803
+ expect(content).toContain(`### Active Files & Extension State`)
804
+ expect(content).toContain(`- **Active Watchers**: 2`)
805
+ })
806
+
807
+ test(`should handle files with no active watchers`, async () => {
808
+ expect(report_bug_command).not.toBeNull()
809
+ if (!report_bug_command) return
810
+
811
+ await report_bug_command()
812
+
813
+ const content = mock_opened_document?.content ?? ``
814
+ expect(content).toContain(`No files currently being watched/rendered.`)
815
+ })
816
+
817
+ test(`should copy to clipboard when user selects that option`, async () => {
818
+ mock_vscode.window.showInformationMessage = vi.fn(() =>
819
+ Promise.resolve(`Copy to Clipboard`),
820
+ )
821
+
822
+ expect(report_bug_command).not.toBeNull()
823
+ if (!report_bug_command) return
824
+
825
+ await report_bug_command()
826
+
827
+ const content = mock_opened_document?.content ?? ``
828
+
829
+ // Should have called clipboard.writeText with the content
830
+ expect(mock_env.clipboard.writeText).toHaveBeenCalledWith(content)
831
+
832
+ // Should show success message
833
+ expect(mock_vscode.window.showInformationMessage).toHaveBeenCalledWith(
834
+ `Debug information copied to clipboard!`,
835
+ )
836
+ })
837
+
838
+ test(`should open GitHub issues when user selects that option`, async () => {
839
+ mock_vscode.window.showInformationMessage = vi.fn(() =>
840
+ Promise.resolve(`Open GitHub Issues`),
841
+ )
842
+
843
+ expect(report_bug_command).not.toBeNull()
844
+ if (!report_bug_command) return
845
+
846
+ await report_bug_command()
847
+
848
+ // Should have opened GitHub issues URL
849
+ expect(mock_env.openExternal).toHaveBeenCalledWith(
850
+ expect.objectContaining({
851
+ toString: expect.any(Function),
852
+ }),
853
+ )
854
+
855
+ // Verify the URL contains the GitHub issues path
856
+ const call_args = mock_env.openExternal.mock.calls[0]
857
+ expect(call_args[0].toString()).toContain(
858
+ `https://github.com/janosh/matterviz/issues/new`,
859
+ )
860
+ })
861
+
862
+ test(`should format file sizes correctly`, async () => {
863
+ // Mock different file sizes
864
+ const test_cases = [
865
+ { size: 500, expected: `500 B` },
866
+ { size: 1024, expected: `1.00 KiB` },
867
+ { size: 1024 * 1024, expected: `1.00 MiB` },
868
+ { size: 1024 * 1024 * 1024, expected: `1.00 GiB` },
869
+ ]
870
+
871
+ // Create a map to track sizes for each file
872
+ const file_sizes = new Map<string, number>()
873
+
874
+ // Set up persistent mock that uses the file_sizes map
875
+ mock_vscode.workspace.fs.stat.mockImplementation((uri) => {
876
+ const size = file_sizes.get(uri.fsPath) ?? 1000
877
+ return Promise.resolve({ size, type: 1 })
878
+ })
879
+
880
+ // Add files to watchers with their sizes
881
+ const watcher_promises = test_cases.map((test_case) => {
882
+ const file_path = `/test/file_${test_case.size}.cif`
883
+ file_sizes.set(file_path, test_case.size)
884
+
885
+ return handle_msg(
886
+ {
887
+ command: `startWatching`,
888
+ file_path,
889
+ filename: `file_${test_case.size}.cif`,
890
+ request_id: `req_${test_case.size}`,
891
+ frame_index: 0,
892
+ } as MessageData,
893
+ mock_webview,
894
+ )
895
+ })
896
+
897
+ await Promise.all(watcher_promises)
898
+
899
+ expect(report_bug_command).not.toBeNull()
900
+ if (!report_bug_command) return
901
+
902
+ await report_bug_command()
903
+
904
+ const content = mock_opened_document?.content ?? ``
905
+
906
+ // Check that sizes are formatted correctly
907
+ for (const test_case of test_cases) {
908
+ if (test_case.size >= 1024) {
909
+ // Only check KB and above (bytes might be rounded)
910
+ expect(content).toContain(test_case.expected)
911
+ }
912
+ }
913
+ })
914
+
915
+ test(`should handle file stat errors gracefully`, async () => {
916
+ // Mock file that exists but throws error on stat
917
+ const file_path = `/test/error-file.cif`
918
+
919
+ await handle_msg(
920
+ {
921
+ command: `startWatching`,
922
+ file_path,
923
+ filename: `error-file.cif`,
924
+ request_id: `req_error`,
925
+ frame_index: 0,
926
+ } as MessageData,
927
+ mock_webview,
928
+ )
929
+
930
+ // Mock stat to throw error for this specific file
931
+ mock_vscode.workspace.fs.stat.mockRejectedValue(new Error(`File not found`))
932
+
933
+ expect(report_bug_command).not.toBeNull()
934
+ if (!report_bug_command) return
935
+
936
+ await report_bug_command()
937
+
938
+ const content = mock_opened_document?.content ?? ``
939
+
940
+ // Should still include the file but with "Unknown" size
941
+ expect(content).toContain(`**error-file.cif**`)
942
+ expect(content).toContain(`- **Size**: Unknown`)
943
+ })
944
+
945
+ test(`should include extension state counters`, async () => {
946
+ // Start watching multiple files
947
+ await handle_msg(
948
+ {
949
+ command: `startWatching`,
950
+ file_path: `/test/file1.cif`,
951
+ filename: `file1.cif`,
952
+ request_id: `req1`,
953
+ frame_index: 0,
954
+ } as MessageData,
955
+ mock_webview,
956
+ )
957
+
958
+ await handle_msg(
959
+ {
960
+ command: `startWatching`,
961
+ file_path: `/test/file2.cif`,
962
+ filename: `file2.cif`,
963
+ request_id: `req2`,
964
+ frame_index: 0,
965
+ } as MessageData,
966
+ mock_webview,
967
+ )
968
+
969
+ expect(report_bug_command).not.toBeNull()
970
+ if (!report_bug_command) return
971
+
972
+ await report_bug_command()
973
+
974
+ const content = mock_opened_document?.content ?? ``
975
+
976
+ // Check combined Active Files & Extension State section
977
+ expect(content).toContain(`### Active Files & Extension State`)
978
+ expect(content).toContain(`- **Active Watchers**: 2`)
979
+ expect(content).toMatch(/- \*\*Active Frame Loaders\*\*: \d+/)
980
+ expect(content).toMatch(/- \*\*Auto-Render Timers\*\*: \d+/)
981
+ expect(content).toMatch(/- \*\*Active Auto-Render Panels\*\*: \d+/)
982
+ })
983
+
984
+ test(`should handle errors during report generation`, async () => {
985
+ // Mock openTextDocument to throw an error
986
+ mock_vscode.workspace.openTextDocument = vi.fn(() =>
987
+ Promise.reject(new Error(`Failed to create document`)),
988
+ )
989
+
990
+ expect(report_bug_command).not.toBeNull()
991
+ if (!report_bug_command) return
992
+
993
+ await report_bug_command()
994
+
995
+ // Should show error message
996
+ expect(mock_vscode.window.showErrorMessage).toHaveBeenCalledWith(
997
+ expect.stringContaining(`Failed to collect debug information`),
998
+ )
999
+ })
1000
+
1001
+ test(`should include console logs instructions in bug report`, async () => {
1002
+ expect(report_bug_command).not.toBeNull()
1003
+ if (!report_bug_command) return
1004
+
1005
+ await report_bug_command()
1006
+
1007
+ const content = mock_opened_document?.content ?? ``
1008
+
1009
+ // Check for console logs section with instructions
1010
+ expect(content).toContain(`### Console Logs`)
1011
+ expect(content).toContain(`**Please check for console errors/warnings:**`)
1012
+ expect(content).toContain(`Toggle Developer Tools`)
1013
+ expect(content).toContain(`Tip: You can filter console messages`)
1014
+ })
1015
+ })
1016
+
1017
+ test(`performance benchmarks`, () => {
1018
+ // Trajectory detection performance
1019
+ const filenames = Array.from({ length: 10000 }, (_, idx) => `test_${idx}.xyz`)
1020
+ const start = performance.now()
1021
+ filenames.forEach((name) => is_trajectory_file(name))
1022
+ expect(performance.now() - start).toBeLessThan(100)
1023
+
1024
+ // HTML generation performance
1025
+ const large_data = {
1026
+ type: `structure`,
1027
+ data: { filename: `large.cif`, content: `x`.repeat(100_000), is_base64: false },
1028
+ theme: `light`,
1029
+ } as const
1030
+ const html_start = performance.now()
1031
+ create_html(mock_webview, mock_context, large_data)
1032
+ expect(performance.now() - html_start).toBeLessThan(50)
1033
+ })
1034
+
1035
+ test(`nonce uniqueness`, () => {
1036
+ const data = {
1037
+ type: `structure`,
1038
+ data: { filename: `test.cif`, content: `content`, is_base64: false },
1039
+ theme: `light`,
1040
+ } as const
1041
+ const nonces = new Set<string>()
1042
+
1043
+ for (let idx = 0; idx < 1000; idx++) {
1044
+ const html = create_html(mock_webview, mock_context, data)
1045
+ const nonce_match = /nonce="([a-zA-Z0-9]+)"/.exec(html)
1046
+ if (nonce_match) nonces.add(nonce_match[1])
1047
+ }
1048
+
1049
+ expect(nonces.size).toBe(1000)
1050
+ })
1051
+
1052
+ test(`XSS prevention`, () => {
1053
+ const dangerous_payloads = [
1054
+ `<script>alert("XSS")</script>`,
1055
+ `<img src="x" onerror="alert(1)">`,
1056
+ `javascript:alert(1)`,
1057
+ `"><script>alert(1)</script>`,
1058
+ `';alert(1);//`,
1059
+ `</script><script>alert(document.cookie)</script>`,
1060
+ ]
1061
+
1062
+ dangerous_payloads.forEach((payload) => {
1063
+ const data = {
1064
+ type: `structure`,
1065
+ data: { filename: `test.cif`, content: payload, is_base64: false },
1066
+ theme: `light`,
1067
+ } as const
1068
+ const html = create_html(mock_webview, mock_context, data)
1069
+ const escaped_json = JSON.stringify(data).replace(/<\//g, `<\\/`)
1070
+
1071
+ expect(html).toContain(escaped_json)
1072
+ // Ensure no raw </script> inside the JSON data breaks out of the script tag
1073
+ const data_script = /window\.matterviz_data=(.*?);/s.exec(html)
1074
+ if (data_script) {
1075
+ expect(data_script[1]).not.toContain(`</script>`)
1076
+ }
1077
+ })
1078
+ })
1079
+
1080
+ test(`concurrent operations`, async () => {
1081
+ const promises = Array.from({ length: 50 }, (_, idx) =>
1082
+ handle_msg({ command: `info`, text: `Message ${idx}`, ...msg_args }),
1083
+ )
1084
+ await Promise.all(promises)
1085
+ expect(mock_vscode.window.showInformationMessage).toHaveBeenCalledTimes(50)
1086
+ })
1087
+
1088
+ describe(`Theme functionality`, () => {
1089
+ test.each([
1090
+ [mock_vscode.ColorThemeKind.Light, `auto`, `light`], // Light VSCode theme, auto setting → light
1091
+ [mock_vscode.ColorThemeKind.Dark, `auto`, `dark`], // Dark VSCode theme, auto setting → dark
1092
+ [mock_vscode.ColorThemeKind.HighContrast, `auto`, `black`], // High contrast VSCode theme, auto setting → black
1093
+ [mock_vscode.ColorThemeKind.HighContrastLight, `auto`, `white`], // High contrast light VSCode theme, auto setting → white
1094
+ [mock_vscode.ColorThemeKind.Light, `light`, `light`], // Light VSCode theme, light setting → light
1095
+ [mock_vscode.ColorThemeKind.Light, `dark`, `dark`], // Light VSCode theme, dark setting → dark
1096
+ [mock_vscode.ColorThemeKind.Light, `white`, `white`], // Light VSCode theme, white setting → white
1097
+ [mock_vscode.ColorThemeKind.Light, `black`, `black`], // Light VSCode theme, black setting → black
1098
+ [mock_vscode.ColorThemeKind.Dark, `light`, `light`], // Dark VSCode theme, light setting → light
1099
+ [mock_vscode.ColorThemeKind.Dark, `dark`, `dark`], // Dark VSCode theme, dark setting → dark
1100
+ [mock_vscode.ColorThemeKind.Dark, `white`, `white`], // Dark VSCode theme, white setting → white
1101
+ [mock_vscode.ColorThemeKind.Dark, `black`, `black`], // Dark VSCode theme, black setting → black
1102
+ ] as const)(
1103
+ `theme detection: VSCode theme %i, setting '%s' → '%s'`,
1104
+ (vscode_theme_kind: number, setting: string, expected: ThemeName) => {
1105
+ const mock_config = {
1106
+ get: vi.fn((key: string, default_value?: string) =>
1107
+ key === `theme` ? setting : default_value,
1108
+ ),
1109
+ }
1110
+ mock_vscode.workspace.getConfiguration = vi.fn(() => mock_config) as ReturnType<
1111
+ typeof vi.fn
1112
+ >
1113
+ mock_vscode.window.activeColorTheme = { kind: vscode_theme_kind }
1114
+
1115
+ const result = get_theme()
1116
+ expect(result).toBe(expected)
1117
+ },
1118
+ )
1119
+
1120
+ test(`webview data includes theme`, () => {
1121
+ const mock_config = {
1122
+ get: vi.fn((key: string, default_value?: string) =>
1123
+ key === `theme` ? `dark` : default_value,
1124
+ ),
1125
+ }
1126
+ mock_vscode.workspace.getConfiguration = vi.fn(() => mock_config) as ReturnType<
1127
+ typeof vi.fn
1128
+ >
1129
+
1130
+ const data = {
1131
+ type: `structure` as const,
1132
+ data: { filename: `test.cif`, content: `content`, is_base64: false },
1133
+ theme: get_theme(),
1134
+ }
1135
+
1136
+ const html = create_html(mock_webview, mock_context, data)
1137
+
1138
+ const parsed_data = JSON.parse(/matterviz_data=(\{[\s\S]*?\});/.exec(html)?.[1] || `{}`)
1139
+ expect(parsed_data.theme).toBe(`dark`)
1140
+ })
1141
+
1142
+ test(`invalid theme setting falls back to auto`, () => {
1143
+ const mock_config = {
1144
+ get: vi.fn((key: string, default_value?: string) =>
1145
+ key === `theme` ? `invalid-theme` : default_value,
1146
+ ),
1147
+ }
1148
+ mock_vscode.workspace.getConfiguration = vi.fn(() => mock_config) as ReturnType<
1149
+ typeof vi.fn
1150
+ >
1151
+ mock_vscode.window.activeColorTheme = {
1152
+ kind: mock_vscode.ColorThemeKind.Light,
1153
+ }
1154
+
1155
+ const result = get_theme()
1156
+ expect(result).toBe(`light`) // Should fall back to system theme
1157
+ })
1158
+
1159
+ test(`high contrast themes are mapped correctly`, () => {
1160
+ const mock_config = {
1161
+ get: vi.fn((key: string, default_value?: string) =>
1162
+ key === `theme` ? `auto` : default_value,
1163
+ ),
1164
+ }
1165
+ mock_vscode.workspace.getConfiguration = vi.fn(() => mock_config) as ReturnType<
1166
+ typeof vi.fn
1167
+ >
1168
+
1169
+ // Test high contrast dark → black
1170
+ mock_vscode.window.activeColorTheme = {
1171
+ kind: mock_vscode.ColorThemeKind.HighContrast,
1172
+ }
1173
+ expect(get_theme()).toBe(`black`)
1174
+
1175
+ // Test high contrast light → white
1176
+ mock_vscode.window.activeColorTheme = {
1177
+ kind: mock_vscode.ColorThemeKind.HighContrastLight,
1178
+ }
1179
+ expect(get_theme()).toBe(`white`)
1180
+ })
1181
+ })
1182
+
1183
+ describe(`Theme listener cleanup`, () => {
1184
+ const setup_panel = (options = {}) => {
1185
+ const mock_dispose = vi.fn()
1186
+ const mock_panel = {
1187
+ webview: { ...mock_webview },
1188
+ onDidDispose: vi.fn(),
1189
+ visible: true,
1190
+ ...options,
1191
+ }
1192
+
1193
+ mock_vscode.window.createWebviewPanel.mockReturnValue(mock_panel)
1194
+ mock_vscode.window.onDidChangeActiveColorTheme.mockReturnValue({
1195
+ dispose: mock_dispose,
1196
+ })
1197
+ mock_vscode.workspace.onDidChangeConfiguration.mockReturnValue({
1198
+ dispose: mock_dispose,
1199
+ })
1200
+ mock_vscode.window.activeTextEditor = {
1201
+ document: { fileName: `/test/active.cif`, getText: () => `content` },
1202
+ } as TextEditor
1203
+
1204
+ return { mock_dispose, mock_panel }
1205
+ }
1206
+
1207
+ test(`sets up and cleans up theme listeners`, async () => {
1208
+ const { mock_dispose, mock_panel } = setup_panel()
1209
+
1210
+ await render(mock_context)
1211
+
1212
+ expect(mock_vscode.window.onDidChangeActiveColorTheme).toHaveBeenCalled()
1213
+ expect(mock_panel.onDidDispose).toHaveBeenCalled()
1214
+
1215
+ // Test cleanup
1216
+ mock_panel.onDidDispose.mock.calls[0][0]()
1217
+ expect(mock_dispose).toHaveBeenCalledTimes(2)
1218
+ })
1219
+
1220
+ test(`respects panel visibility for theme updates`, async () => {
1221
+ const mock_panel = {
1222
+ webview: { ...mock_webview },
1223
+ onDidDispose: vi.fn(),
1224
+ visible: false,
1225
+ }
1226
+
1227
+ mock_vscode.window.createWebviewPanel.mockReturnValue(mock_panel)
1228
+ mock_vscode.window.onDidChangeActiveColorTheme.mockReturnValue({
1229
+ dispose: vi.fn(),
1230
+ })
1231
+ mock_vscode.workspace.onDidChangeConfiguration.mockReturnValue({
1232
+ dispose: vi.fn(),
1233
+ })
1234
+ mock_vscode.window.activeTextEditor = {
1235
+ document: { fileName: `/test/active.cif`, getText: () => `content` },
1236
+ } as TextEditor
1237
+
1238
+ await render(mock_context)
1239
+
1240
+ // Store initial HTML after render (render always sets HTML initially)
1241
+ const initial_html = mock_panel.webview.html
1242
+
1243
+ const theme_calls = mock_vscode.window.onDidChangeActiveColorTheme.mock
1244
+ .calls as unknown as unknown[][]
1245
+ const theme_callback = theme_calls[0]?.[0] as (() => Promise<void>) | undefined
1246
+ expect(theme_callback).toBeDefined()
1247
+
1248
+ // Should not update when invisible
1249
+ await theme_callback?.()
1250
+ expect(mock_panel.webview.html).toBe(initial_html)
1251
+
1252
+ // Should update when visible
1253
+ mock_panel.visible = true
1254
+ await theme_callback?.()
1255
+ expect(mock_panel.webview.html).not.toBe(initial_html)
1256
+ })
1257
+
1258
+ test(`multiple panels dispose independently`, async () => {
1259
+ const dispose1 = vi.fn()
1260
+ const dispose2 = vi.fn()
1261
+ const panel1 = { webview: { ...mock_webview }, onDidDispose: vi.fn() }
1262
+ const panel2 = { webview: { ...mock_webview }, onDidDispose: vi.fn() }
1263
+
1264
+ mock_vscode.window.createWebviewPanel
1265
+ .mockReturnValueOnce(panel1)
1266
+ .mockReturnValueOnce(panel2)
1267
+ mock_vscode.window.onDidChangeActiveColorTheme
1268
+ .mockReturnValueOnce({ dispose: dispose1 })
1269
+ .mockReturnValueOnce({
1270
+ dispose: dispose2,
1271
+ })
1272
+ mock_vscode.workspace.onDidChangeConfiguration
1273
+ .mockReturnValueOnce({ dispose: dispose1 })
1274
+ .mockReturnValueOnce({
1275
+ dispose: dispose2,
1276
+ })
1277
+
1278
+ mock_vscode.window.activeTextEditor = {
1279
+ document: { fileName: `/test/active.cif`, getText: () => `content` },
1280
+ } as TextEditor
1281
+
1282
+ await render(mock_context)
1283
+ await render(mock_context)
1284
+
1285
+ panel1.onDidDispose.mock.calls[0][0]()
1286
+ expect(dispose1).toHaveBeenCalledTimes(2)
1287
+ expect(dispose2).not.toHaveBeenCalled()
1288
+ })
1289
+ })
1290
+
1291
+ describe(`File Watching`, () => {
1292
+ describe(`message handling`, () => {
1293
+ test(`should handle startWatching message`, async () => {
1294
+ const message = {
1295
+ command: `startWatching` as const,
1296
+ ...msg_args,
1297
+ file_path: `/test/file.cif`,
1298
+ }
1299
+ await handle_msg(message, mock_webview)
1300
+
1301
+ expect(mock_vscode.workspace.createFileSystemWatcher).toHaveBeenCalledWith(
1302
+ expect.objectContaining({
1303
+ base: expect.anything(),
1304
+ pattern: `file.cif`,
1305
+ }),
1306
+ )
1307
+ expect(mock_file_system_watcher.onDidChange).toHaveBeenCalledWith(expect.any(Function))
1308
+ expect(mock_file_system_watcher.onDidDelete).toHaveBeenCalledWith(expect.any(Function))
1309
+ })
1310
+
1311
+ test(`should handle stopWatching message`, async () => {
1312
+ // First start watching
1313
+ const start_message = {
1314
+ command: `startWatching` as const,
1315
+ ...msg_args,
1316
+ file_path: `/test/file.cif`,
1317
+ }
1318
+ await handle_msg(start_message, mock_webview)
1319
+
1320
+ // Then test stopping
1321
+ const stop_message = {
1322
+ command: `stopWatching` as const,
1323
+ ...msg_args,
1324
+ file_path: `/test/file.cif`,
1325
+ }
1326
+ await handle_msg(stop_message, mock_webview)
1327
+
1328
+ expect(mock_file_system_watcher.dispose).toHaveBeenCalled()
1329
+ })
1330
+
1331
+ test(`should handle startWatching without webview gracefully`, async () => {
1332
+ const message = {
1333
+ command: `startWatching` as const,
1334
+ ...msg_args,
1335
+ file_path: `/test/file.cif`,
1336
+ }
1337
+
1338
+ await expect(handle_msg(message)).resolves.not.toThrow()
1339
+ expect(mock_vscode.workspace.createFileSystemWatcher).not.toHaveBeenCalled()
1340
+ })
1341
+
1342
+ test(`should handle startWatching without file_path gracefully`, async () => {
1343
+ const message = {
1344
+ command: `startWatching` as const,
1345
+ ...msg_args,
1346
+ }
1347
+
1348
+ await expect(handle_msg(message, mock_webview)).resolves.not.toThrow()
1349
+ expect(mock_vscode.workspace.createFileSystemWatcher).not.toHaveBeenCalled()
1350
+ })
1351
+
1352
+ test(`should send error message when file watching fails`, async () => {
1353
+ mock_vscode.workspace.createFileSystemWatcher.mockImplementation(() => {
1354
+ throw new Error(`File system watcher creation failed`)
1355
+ })
1356
+
1357
+ const message = {
1358
+ command: `startWatching` as const,
1359
+ ...msg_args,
1360
+ file_path: `/test/large-file.cif`,
1361
+ }
1362
+
1363
+ await handle_msg(message, mock_webview)
1364
+
1365
+ expect(mock_webview.postMessage).toHaveBeenCalledWith({
1366
+ command: `error`,
1367
+ text: expect.stringContaining(`Failed to start watching file`),
1368
+ })
1369
+ })
1370
+ })
1371
+
1372
+ describe(`file change notifications`, () => {
1373
+ test(`should send file change notification to webview`, async () => {
1374
+ const message = {
1375
+ command: `startWatching` as const,
1376
+ ...msg_args,
1377
+ file_path: `/test/file.cif`,
1378
+ }
1379
+
1380
+ await handle_msg(message, mock_webview)
1381
+
1382
+ // Get the change handler
1383
+ const change_handler = mock_file_system_watcher.onDidChange.mock.calls[0][0]
1384
+
1385
+ // Trigger file change
1386
+ await change_handler()
1387
+
1388
+ // Wait for postMessage to be called (it's async)
1389
+ await vi.waitFor(() => {
1390
+ expect(mock_webview.postMessage).toHaveBeenCalledWith({
1391
+ command: `fileUpdated`,
1392
+ data: expect.objectContaining({
1393
+ filename: `file.cif`,
1394
+ content: `mock content`,
1395
+ is_base64: false,
1396
+ }),
1397
+ type: `structure`,
1398
+ ...msg_args,
1399
+ file_path: `/test/file.cif`,
1400
+ theme: `light`,
1401
+ })
1402
+ })
1403
+ })
1404
+
1405
+ test(`should handle file deletion notifications`, async () => {
1406
+ const message = {
1407
+ command: `startWatching` as const,
1408
+ ...msg_args,
1409
+ file_path: `/test/file.cif`,
1410
+ }
1411
+
1412
+ await handle_msg(message, mock_webview)
1413
+
1414
+ // Get the delete handler
1415
+ const delete_handler = mock_file_system_watcher.onDidDelete.mock.calls[0][0]
1416
+
1417
+ // Trigger file deletion
1418
+ delete_handler()
1419
+
1420
+ expect(mock_webview.postMessage).toHaveBeenCalledWith(
1421
+ expect.objectContaining({
1422
+ command: `fileDeleted`,
1423
+ file_path: `/test/file.cif`,
1424
+ }),
1425
+ )
1426
+ })
1427
+ })
1428
+
1429
+ describe(`lifecycle management`, () => {
1430
+ test(`should handle activation gracefully`, () => {
1431
+ const mock_context = {
1432
+ extensionUri: { fsPath: `/test/extension` },
1433
+ subscriptions: [],
1434
+ } as unknown as ExtensionContext
1435
+
1436
+ expect(() => activate(mock_context)).not.toThrow()
1437
+
1438
+ expect(mock_vscode.commands.registerCommand).toHaveBeenCalledWith(
1439
+ `matterviz.open`,
1440
+ expect.any(Function),
1441
+ )
1442
+ })
1443
+ })
1444
+ })
1445
+
1446
+ describe(`Auto-Render Functionality`, () => {
1447
+ test.each([
1448
+ // Supported structure files
1449
+ [`structure.cif`, true],
1450
+ [`molecule.xyz`, true],
1451
+ [`crystal.poscar`, true],
1452
+ [`data.json`, false], // "data" is too broad, will not auto-render without structure-specific keywords in filename
1453
+ [`structure.xml`, true],
1454
+ [`molecule.pdb`, true],
1455
+ [`compound.mol`, true],
1456
+ [`structure.mol2`, true],
1457
+ [`data.sdf`, true],
1458
+ [`crystal.mmcif`, true],
1459
+ // Supported trajectory files
1460
+ [`trajectory.traj`, true],
1461
+ [`simulation.h5`, true],
1462
+ [`data.hdf5`, false],
1463
+ [`traj.xtc`, true],
1464
+ // Compressed supported files
1465
+ [`trajectory.xyz.gz`, true],
1466
+ [`data.json.gz`, false], // "data" is too broad, will not auto-render without structure-specific keywords in filename
1467
+ [`structure.cif.gz`, true],
1468
+ // Special filenames
1469
+ [`POSCAR`, true],
1470
+ [`CONTCAR`, true],
1471
+ [`XDATCAR`, true],
1472
+ [`trajectory.dat`, true],
1473
+ [`md.xyz`, true],
1474
+ [`relax.out`, true],
1475
+ [`npt.log`, true],
1476
+ [`nvt.data`, true],
1477
+ [`nve.traj`, true],
1478
+ // Files with special characters
1479
+ [`structure (1).cif`, true],
1480
+ [`trajectory[test].xyz.gz`, true],
1481
+ [`crystal@test.poscar`, true],
1482
+ [`molecule#test.xyz`, true],
1483
+ [`structure$test.json`, true],
1484
+ [`trajectory%test.h5`, true],
1485
+ [`crystal^test.traj`, true],
1486
+ [`molecule&test.extxyz`, true],
1487
+ [`structure*test.xml`, true],
1488
+ [`trajectory+test.pdb`, true],
1489
+ [`crystal=test.mol`, true],
1490
+ [`molecule|test.mol2`, true],
1491
+ [`structure\`test.sdf`, true],
1492
+ [`trajectory~test.mmcif`, true],
1493
+ // Case sensitivity tests
1494
+ [`STRUCTURE.CIF`, true],
1495
+ [`structure.CIF`, true],
1496
+ [`Structure.cif`, true],
1497
+ [`TRAJECTORY.XYZ`, true],
1498
+ [`trajectory.XYZ`, true],
1499
+ [`Trajectory.xyz`, true],
1500
+ [`POSCAR`, true],
1501
+ [`poscar`, true],
1502
+ [`Poscar`, true],
1503
+ [`CONTCAR`, true],
1504
+ [`contcar`, true],
1505
+ [`Contcar`, true],
1506
+ [`XDATCAR`, true],
1507
+ [`xdatcar`, true],
1508
+ [`Xdatcar`, true],
1509
+ // Fermi surface files
1510
+ [`band.bxsf`, true],
1511
+ [`fermi.frmsf`, true],
1512
+ [`BAND.BXSF`, true],
1513
+ [`fermi.FRMSF`, true],
1514
+ [`band.bxsf.gz`, true],
1515
+ [`fermi.frmsf.gz`, true],
1516
+ // Volumetric data files
1517
+ ...volumetric_auto_render_filenames,
1518
+ [`DENSITY.CUBE`, true],
1519
+ [`CHGCAR.lobster`, true],
1520
+ // Files that look like structure files but are supported
1521
+ [`structure_copy.cif`, true],
1522
+ [`trajectory_backup.xyz`, true],
1523
+ [`trajectory.log`, true], // Contains "trajectory" keyword
1524
+ // Very long filenames
1525
+ [`structure`.repeat(100) + `.cif`, true],
1526
+ // Unsupported files
1527
+ [`config.yaml`, false],
1528
+ [`simulation.trr`, false], // .trr files not supported
1529
+ [`md.dcd`, false], // .dcd files not supported
1530
+ [`document.txt`, false],
1531
+ [`script.py`, false],
1532
+ [`data.csv`, false],
1533
+ [`image.png`, false],
1534
+ [`archive.zip`, false],
1535
+ [`fake.gz`, false],
1536
+ [`config.ini`, false],
1537
+ [`log.txt`, false],
1538
+ [`README.md`, false],
1539
+ [`readme.md`, false],
1540
+ [`ReadMe.Md`, false],
1541
+ [`vite.config.ts`, false],
1542
+ [`test.spec.ts`, false],
1543
+ [`index.html`, false],
1544
+ [`style.css`, false],
1545
+ [`app.js`, false],
1546
+ [`data.sql`, false],
1547
+ [`backup.tar`, false],
1548
+ [`compressed.7z`, false],
1549
+ [`binary.bin`, false],
1550
+ [`.pre-commit-config.yaml`, false],
1551
+ [`changelog.md`, false],
1552
+ [`.prettierrc`, false],
1553
+ [`.gitignore`, false],
1554
+ [`dockerfile`, false],
1555
+ [`makefile`, false],
1556
+ [`.env`, false],
1557
+ [`.env.local`, false],
1558
+ [`.env.production`, false],
1559
+ [`.github/workflows/ci.yml`, false],
1560
+ [`dist/bundle.js`, false],
1561
+ [`build/index.html`, false],
1562
+ [`coverage/lcov.info`, false],
1563
+ [`.cache/build.js`, false],
1564
+ [`structure.json.bak`, false],
1565
+ [`crystal.poscar.lock`, true],
1566
+ [`simulation.log`, true],
1567
+ [`backup.old`, false],
1568
+ [`original.orig`, false],
1569
+ [`patch.diff`, false],
1570
+ [`structure.txt`, false],
1571
+ [`crystal.md`, false],
1572
+ [`molecule.doc`, false],
1573
+ [`poscar.bak`, true],
1574
+ [`contcar.old`, true],
1575
+ [`document.txt.gz`, false],
1576
+ [`script.py.gz`, false],
1577
+ [`data.csv.gz`, false],
1578
+ [`image.png.gz`, false],
1579
+ [`archive.zip.gz`, false],
1580
+ [`structure.cif.bz2`, false],
1581
+ [`density.cube.bz2`, false],
1582
+ [`PARCHG.bz2`, false],
1583
+ [`myCHGCARfile`, false],
1584
+ [`prefixPARCHGsuffix`, false],
1585
+ [`structure.cif.bak`, false],
1586
+ [`crystal.poscar.old`, true],
1587
+ [`molecule.xyz~`, false],
1588
+ [`structure.cif.swp`, false],
1589
+ [`DOCUMENT.TXT`, false],
1590
+ [`document.TXT`, false],
1591
+ [`Document.txt`, false],
1592
+ [`SCRIPT.PY`, false],
1593
+ [`script.PY`, false],
1594
+ [`Script.py`, false],
1595
+ [`DATA.CSV`, false],
1596
+ [`data.CSV`, false],
1597
+ [`Data.csv`, false],
1598
+ // Configuration files that should never auto-render
1599
+ [`package.json`, false],
1600
+ [`tsconfig.json`, false],
1601
+ [`vite.config.ts`, false],
1602
+ [`webpack.config.js`, false],
1603
+ [`rollup.config.js`, false],
1604
+ [`eslint.config.js`, false],
1605
+ [`prettier.config.js`, false],
1606
+ [`babel.config.js`, false],
1607
+ [`jest.config.js`, false],
1608
+ [`karma.conf.js`, false],
1609
+ [`cypress.json`, false],
1610
+ [`playwright.config.ts`, false],
1611
+ [`.eslintrc.json`, false],
1612
+ [`.prettierrc`, false],
1613
+ [`.babelrc`, false],
1614
+ [`.jest.config.js`, false],
1615
+ [`.karma.conf.js`, false],
1616
+ [`.cypress.json`, false],
1617
+ [`.playwright.config.ts`, false],
1618
+ [`.npmrc`, false],
1619
+ [`.yarnrc`, false],
1620
+ [`.vscode/settings.json`, false],
1621
+ [`.idea/workspace.xml`, false],
1622
+ [`.nyc_output/coverage.json`, false],
1623
+ [`.tmp/temp.json`, false],
1624
+ [`.temp/structure.json`, false],
1625
+ [`node_modules/package.json`, false],
1626
+ // Edge cases
1627
+ [``, false],
1628
+ [` `, false],
1629
+ [`.`, false],
1630
+ [`..`, false],
1631
+ [`/`, false],
1632
+ [`\\`, false],
1633
+ [`a`.repeat(1000) + `.txt`, false],
1634
+ // Null/undefined inputs
1635
+ [null as unknown as string, false],
1636
+ [undefined as unknown as string, false],
1637
+ ])(`should detect auto-render for "%s" as %s`, (filename, expected) => {
1638
+ expect(should_auto_render(filename)).toBe(expected)
1639
+ })
1640
+
1641
+ test(`should register auto-render functionality`, () => {
1642
+ const mock_context = {
1643
+ subscriptions: { push: vi.fn() },
1644
+ } as unknown as ExtensionContext
1645
+ activate(mock_context)
1646
+ expect(mock_vscode.workspace.onDidOpenTextDocument).toHaveBeenCalledWith(
1647
+ expect.any(Function),
1648
+ )
1649
+ })
1650
+
1651
+ test(`should handle rapid file detection efficiently`, () => {
1652
+ const filenames = Array.from({ length: 100 }, (_, idx) => `test_${idx}.cif`)
1653
+ const start = performance.now()
1654
+ filenames.forEach(should_auto_render)
1655
+ expect(performance.now() - start).toBeLessThan(10)
1656
+ })
1657
+
1658
+ test(`should not trigger on non-file URIs`, () => {
1659
+ const mock_context = {
1660
+ subscriptions: { push: vi.fn() },
1661
+ } as unknown as ExtensionContext
1662
+
1663
+ activate(mock_context)
1664
+
1665
+ // Get the registered callback
1666
+ const open_doc_calls = mock_vscode.workspace.onDidOpenTextDocument.mock
1667
+ .calls as unknown as unknown[][]
1668
+ const on_did_open_text_document_callback = open_doc_calls[0]?.[0] as
1669
+ | ((doc: unknown) => void)
1670
+ | undefined
1671
+ expect(on_did_open_text_document_callback).toBeDefined()
1672
+
1673
+ // Mock document with non-file URI
1674
+ const mock_document = {
1675
+ uri: { scheme: `untitled` },
1676
+ }
1677
+
1678
+ expect(() => on_did_open_text_document_callback?.(mock_document)).not.toThrow()
1679
+ })
1680
+
1681
+ test(`should respect auto_render configuration setting`, () => {
1682
+ const mock_context = {
1683
+ subscriptions: { push: vi.fn() },
1684
+ } as unknown as ExtensionContext
1685
+
1686
+ // Mock configuration to disable auto_render
1687
+ mock_vscode.workspace.getConfiguration.mockReturnValue({
1688
+ get: vi.fn((key: string, default_val: string) => {
1689
+ if (key === `auto_render`) return false
1690
+ return default_val
1691
+ }),
1692
+ })
1693
+
1694
+ activate(mock_context)
1695
+
1696
+ const open_doc_calls = mock_vscode.workspace.onDidOpenTextDocument.mock
1697
+ .calls as unknown as unknown[][]
1698
+ const on_did_open_text_document_callback = open_doc_calls[0]?.[0] as
1699
+ | ((doc: unknown) => void)
1700
+ | undefined
1701
+ expect(on_did_open_text_document_callback).toBeDefined()
1702
+
1703
+ // Mock document with supported file
1704
+ const mock_document = {
1705
+ uri: { scheme: `file`, fsPath: `/test/structure.cif` },
1706
+ }
1707
+
1708
+ expect(() => on_did_open_text_document_callback?.(mock_document)).not.toThrow()
1709
+ })
1710
+
1711
+ test(`should handle file reading errors gracefully during auto-render`, async () => {
1712
+ const mock_context = {
1713
+ subscriptions: { push: vi.fn() },
1714
+ } as unknown as ExtensionContext
1715
+
1716
+ // Mock vscode.workspace.fs.stat to throw an error
1717
+ mock_vscode.workspace.fs.stat.mockRejectedValue(new Error(`File not found`))
1718
+
1719
+ // Enable auto_render in config
1720
+ mock_vscode.workspace.getConfiguration.mockReturnValue({
1721
+ get: vi.fn((_key: string, default_val: string) =>
1722
+ _key === `auto_render` ? `true` : default_val,
1723
+ ),
1724
+ })
1725
+
1726
+ activate(mock_context)
1727
+
1728
+ const open_doc_calls = mock_vscode.workspace.onDidOpenTextDocument.mock
1729
+ .calls as unknown as unknown[][]
1730
+ const on_did_open_text_document_callback = open_doc_calls[0]?.[0] as
1731
+ | ((doc: unknown) => void)
1732
+ | undefined
1733
+ expect(on_did_open_text_document_callback).toBeDefined()
1734
+
1735
+ // Mock document with supported file
1736
+ const mock_document = {
1737
+ uri: { scheme: `file`, fsPath: `/test/structure.cif` },
1738
+ }
1739
+
1740
+ // Should show error message when file reading fails
1741
+ expect(() => on_did_open_text_document_callback?.(mock_document)).not.toThrow()
1742
+
1743
+ await vi.waitFor(() => {
1744
+ expect(mock_vscode.window.showErrorMessage).toHaveBeenCalledWith(
1745
+ expect.stringContaining(`MatterViz auto-render failed:`),
1746
+ )
1747
+ })
1748
+ })
1749
+ })
1750
+
1751
+ describe(`Multi-frame xyz/extxyz handling`, () => {
1752
+ test(`should correctly identify multi-frame XYZ as trajectory using content`, () => {
1753
+ // Multi-frame XYZ content (2 frames)
1754
+ const multi_frame_xyz_content = `3
1755
+ frame 1
1756
+ H 0.0 0.0 0.0
1757
+ O 0.0 0.0 1.0
1758
+ H 0.0 1.0 0.0
1759
+ 3
1760
+ frame 2
1761
+ H 0.1 0.0 0.0
1762
+ O 0.0 0.1 1.0
1763
+ H 0.0 1.0 0.1`
1764
+
1765
+ // Single-frame XYZ content
1766
+ const single_frame_xyz_content = `3
1767
+ water molecule
1768
+ H 0.0 0.0 0.0
1769
+ O 0.0 0.0 1.0
1770
+ H 0.0 1.0 0.0`
1771
+
1772
+ // Test 1: Verify is_trajectory_file directly detects multi-frame content
1773
+ expect(is_trajectory_file(`multi-frame.xyz`, multi_frame_xyz_content)).toBe(true)
1774
+ expect(is_trajectory_file(`single-frame.xyz`, single_frame_xyz_content)).toBe(false)
1775
+
1776
+ // Test 2: Verify filename-only detection doesn't identify .xyz as trajectory
1777
+ expect(is_trajectory_file(`multi-frame.xyz`)).toBe(false) // filename-only should be false
1778
+ expect(is_trajectory_file(`single-frame.xyz`)).toBe(false) // filename-only should be false
1779
+
1780
+ // Test 3: Test with FileData objects (simulating what infer_view_type receives)
1781
+ const multi_frame_file: FileData = {
1782
+ filename: `multi-frame.xyz`,
1783
+ content: multi_frame_xyz_content,
1784
+ is_base64: false,
1785
+ }
1786
+
1787
+ const single_frame_file: FileData = {
1788
+ filename: `single-frame.xyz`,
1789
+ content: single_frame_xyz_content,
1790
+ is_base64: false,
1791
+ }
1792
+
1793
+ const compressed_file: FileData = {
1794
+ filename: `trajectory.xyz.gz`,
1795
+ content: `base64encodedcontent`,
1796
+ is_base64: true,
1797
+ }
1798
+
1799
+ // Test what infer_view_type logic would do:
1800
+ // For non-compressed files, pass content
1801
+ expect(is_trajectory_file(multi_frame_file.filename, multi_frame_file.content)).toBe(
1802
+ true,
1803
+ )
1804
+ expect(is_trajectory_file(single_frame_file.filename, single_frame_file.content)).toBe(
1805
+ false,
1806
+ )
1807
+
1808
+ // For compressed files, don't pass content (falls back to filename-only)
1809
+ expect(is_trajectory_file(compressed_file.filename)).toBe(true) // .xyz.gz with trajectory keyword is detected as trajectory by filename
1810
+
1811
+ // Test 4: Test webview creation scenario directly with content-based detection
1812
+ const multi_frame_html = create_html(mock_webview, mock_context, {
1813
+ type: is_trajectory_file(multi_frame_file.filename, multi_frame_file.content)
1814
+ ? `trajectory`
1815
+ : `structure`,
1816
+ data: multi_frame_file,
1817
+ theme: `light`,
1818
+ })
1819
+
1820
+ const multi_frame_parsed_data = JSON.parse(
1821
+ /matterviz_data=(\{[\s\S]*?\});/.exec(multi_frame_html)?.[1] ?? `{}`,
1822
+ )
1823
+
1824
+ expect(multi_frame_parsed_data.type).toBe(`trajectory`)
1825
+ expect(multi_frame_parsed_data.data.filename).toBe(`multi-frame.xyz`)
1826
+ expect(multi_frame_parsed_data.data.content).toBe(multi_frame_xyz_content)
1827
+
1828
+ // Test 5: Test single-frame for comparison
1829
+ const single_frame_html = create_html(mock_webview, mock_context, {
1830
+ type: is_trajectory_file(single_frame_file.filename, single_frame_file.content)
1831
+ ? `trajectory`
1832
+ : `structure`,
1833
+ data: single_frame_file,
1834
+ theme: `light`,
1835
+ })
1836
+
1837
+ const single_frame_parsed_data = JSON.parse(
1838
+ /matterviz_data=(\{[\s\S]*?\});/.exec(single_frame_html)?.[1] ?? `{}`,
1839
+ )
1840
+
1841
+ expect(single_frame_parsed_data.type).toBe(`structure`)
1842
+
1843
+ // Test 6: Test compressed file falls back correctly
1844
+ const compressed_html = create_html(mock_webview, mock_context, {
1845
+ type: is_trajectory_file(compressed_file.filename) ? `trajectory` : `structure`,
1846
+ data: compressed_file,
1847
+ theme: `light`,
1848
+ })
1849
+
1850
+ const compressed_parsed_data = JSON.parse(
1851
+ /matterviz_data=(\{[\s\S]*?\});/.exec(compressed_html)?.[1] ?? `{}`,
1852
+ )
1853
+
1854
+ expect(compressed_parsed_data.type).toBe(`trajectory`) // Should be trajectory since filename contains trajectory keyword
1855
+ })
1856
+
1857
+ test(`should handle compressed XYZ files by falling back to filename-only detection`, () => {
1858
+ // Test compressed file (should fall back to filename-only detection)
1859
+ const compressed_file = {
1860
+ filename: `trajectory.xyz.gz`,
1861
+ content: `base64encodedcontent`, // This is binary/compressed
1862
+ is_base64: true,
1863
+ }
1864
+
1865
+ // For compressed files, infer_view_type should fall back to filename-only detection
1866
+ // Since .xyz.gz with trajectory keyword is detected as trajectory by filename, it should be 'trajectory'
1867
+ expect(is_trajectory_file(compressed_file.filename)).toBe(true) // filename-only detection
1868
+
1869
+ // Test the HTML generation scenario
1870
+ const html = create_html(mock_webview, mock_context, {
1871
+ type: is_trajectory_file(compressed_file.filename) ? `trajectory` : `structure`,
1872
+ data: compressed_file,
1873
+ theme: `light`,
1874
+ })
1875
+
1876
+ const parsed_data = JSON.parse(/matterviz_data=(\{[\s\S]*?\});/.exec(html)?.[1] ?? `{}`)
1877
+
1878
+ // Should be 'trajectory' since filename contains trajectory keyword
1879
+ expect(parsed_data.type).toBe(`trajectory`)
1880
+ })
1881
+ })
1882
+
1883
+ describe(`Default Settings`, () => {
1884
+ // Helper to create mock config and test setting
1885
+ const test_setting = (
1886
+ result_path: string,
1887
+ expected_value: unknown,
1888
+ config_key: string,
1889
+ ) => {
1890
+ const parts = config_key.split(`.`)
1891
+
1892
+ const mock_config = {
1893
+ get: vi.fn((key: string, default_val?: unknown): unknown => {
1894
+ if (parts.length === 2 && key === parts[0]) {
1895
+ return { [parts[1]]: expected_value }
1896
+ } else if (parts.length === 1 && key === parts[0]) return expected_value
1897
+ return default_val
1898
+ }),
1899
+ }
1900
+ // @ts-expect-error: Mock type override needed for testing
1901
+ mock_vscode.workspace.getConfiguration.mockReturnValue(mock_config)
1902
+
1903
+ const result = get_defaults()
1904
+ const value = result_path
1905
+ .split(`.`)
1906
+ .reduce(
1907
+ (obj: Record<string, unknown>, key: string) => obj?.[key] as Record<string, unknown>,
1908
+ result,
1909
+ )
1910
+
1911
+ return Array.isArray(expected_value)
1912
+ ? expect(value).toEqual(expected_value)
1913
+ : expect(value).toBe(expected_value)
1914
+ }
1915
+
1916
+ test(`should merge user settings with defaults`, () => {
1917
+ const user_config = {
1918
+ structure: { atom_radius: 1.5, show_bonds: `always`, bond_color: `#ff0000` },
1919
+ trajectory: { auto_play: true },
1920
+ }
1921
+ const mock_config = {
1922
+ get: vi.fn((key: string, default_val?: unknown) => {
1923
+ if (key === `structure`) return user_config.structure
1924
+ if (key === `trajectory`) return user_config.trajectory
1925
+ return default_val
1926
+ }),
1927
+ }
1928
+ // @ts-expect-error: Mock type override needed for testing
1929
+ mock_vscode.workspace.getConfiguration.mockReturnValue(mock_config)
1930
+
1931
+ const result = get_defaults()
1932
+
1933
+ expect(result.structure.atom_radius).toBe(1.5)
1934
+ expect(result.structure.show_bonds).toBe(`always`)
1935
+ expect(result.structure.bond_color).toBe(`#ff0000`)
1936
+ expect(result.trajectory.auto_play).toBe(true)
1937
+ expect(result.structure.same_size_atoms).toBe(false) // Falls back to default
1938
+ })
1939
+
1940
+ test.each([
1941
+ // Numbers
1942
+ [`structure.atom_radius`, 1.5],
1943
+ [`structure.sphere_segments`, 24],
1944
+ [`structure.bond_thickness`, 0.2],
1945
+ [`structure.rotation_damping`, 0.2],
1946
+ [`structure.zoom_speed`, 1.0],
1947
+ [`structure.pan_speed`, 1.0],
1948
+ [`structure.auto_rotate`, 2.0],
1949
+ [`structure.site_label_size`, 14],
1950
+ [`structure.site_label_padding`, 4],
1951
+ [`structure.ambient_light`, 0.6],
1952
+ [`structure.directional_light`, 0.8],
1953
+ [`structure.vector_scale`, 2.0],
1954
+ [`structure.vector_origin_gap`, 0.25],
1955
+ [`structure.cell_edge_opacity`, 0.5],
1956
+ [`structure.cell_surface_opacity`, 0.2],
1957
+ [`background_opacity`, 0.8],
1958
+ [`trajectory.fps`, 10],
1959
+ [`trajectory.step_labels`, 10],
1960
+
1961
+ // Booleans
1962
+ [`structure.same_size_atoms`, true],
1963
+ [`structure.show_atoms`, false],
1964
+ [`structure.show_bonds`, `always`],
1965
+ [`structure.show_site_labels`, true],
1966
+ [`structure.show_cell`, true],
1967
+ [`structure.show_cell_vectors`, true],
1968
+ [`structure.show_image_atoms`, true],
1969
+ [`structure.show_gizmo`, false],
1970
+ [`trajectory.auto_play`, true],
1971
+ [`trajectory.show_controls`, false],
1972
+
1973
+ // Colors (strings)
1974
+ [`structure.bond_color`, `#ff0000`],
1975
+ [`structure.site_label_color`, `#00ff00`],
1976
+ [`structure.site_label_bg_color`, `#333333`],
1977
+ [`structure.cell_edge_color`, `#aaaaaa`],
1978
+ [`structure.cell_surface_color`, `#bbbbbb`],
1979
+ [`structure.vector_color`, `#ffff00`],
1980
+ [`background_color`, `#111111`],
1981
+
1982
+ // String enums
1983
+ [`structure.bonding_strategy`, `solid_angle`],
1984
+ [`structure.camera_projection`, `orthographic`],
1985
+ [`color_scheme`, `Jmol`],
1986
+ [`composition.color_scheme`, `Alloy`],
1987
+ [`trajectory.display_mode`, `scatter`],
1988
+ [`trajectory.layout`, `vertical`],
1989
+ [`composition.display_mode`, `bar`],
1990
+
1991
+ // Arrays
1992
+ [`structure.camera_position`, [1, 2, 3]],
1993
+ [`structure.site_label_offset`, [0.5, 1.0, 0]],
1994
+ [`trajectory.fps_range`, [0.5, 60]],
1995
+ ])(`should handle setting: %s = %s`, (result_path, expected_value) => {
1996
+ test_setting(result_path, expected_value, result_path)
1997
+ })
1998
+
1999
+ test.each([
2000
+ [{ get: vi.fn(() => undefined) }, `missing config`],
2001
+ [
2002
+ {
2003
+ get: vi.fn((key: string, default_val?: unknown) =>
2004
+ key === `defaults`
2005
+ ? {
2006
+ structure: {
2007
+ atom_radius: `invalid`,
2008
+ show_bonds: `invalid-value`,
2009
+ bond_color: 123,
2010
+ },
2011
+ }
2012
+ : default_val,
2013
+ ),
2014
+ },
2015
+ `invalid values`,
2016
+ ],
2017
+ ])(`should handle %s gracefully`, (mock_config, _description) => {
2018
+ // @ts-expect-error: Mock type override needed for testing
2019
+ mock_vscode.workspace.getConfiguration.mockReturnValue(mock_config)
2020
+
2021
+ expect(() => get_defaults()).not.toThrow()
2022
+ const result = get_defaults()
2023
+
2024
+ expect(result).toEqual(
2025
+ expect.objectContaining({
2026
+ structure: expect.any(Object),
2027
+ trajectory: expect.any(Object),
2028
+ composition: expect.any(Object),
2029
+ }),
2030
+ )
2031
+ })
2032
+
2033
+ test(`should handle workspace config errors`, () => {
2034
+ mock_vscode.workspace.getConfiguration.mockImplementation(() => {
2035
+ throw new Error(`Config access failed`)
2036
+ })
2037
+
2038
+ expect(() => get_defaults()).not.toThrow()
2039
+ })
2040
+ })
2041
+ })