diff --git a/src/electron/frontend/core/components/Neurosift.js b/src/electron/frontend/core/components/Neurosift.ts
similarity index 68%
rename from src/electron/frontend/core/components/Neurosift.js
rename to src/electron/frontend/core/components/Neurosift.ts
index 381e6dbca..3214396f1 100644
--- a/src/electron/frontend/core/components/Neurosift.js
+++ b/src/electron/frontend/core/components/Neurosift.ts
@@ -1,99 +1,110 @@
-import { LitElement, css, html } from "lit";
-
-import { Loader } from "./Loader";
-import { FullScreenToggle } from "./FullScreenToggle";
-import { baseUrl } from "../server/globals";
-
-export function getURLFromFilePath(file, projectName) {
- const regexp = new RegExp(`.+(${projectName}.+)`);
- return `${baseUrl}/preview/${file.match(regexp)[1]}`;
-}
-
-export class Neurosift extends LitElement {
- static get styles() {
- return css`
- :host {
- background: white;
- width: 100%;
- height: 100%;
- display: grid;
- grid-template-rows: 100%;
- grid-template-columns: 100%;
- position: relative;
- --loader-color: hsl(200, 80%, 50%);
- }
-
- iframe,
- .loader-container {
- width: 100%;
- height: 100%;
- }
-
- .loader-container {
- display: flex;
- align-items: center;
- justify-content: center;
- position: absolute;
- top: 0;
- left: 0;
- }
-
- .fullscreen-toggle {
- display: flex;
- position: absolute;
- top: 10px;
- right: 10px;
- padding: 10px;
- color: white;
- background-color: gainsboro;
- border: 1px solid gray;
- border-radius: 10px;
- cursor: pointer;
- }
-
- span {
- font-size: 14px;
- }
-
- small {
- padding-left: 10px;
- }
-
- iframe {
- border: 0;
- }
- `;
- }
-
- static get properties() {
- return {
- url: { type: String, reflect: true },
- };
- }
-
- constructor({ url, fullscreen = true } = {}) {
- super();
- this.url = url;
- this.fullscreen = fullscreen;
- }
-
- render() {
- return this.url
- ? html`
- ${new Loader({
- message: `Loading Neurosift view...
${this.url}`,
- })}
-
- ${this.fullscreen ? new FullScreenToggle({ target: this }) : ""}
- `
- : ``;
- }
-}
-
-customElements.get("neurosift-iframe") || customElements.define("neurosift-iframe", Neurosift);
+import { LitElement, css, html, CSSResult, TemplateResult } from "lit";
+import { Loader } from "./Loader";
+import { FullScreenToggle } from "./FullScreenToggle";
+import { baseUrl } from "../server/globals";
+
+export function getURLFromFilePath(file: string, projectName: string): string {
+ const regexp = new RegExp(`.+(${projectName}.+)`);
+ const match = file.match(regexp);
+ if (!match) throw new Error(`File path ${file} does not contain project name ${projectName}`);
+ return `${baseUrl}/preview/${match[1]}`;
+}
+
+export class Neurosift extends LitElement {
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ background: white;
+ width: 100%;
+ height: 100%;
+ display: grid;
+ grid-template-rows: 100%;
+ grid-template-columns: 100%;
+ position: relative;
+ --loader-color: hsl(200, 80%, 50%);
+ }
+
+ iframe,
+ .loader-container {
+ width: 100%;
+ height: 100%;
+ }
+
+ .loader-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ .fullscreen-toggle {
+ display: flex;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ padding: 10px;
+ color: white;
+ background-color: gainsboro;
+ border: 1px solid gray;
+ border-radius: 10px;
+ cursor: pointer;
+ }
+
+ span {
+ font-size: 14px;
+ }
+
+ small {
+ padding-left: 10px;
+ }
+
+ iframe {
+ border: 0;
+ }
+ `;
+ }
+
+ static get properties() {
+ return {
+ url: { type: String, reflect: true },
+ fullscreen: { type: Boolean }
+ };
+ }
+
+ declare url?: string;
+ declare fullscreen: boolean;
+
+ constructor(options: { url?: string; fullscreen?: boolean } = {}) {
+ super();
+ this.url = options.url;
+ this.fullscreen = options.fullscreen ?? true;
+ }
+
+ render(): TemplateResult | string {
+ // Clear neurosift cross-session storage database
+ // see https://github.com/NeurodataWithoutBorders/nwb-guide/issues/974
+ if (this.url) {
+ indexedDB.deleteDatabase("neurosift-hdf5-cache");
+ }
+
+ return this.url
+ ? html`
+ ${new Loader({
+ message: `Loading Neurosift view...
${this.url}`,
+ })}
+
+ ${this.fullscreen ? new FullScreenToggle({ target: this }) : ""}
+ `
+ : "";
+ }
+}
+
+customElements.get("neurosift-iframe") || customElements.define("neurosift-iframe", Neurosift);
diff --git a/src/electron/frontend/core/components/pages/preview/PreviewPage.js b/src/electron/frontend/core/components/pages/preview/PreviewPage.js
index 8dfe7b4d6..0b4c44eca 100644
--- a/src/electron/frontend/core/components/pages/preview/PreviewPage.js
+++ b/src/electron/frontend/core/components/pages/preview/PreviewPage.js
@@ -2,7 +2,7 @@ import { html } from "lit";
import { Page } from "../Page.js";
import { onThrow } from "../../../errors";
import { JSONSchemaInput } from "../../JSONSchemaInput.js";
-import { Neurosift } from "../../Neurosift.js";
+import { Neurosift } from "../../Neurosift";
import { baseUrl } from "../../../server/globals";
export class PreviewPage extends Page {
diff --git a/tests/components/Neurosift.test.ts b/tests/components/Neurosift.test.ts
new file mode 100644
index 000000000..d9efe93fc
--- /dev/null
+++ b/tests/components/Neurosift.test.ts
@@ -0,0 +1,74 @@
+// Test that the indexed database is not cleared on initial render and cleared
+// when a url is provided and the component is rendered again as in PreviewPage.js
+import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
+import { Neurosift } from '../../src/electron/frontend/core/components/Neurosift';
+
+describe('Neurosift', () => {
+ let originalIndexedDB: IDBFactory;
+ let mockDeleteDatabase: ReturnType;
+
+ beforeEach(() => {
+ // Mock indexedDB.deleteDatabase so that we can test if it is called
+ mockDeleteDatabase = vi.fn();
+ originalIndexedDB = window.indexedDB;
+ window.indexedDB = {
+ deleteDatabase: mockDeleteDatabase,
+ open: vi.fn(),
+ databases: vi.fn(),
+ cmp: vi.fn()
+ } as unknown as IDBFactory;
+ });
+
+ afterEach(() => {
+ // Restore original indexedDB
+ window.indexedDB = originalIndexedDB;
+ });
+
+ test('if neurosift storage is not cleared on initial render', async () => {
+ const neurosift = new Neurosift();
+
+ // Add the component to the DOM
+ document.body.appendChild(neurosift);
+
+ // Wait for the component to render
+ neurosift.requestUpdate();
+
+ // Verify deleteDatabase was not called
+ expect(mockDeleteDatabase).not.toHaveBeenCalled();
+
+ // Remove the component from the DOM
+ document.body.removeChild(neurosift);
+ });
+
+ test('if neurosift storage is cleared when url is provided and component is rendered again', async () => {
+ const neurosift = new Neurosift();
+
+ // Add the component to the DOM
+ document.body.appendChild(neurosift);
+
+ // Wait for the component to render
+ neurosift.requestUpdate();
+
+ // Set the URL to a test URL
+ neurosift.url = 'http://test.url/files/test.nwb';
+
+ // Wait for the component to render again
+ neurosift.requestUpdate();
+ await neurosift.updateComplete;
+
+ // Verify deleteDatabase was called
+ expect(mockDeleteDatabase).toHaveBeenCalledWith('neurosift-hdf5-cache');
+ expect(mockDeleteDatabase).toHaveBeenCalledTimes(1);
+
+ // Wait for the component to render again
+ neurosift.requestUpdate();
+ await neurosift.updateComplete;
+
+ // Verify deleteDatabase was called
+ expect(mockDeleteDatabase).toHaveBeenCalledWith('neurosift-hdf5-cache');
+ expect(mockDeleteDatabase).toHaveBeenCalledTimes(2);
+
+ // Remove the component from the DOM
+ document.body.removeChild(neurosift);
+ });
+});