diff --git a/.github/workflows/daily_tests.yml b/.github/workflows/daily_tests.yml
index 32b8b1edd5..fcae04f533 100644
--- a/.github/workflows/daily_tests.yml
+++ b/.github/workflows/daily_tests.yml
@@ -39,10 +39,7 @@ jobs:
NotifyOnAnyFailure:
runs-on: ubuntu-latest
needs: [DevTests, LiveServices, BuildTests, ExampleDataCache, ExampleDataTests]
- if: |
- ${{
- always() && (needs.DevTests.result == 'failure' || needs.LiveServices.result == 'failure' || needs.BuildTests.result == 'failure' || needs.ExampleDataCache.result == 'failure' || needs.ExampleDataTests.result == 'failure')
- }}
+ if: always() && (${{ needs.DevTests.result }} == 'failure' || ${{ needs.LiveServices.result }} == 'failure' || ${{ needs.BuildTests.result }} == 'failure' || ${{ needs.ExampleDataCache.result }} == 'failure' || ${{ needs.ExampleDataTests.result }} == 'failure')
steps:
- name: Printout which failed
run: |
@@ -51,6 +48,10 @@ jobs:
echo "BuildTests: ${{ needs.BuildTests.result }}"
echo "ExampleDataCache: ${{ needs.ExampleDataCache.result }}"
echo "ExampleDataTests: ${{ needs.ExampleDataTests.result }}"
+ - name: debugging
+ run: export test=(${{ needs.DevTests.result }} == 'failure' || ${{ needs.LiveServices.result }} == 'failure' || ${{ needs.BuildTests.result }} == 'failure' || ${{ needs.ExampleDataCache.result }} == 'failure' || ${{ needs.ExampleDataTests.result }} == 'failure')
+ - name: debugging print
+ run: echo $test
#- name: Printout logic trigger
# run: ${{ needs.DevTests.result }} == 'failure' || ${{ needs.LiveServices.result }} == 'failure' || ${{ needs.BuildTests.result }} == 'failure' || ${{ needs.ExampleDataCache.result }} == 'failure' || ${{ needs.ExampleDataTests.result }} == 'failure'
- uses: dawidd6/action-send-mail@v3
diff --git a/src/electron/frontend/core/components/FileSystemSelector.js b/src/electron/frontend/core/components/FileSystemSelector.js
index da6f4925e8..d9d444d8e4 100644
--- a/src/electron/frontend/core/components/FileSystemSelector.js
+++ b/src/electron/frontend/core/components/FileSystemSelector.js
@@ -280,9 +280,11 @@ export class FilesystemSelector extends LitElement {
>`
: ""}`}
-
diff --git a/src/electron/frontend/core/components/InspectorList.js b/src/electron/frontend/core/components/InspectorList.js
index 790124124b..5af3ac696c 100644
--- a/src/electron/frontend/core/components/InspectorList.js
+++ b/src/electron/frontend/core/components/InspectorList.js
@@ -4,13 +4,12 @@ import { getMessageType, isErrorImportance } from "../validation";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
-import { header } from '../../utils/text'
+import { header } from "../../utils/text";
const sortAlphabeticallyWithNumberedStrings = (a, b) => {
if (a === b) return 0;
- return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
-}
-
+ return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
+};
const sortList = (items) => {
return items
@@ -23,19 +22,19 @@ const sortList = (items) => {
else if (lowA) return 1;
else return -1;
})
-
+
.sort((a, b) => {
const aCritical = isErrorImportance.includes(a.importance);
const bCritical = isErrorImportance.includes(b.importance);
if (aCritical && bCritical) return 0;
else if (aCritical) return -1;
else return 1;
- })
+ });
};
const aggregateMessages = (items) => {
let messages = {};
- console.log(items)
+ console.log(items);
items.forEach((item) => {
const copy = { ...item };
delete copy.file_path;
diff --git a/src/electron/frontend/core/components/InstanceListItem.ts b/src/electron/frontend/core/components/InstanceListItem.ts
index 61ebc2afec..f2c8b5896e 100644
--- a/src/electron/frontend/core/components/InstanceListItem.ts
+++ b/src/electron/frontend/core/components/InstanceListItem.ts
@@ -7,7 +7,6 @@ export class InstanceListItem extends LitElement {
declare label: string
declare status: string
declare selected: boolean
- declare onRemoved?: Function
static get styles() {
return css`
@@ -120,7 +119,7 @@ export class InstanceListItem extends LitElement {
}
}
- constructor({ label, status, selected, onRemoved, id, ...metadata } = {
+ constructor({ label, status, selected, id, ...metadata } = {
label: "",
status: "",
selected: false
@@ -131,7 +130,6 @@ export class InstanceListItem extends LitElement {
this.status = status;
this.selected = selected;
this.metadata = metadata
- if (this.onRemoved) this.onRemoved = onRemoved;
}
#onClick () {
@@ -154,18 +152,6 @@ export class InstanceListItem extends LitElement {
>${this.label}
- ${this.onRemoved
- ? html`
x`
- : ""}
`
}
diff --git a/src/electron/frontend/core/components/InstanceManager.js b/src/electron/frontend/core/components/InstanceManager.js
index ad0d8a1e6e..cfb4e1f86d 100644
--- a/src/electron/frontend/core/components/InstanceManager.js
+++ b/src/electron/frontend/core/components/InstanceManager.js
@@ -1,6 +1,6 @@
import { LitElement, css, html } from "lit";
import "./Button";
-import { notify } from "../dependencies";
+import { notify } from "../notifications";
import { Accordion } from "./Accordion";
import { InstanceListItem } from "./InstanceListItem";
import { checkStatus } from "../validation";
@@ -142,7 +142,6 @@ export class InstanceManager extends LitElement {
this.header = props.header;
this.instanceType = props.instanceType ?? "Instance";
if (props.onAdded) this.onAdded = props.onAdded;
- if (props.onRemoved) this.onRemoved = props.onRemoved;
if (props.onDisplay) this.onDisplay = props.onDisplay;
this.controls = props.controls ?? [];
}
@@ -157,8 +156,12 @@ export class InstanceManager extends LitElement {
} else return content;
};
+ getInstance(id) {
+ return this.#items.find((item) => item.id === id);
+ }
+
updateState = (id, state) => {
- const item = this.#items.find((i) => i.id === id);
+ const item = this.getInstance(id);
item.status = state;
@@ -178,7 +181,6 @@ export class InstanceManager extends LitElement {
};
// onAdded = () => {}
- // onRemoved = () => {}
toggleInput = (force) => {
const newInfoDiv = this.shadowRoot.querySelector("#new-info");
@@ -248,34 +250,6 @@ export class InstanceManager extends LitElement {
#items = [];
#info = {};
- #onRemoved(ev) {
- const parent = ev.target.parentNode;
- const name = parent.getAttribute("data-instance");
- const ogPath = name.split("/");
- const path = [...ogPath];
- let target = toRender;
- const key = path.pop();
- target = path.reduce((acc, cur) => acc[cur], target);
- this.onRemoved(target[key], ogPath);
- delete target[key];
-
- if (parent.hasAttribute("selected")) {
- const previous = parent.previousElementSibling?.getAttribute("data-instance");
- if (previous) this.#selected = previous;
- else {
- const next = parent.nextElementSibling?.getAttribute("data-instance");
- if (next) this.#selected = next;
- else this.#selected = undefined;
- }
- }
-
- // parent.remove()
- // const instance = this.shadowRoot.querySelector(`div[data-instance="${name}"]`)
- // instance.remove()
-
- this.requestUpdate();
- }
-
#hideAll(chosenInstanceElement) {
Array.from(this.shadowRoot.querySelectorAll("div[data-instance]")).forEach((instanceElement) => {
if (instanceElement !== chosenInstanceElement) instanceElement.hidden = true;
@@ -332,7 +306,6 @@ export class InstanceManager extends LitElement {
id: key,
label: key.split("/").pop(),
selected: key === this.#selected,
- onRemoved: this.#onRemoved.bind(this),
...info,
};
diff --git a/src/electron/frontend/core/components/JSONSchemaInput.js b/src/electron/frontend/core/components/JSONSchemaInput.js
index f9966222f6..91ec60014d 100644
--- a/src/electron/frontend/core/components/JSONSchemaInput.js
+++ b/src/electron/frontend/core/components/JSONSchemaInput.js
@@ -19,6 +19,7 @@ import { OptionalSection } from "./OptionalSection";
import { InspectorListItem } from "./InspectorList.js";
import { renderDateTime, resolveDateTime } from "./DateTimeSelector";
import { isObject } from "../../utils/typecheck";
+import { resolve } from "../../utils/promises";
const isDevelopment = !!import.meta.env;
@@ -583,7 +584,7 @@ export class JSONSchemaInput extends LitElement {
// onUpdate = () => {}
// onValidate = () => {}
- updateData(value, forceValidate = false) {
+ async updateData(value, forceValidate = false) {
if (!forceValidate) {
// Update the actual input element
const inputElement = this.getElement();
@@ -603,9 +604,9 @@ export class JSONSchemaInput extends LitElement {
const name = path.splice(-1)[0];
this.#updateData(fullPath, value);
- this.#triggerValidation(name, path); // NOTE: Is asynchronous
+ const possiblePromise = this.#triggerValidation(name, path);
- return true;
+ return resolve(possiblePromise, () => true);
}
getElement = () => this.shadowRoot.querySelector(".schema-input");
@@ -641,7 +642,7 @@ export class JSONSchemaInput extends LitElement {
if (hooks.willTimeout !== false) this.#activateTimeoutValidation(name, path, hooks);
};
- #triggerValidation = async (name, path) => {
+ #triggerValidation = (name, path) => {
this.#clearTimeoutValidation();
return this.onValidate
? this.onValidate()
diff --git a/src/electron/frontend/core/components/NWBFilePreview.js b/src/electron/frontend/core/components/NWBFilePreview.js
index c5cc63cb23..43ead6138e 100644
--- a/src/electron/frontend/core/components/NWBFilePreview.js
+++ b/src/electron/frontend/core/components/NWBFilePreview.js
@@ -159,11 +159,7 @@ export class NWBFilePreview extends LitElement {
{ path: fileArr[0].info.file, ...options },
{ title }
) // Inspect the first file
- : await run(
- "neuroconv/inspect",
- { path, ...options },
- { title: title + "s" }
- ); // Inspect the folder
+ : await run("neuroconv/inspect", { path, ...options }, { title: title + "s" }); // Inspect the folder
const result = onlyFirstFile
? {
diff --git a/src/electron/frontend/core/components/pages/Page.js b/src/electron/frontend/core/components/pages/Page.js
index 886987dfbe..8cda7e1e36 100644
--- a/src/electron/frontend/core/components/pages/Page.js
+++ b/src/electron/frontend/core/components/pages/Page.js
@@ -2,7 +2,7 @@ import { LitElement, html } from "lit";
import { run } from "../../../utils/run";
import { get, save } from "../../progress/index.js";
-import { dismissNotification, notify } from "../../dependencies.js";
+import { dismissNotification, notify } from "../../notifications";
import { isStorybook } from "../../globals.js";
import { mapSessions, merge } from "../../../utils/data";
diff --git a/src/electron/frontend/core/components/pages/contact-us/Contact.js b/src/electron/frontend/core/components/pages/contact-us/Contact.js
index 2b8995dae9..c2e35bbb5a 100644
--- a/src/electron/frontend/core/components/pages/contact-us/Contact.js
+++ b/src/electron/frontend/core/components/pages/contact-us/Contact.js
@@ -2,7 +2,7 @@ import { html } from "lit";
import { contact_lottie } from "../../../../assets/lotties/contact-us-lotties.js";
import { Page } from "../Page.js";
-import { startLottie } from "../../../dependencies.js";
+import { startLottie } from "../../../lotties";
export class ContactPage extends Page {
header = {
diff --git a/src/electron/frontend/core/components/pages/documentation/Documentation.js b/src/electron/frontend/core/components/pages/documentation/Documentation.js
index 77ca419d45..8a3b62dbe2 100644
--- a/src/electron/frontend/core/components/pages/documentation/Documentation.js
+++ b/src/electron/frontend/core/components/pages/documentation/Documentation.js
@@ -2,7 +2,7 @@ import { html } from "lit";
import { docu_lottie } from "../../../../assets/lotties/documentation-lotties.js";
import { Page } from "../Page.js";
-import { startLottie } from "../../../dependencies.js";
+import { startLottie } from "../../../lotties";
import { Button } from "../../Button.js";
diff --git a/src/electron/frontend/core/components/pages/guided-mode/GuidedHome.js b/src/electron/frontend/core/components/pages/guided-mode/GuidedHome.js
index a71dc03d6b..8352f49a90 100644
--- a/src/electron/frontend/core/components/pages/guided-mode/GuidedHome.js
+++ b/src/electron/frontend/core/components/pages/guided-mode/GuidedHome.js
@@ -2,7 +2,7 @@ import { html } from "lit";
import { Page } from "../Page.js";
import { ProgressCard } from "./ProgressCard.js";
-import { startLottie } from "../../../dependencies.js";
+import { startLottie } from "../../../lotties";
import * as progress from "../../../progress/index.js";
import { newDataset } from "../../../../assets/lotties/index.js";
diff --git a/src/electron/frontend/core/components/pages/guided-mode/options/GuidedInspectorPage.js b/src/electron/frontend/core/components/pages/guided-mode/options/GuidedInspectorPage.js
index 3df88eb5f0..3cb9131854 100644
--- a/src/electron/frontend/core/components/pages/guided-mode/options/GuidedInspectorPage.js
+++ b/src/electron/frontend/core/components/pages/guided-mode/options/GuidedInspectorPage.js
@@ -134,29 +134,27 @@ export class GuidedInspectorPage extends Page {
return html`
${until(
(async () => {
-
this.report = inspector;
const oneFile = fileArr.length <= 1;
- const willRun = !this.report
+ const willRun = !this.report;
const swalOpts = willRun ? await createProgressPopup({ title: oneFile ? title : `${title}s` }) : {};
const { close: closeProgressPopup } = swalOpts;
if (oneFile) {
-
if (!this.report) {
const result = await run(
"neuroconv/inspect",
{ path: fileArr[0].info.file, ...options, request_id: swalOpts.id },
swalOpts
- ).catch((error) => {
- this.notify(error.message, "error");
- return null;
- })
- .finally(() => closeProgressPopup());
-
+ )
+ .catch((error) => {
+ this.notify(error.message, "error");
+ return null;
+ })
+ .finally(() => closeProgressPopup());
if (!result) return "Failed to generate inspector report.";
@@ -182,7 +180,6 @@ export class GuidedInspectorPage extends Page {
const path = getSharedPath(fileArr.map(({ info }) => info.file));
if (!this.report) {
-
const result = await run(
"neuroconv/inspect",
{ path, ...options, request_id: swalOpts.id },
diff --git a/src/electron/frontend/core/components/pages/inspect/InspectPage.js b/src/electron/frontend/core/components/pages/inspect/InspectPage.js
index e97605d83f..e711673b32 100644
--- a/src/electron/frontend/core/components/pages/inspect/InspectPage.js
+++ b/src/electron/frontend/core/components/pages/inspect/InspectPage.js
@@ -39,7 +39,7 @@ export class InspectPage extends Page {
}
);
- console.log(result)
+ console.log(result);
closeProgressPopup();
diff --git a/src/electron/frontend/core/components/pages/settings/SettingsPage.js b/src/electron/frontend/core/components/pages/settings/SettingsPage.js
index ae866c70f9..77746f42c0 100644
--- a/src/electron/frontend/core/components/pages/settings/SettingsPage.js
+++ b/src/electron/frontend/core/components/pages/settings/SettingsPage.js
@@ -12,7 +12,7 @@ import { Button } from "../../Button.js";
import { global, remove, save } from "../../../progress/index.js";
import { merge, setUndefinedIfNotDeclared } from "../../../../utils/data";
-import { notyf } from "../../../dependencies.js";
+import { notyf } from "../../../notifications";
import { homeDirectory, testDataFolderPath } from "../../../globals.js";
import { SERVER_FILE_PATH, electron, path, port, fs } from "../../../../utils/electron";
diff --git a/src/electron/frontend/core/components/table/Cell.ts b/src/electron/frontend/core/components/table/Cell.ts
index af458033bc..97854d716b 100644
--- a/src/electron/frontend/core/components/table/Cell.ts
+++ b/src/electron/frontend/core/components/table/Cell.ts
@@ -179,9 +179,8 @@ export class TableCell extends LitElement {
if (!this.editable) return // Don't set value if not editable
- if (this.input) this.input.set(value) // Ensure all operations are undoable
+ if (this.input) return this.input.set(value) // Ensure all operations are undoable
else this.#value = value // Silently set value if not rendered yet
-
}
#value
diff --git a/src/electron/frontend/core/errors.ts b/src/electron/frontend/core/errors.ts
index 8c64621d12..d60c6a5a42 100644
--- a/src/electron/frontend/core/errors.ts
+++ b/src/electron/frontend/core/errors.ts
@@ -1,2 +1,2 @@
-import { notify } from './dependencies'
+import { notify } from './notifications'
export const onThrow = (message: string, id?: string) => notify(id ? `
[${id}]: ${message}` : message, "error", 7000);
diff --git a/src/electron/frontend/core/index.ts b/src/electron/frontend/core/index.ts
index 815094fc84..ef44860c34 100644
--- a/src/electron/frontend/core/index.ts
+++ b/src/electron/frontend/core/index.ts
@@ -9,7 +9,7 @@ import { Dashboard } from './components/Dashboard.js'
import {
notyf,
notify
-} from './dependencies.js'
+} from './notifications'
import Swal from 'sweetalert2'
import { loadServerEvents, pythonServerOpened } from "./server/index.js";
diff --git a/src/electron/frontend/core/lotties.ts b/src/electron/frontend/core/lotties.ts
new file mode 100644
index 0000000000..a29418ebd6
--- /dev/null
+++ b/src/electron/frontend/core/lotties.ts
@@ -0,0 +1,19 @@
+import lottie from "lottie-web";
+
+import checkChromatic from "chromatic/isChromatic";
+export const isChromatic = checkChromatic();
+
+export const startLottie = (lottieElement: HTMLElement, animationData: any) => {
+ lottieElement.innerHTML = "";
+ const thisLottie = lottie.loadAnimation({
+ container: lottieElement,
+ animationData,
+ renderer: "svg",
+ loop: !isChromatic,
+ autoplay: !isChromatic,
+ });
+
+ if (isChromatic) thisLottie.goToAndStop(thisLottie.getDuration(true) - 1, true); // Go to last frame
+
+ return thisLottie;
+};
diff --git a/src/electron/frontend/core/dependencies.js b/src/electron/frontend/core/notifications.ts
similarity index 82%
rename from src/electron/frontend/core/dependencies.js
rename to src/electron/frontend/core/notifications.ts
index 810456d112..261007df0c 100644
--- a/src/electron/frontend/core/dependencies.js
+++ b/src/electron/frontend/core/notifications.ts
@@ -1,24 +1,4 @@
import { Notyf } from "notyf";
-import checkChromatic from "chromatic/isChromatic";
-import lottie from "lottie-web";
-
-// ---------- Lottie Helper ----------
-const isChromatic = checkChromatic();
-
-export const startLottie = (lottieElement, animationData) => {
- lottieElement.innerHTML = "";
- const thisLottie = lottie.loadAnimation({
- container: lottieElement,
- animationData,
- renderer: "svg",
- loop: !isChromatic,
- autoplay: !isChromatic,
- });
-
- if (isChromatic) thisLottie.goToAndStop(thisLottie.getDuration(true) - 1, true); // Go to last frame
-
- return thisLottie;
-};
const longDuration = 20000;
diff --git a/src/electron/frontend/core/server/index.ts b/src/electron/frontend/core/server/index.ts
index 89edce13a7..4fbc898adc 100644
--- a/src/electron/frontend/core/server/index.ts
+++ b/src/electron/frontend/core/server/index.ts
@@ -5,7 +5,7 @@ import { isTestEnvironment } from '../globals.js'
import {
notyf,
-} from '../dependencies.js'
+} from '../notifications'
import Swal from 'sweetalert2'
diff --git a/src/electron/frontend/utils/popups.ts b/src/electron/frontend/utils/popups.ts
index b89928d1dc..d7584704c8 100644
--- a/src/electron/frontend/utils/popups.ts
+++ b/src/electron/frontend/utils/popups.ts
@@ -40,7 +40,7 @@ export const createProgressPopup = async (
options.showCancelButton = true;
options.customClass = { actions: "swal-conversion-actions" };
}
-
+
await openProgressSwal(options, (result) => {
if (!result.isConfirmed) cancelController.abort();
});
diff --git a/src/electron/frontend/utils/random.ts b/src/electron/frontend/utils/random.ts
index 04fcbc60e2..12dec871cf 100644
--- a/src/electron/frontend/utils/random.ts
+++ b/src/electron/frontend/utils/random.ts
@@ -1,7 +1,13 @@
+const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
export const getRandomIndex = (count: number) => Math.floor(count * Math.random());
-export const getRandomString = () => Math.random().toString(36).substring(7);
+// return random string always of len characters
+export const getRandomString = (len = 7) => {
+ let result = "";
+ for (let i = 0; i < len; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
+ return result;
+}
export const getRandomSample = (
array: any[],
@@ -18,4 +24,4 @@ export const getRandomSample = (
result.push(element);
}
return result;
-};
+};
diff --git a/src/pyflask/manageNeuroconv/manage_neuroconv.py b/src/pyflask/manageNeuroconv/manage_neuroconv.py
index eac97906a1..007491060a 100644
--- a/src/pyflask/manageNeuroconv/manage_neuroconv.py
+++ b/src/pyflask/manageNeuroconv/manage_neuroconv.py
@@ -1438,10 +1438,9 @@ def _inspect_all(url, config):
from nwbinspector.utils import calculate_number_of_cpu
from tqdm_publisher import TQDMProgressSubscriber
-
path = config.pop("path", None)
- paths = [ path ] if path else config.pop("paths", [])
+ paths = [path] if path else config.pop("paths", [])
nwbfile_paths = []
for path in paths:
@@ -1450,7 +1449,7 @@ def _inspect_all(url, config):
nwbfile_paths.append(posix_path)
else:
nwbfile_paths.extend(list(posix_path.rglob("*.nwb")))
-
+
request_id = config.pop("request_id", None)
n_jobs = config.get("n_jobs", -2) # Default to all but one CPU
@@ -1542,6 +1541,7 @@ def _aggregate_symlinks_in_new_directory(paths, reason="", folder_path=None) ->
return folder_path
+
def _format_spikeglx_meta_file(bin_file_path: str) -> str:
bin_file_path = Path(bin_file_path)
diff --git a/src/pyflask/namespaces/neuroconv.py b/src/pyflask/namespaces/neuroconv.py
index b5246c991d..4312d427bc 100644
--- a/src/pyflask/namespaces/neuroconv.py
+++ b/src/pyflask/namespaces/neuroconv.py
@@ -161,6 +161,7 @@ def post(self):
else:
return upload_multiple_filesystem_objects_to_dandi(**neuroconv_namespace.payload)
+
@neuroconv_namespace.route("/announce/progress")
class InspectNWBFolder(Resource):
@neuroconv_namespace.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"})
diff --git a/tests/components/InstanceManager.test.ts b/tests/components/InstanceManager.test.ts
new file mode 100644
index 0000000000..7a52b8de4b
--- /dev/null
+++ b/tests/components/InstanceManager.test.ts
@@ -0,0 +1,120 @@
+import { describe, expect, it } from "vitest";
+import { InstanceManager } from "../../src/electron/frontend/core/components/InstanceManager";
+
+const createInstance = () => {
+ return true
+}
+
+const singleInstance = {
+ instance1: createInstance
+}
+
+const multipleInstances = {
+ instance1: createInstance,
+ instance2: createInstance,
+}
+
+const categorizedInstances = {
+ category1: {
+ instance1: createInstance,
+ instance2: createInstance,
+ },
+ category2: {
+ instance3: createInstance,
+ instance4: createInstance,
+ }
+}
+
+describe('InstanceManager', () => {
+
+ const mountComponent = async (props = {}) => {
+ const element = new InstanceManager(props);
+ document.body.appendChild(element);
+ await element.updateComplete;
+ return element
+ }
+
+
+ it('renders', async () => {
+ const element = await mountComponent({ instances: singleInstance });
+ expect(element.shadowRoot.querySelector('#content')).to.exist;
+ expect(element.shadowRoot.querySelector('#instance-sidebar')).to.exist;
+ element.remove()
+ });
+
+ it('toggles input visibility', async () => {
+ const element = await mountComponent({
+ instances: multipleInstances,
+ onAdded: () => ({ value: createInstance })
+ });
+
+ const button = element.shadowRoot.querySelector('#add-new-button');
+ const newInfoContainer = element.shadowRoot.querySelector('#new-info');
+ const input = newInfoContainer.querySelector('input');
+ const submitButton = newInfoContainer.querySelector('nwb-button');
+
+ expect(newInfoContainer.hidden).to.be.true;
+
+ button.click();
+ await element.updateComplete;
+ expect(newInfoContainer.hidden).to.be.false;
+
+ input.value = 'newInstance';
+ submitButton.click();
+ await element.updateComplete;
+
+ expect(newInfoContainer.hidden).to.be.true;
+
+ // NOTE: Check for new instance
+ expect(element.instances['newInstance']).to.exist;
+
+ element.remove()
+ });
+
+ it('updates state correctly', async () => {
+ const element = await mountComponent({
+ instances: multipleInstances,
+ });
+
+ element.updateState('instance1', 'inactive');
+ await element.updateComplete;
+
+ const instance = element.getInstance('instance1')
+
+ expect(instance.status).to.equal('inactive');
+ element.remove()
+ });
+
+ it('selects instance on click', async () => {
+ const element = await mountComponent({
+ instances: multipleInstances
+ });
+
+ const instance1 = element.getInstance('instance1');
+ const instance2 = element.getInstance('instance2');
+ instance2.click();
+ await element.updateComplete;
+
+ expect(element.shadowRoot.querySelector(`[data-instance=${instance1.id}]`).getAttribute('hidden')).to.be.null;
+ expect(element.shadowRoot.querySelector(`[data-instance=${instance2.id}]`).getAttribute('hidden')).to.exist;
+
+ // instance2.getAttribute('selected')).to.exist;
+ // expect(instance1.getAttribute('selected')).to.be.null;
+
+ element.remove()
+ });
+
+ it('renders accordion for categories', async () => {
+ const element = await mountComponent({
+ instances: categorizedInstances
+ });
+
+ await element.updateComplete;
+
+ const accordion1 = element.shadowRoot.querySelector('nwb-accordion[name="category1"]');
+ const accordion2 = element.shadowRoot.querySelector('nwb-accordion[name="instance1"]');
+ expect(accordion1).to.exist;
+ expect(accordion2).to.not.exist;
+ element.remove()
+ });
+});
diff --git a/tests/components/forms.test.ts b/tests/components/forms.test.ts
new file mode 100644
index 0000000000..a609f16984
--- /dev/null
+++ b/tests/components/forms.test.ts
@@ -0,0 +1,375 @@
+import { JSONSchemaForm } from '../../src/electron/frontend/core/components/JSONSchemaForm';
+import { describe, it, expect } from 'vitest';
+
+import { validateOnChange } from "../../src/electron/frontend/core/validation/index.js";
+import { SimpleTable } from '../../src/electron/frontend/core/components/SimpleTable'
+
+import baseMetadataSchema from '../../src/schemas/base-metadata.schema'
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+const NWBFileSchemaProperties = baseMetadataSchema.properties.NWBFile.properties
+
+// Helper function to mount the component
+async function mountComponent(props) {
+
+ const form = new JSONSchemaForm(props);
+
+ document.body.append(form)
+ await form.rendered
+
+ return form;
+}
+
+describe('JSONSchemaForm', () => {
+ it('renders text input correctly', async () => {
+
+ const defaultValue = 'John Doe';
+ const schema = {
+ properties: {
+ name: { type: 'string', title: 'Name', default: defaultValue },
+ },
+ };
+
+ const form = await mountComponent({ schema });
+ await form.rendered
+ const nameInput = form.getFormElement('name');
+ expect(nameInput).toBeDefined();
+ expect(nameInput.value).toBe(defaultValue);
+ expect(form.resolved.name).toBe(defaultValue);
+ await nameInput.updateData('Jane Doe');
+ expect(form.resolved.name).toBe('Jane Doe');
+ });
+
+ it('renders number input correctly', async () => {
+ const schema = {
+ properties: {
+ age: { type: 'number', title: 'Age' },
+ },
+ };
+
+ const form = await mountComponent({ schema });
+ const ageInput = form.getFormElement('age');
+ expect(ageInput).toBeDefined();
+ await ageInput.updateData(30);
+ expect(form.resolved.age).toBe(30);
+ });
+
+ it('renders boolean input correctly', async () => {
+ const schema = {
+ properties: {
+ active: { type: 'boolean', title: 'Active' },
+ },
+ };
+
+ const form = await mountComponent({ schema });
+ const activeInput = form.getFormElement('active');
+ expect(activeInput).toBeDefined();
+ await activeInput.updateData(true);
+ expect(form.resolved.active).toBe(true);
+ });
+
+
+ it('renders array input correctly', async () => {
+ const schema = {
+ properties: {
+ hobbies: {
+ type: 'array',
+ title: 'Hobbies',
+ items: { type: 'string' },
+ },
+ },
+ };
+
+ const form = await mountComponent({ schema });
+ const hobbies = form.getFormElement('hobbies');
+ expect(hobbies).toBeDefined();
+ expect(hobbies.value).toBeUndefined();
+ await hobbies.updateData(['Reading']);
+ expect(form.resolved.hobbies).toEqual(['Reading']);
+ });
+
+ it('renders object input correctly', async () => {
+ const schema = {
+ properties: {
+ address: {
+ type: 'object',
+ title: 'Address',
+ properties: {
+ street: { type: 'string', title: 'Street' },
+ city: { type: 'string', title: 'City' },
+ zip: { type: 'string', title: 'ZIP Code' },
+ },
+ },
+ },
+ };
+
+ const form = await mountComponent({ schema });
+ const streetInput = form.getFormElement(['address', 'street']);
+ expect(streetInput).toBeDefined();
+ await streetInput.updateData('123 Main St');
+ expect(form.resolved.address.street).toBe('123 Main St');
+ });
+
+ it('renders enum input correctly', async () => {
+ const schema = {
+ properties: {
+ status: {
+ type: 'string',
+ title: 'Status',
+ enum: ['Active', 'Inactive', 'Pending'],
+ },
+ },
+ };
+
+ const form = await mountComponent({ schema });
+ const input = form.getFormElement('status');
+ expect(input).toBeDefined();
+ await input.updateData('Inactive');
+ expect(form.resolved.status).toBe('Inactive');
+ });
+
+ it("renders tables correctly", async () => {
+ const schema = {
+ properties: {
+ users: {
+ type: 'array',
+ title: 'Users',
+ items: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', title: 'Name' },
+ age: { type: 'number', title: 'Age' },
+ },
+ },
+ },
+ },
+ };
+
+ const form = await mountComponent({
+ schema,
+ renderTable: (name, metadata, path) => {
+ if (name !== "Electrodes") return new SimpleTable(metadata);
+ else return true
+ },
+ });
+
+ const users = form.getFormElement('users');
+ expect(users).toBeDefined();
+ await users.addRow();
+ expect(users.data).toHaveLength(1);
+
+ const row = users.getRow(0)
+ const newData = { name: 'John Doe', age: 30 }
+ await Promise.all(Object.entries(newData).map(([key, value]) => {
+ const cell = row.find(cell => cell.simpleTableInfo.col === key)
+ return cell.setInput(value)
+ }))
+
+ await sleep(100) // Wait for updates to register on the table
+
+ expect(form.resolved.users).toEqual([{ name: 'John Doe', age: 30 }]);
+ })
+
+ it('validates form correctly', async () => {
+ const schema = {
+ properties: {
+ name: { type: 'string', title: 'Name' },
+ },
+ required: ['name'],
+ };
+
+ const form = await mountComponent({ schema });
+ const nameInput = form.getFormElement('name');
+ expect(nameInput).toBeDefined();
+
+ let errors = false;
+ await form.validate().catch(() => errors = true);
+ expect(errors).toBe(true);
+
+ await nameInput.updateData('John Doe');
+
+ await form.validate()
+ .then(() => errors = false)
+ .catch(() => errors = true);
+
+ expect(errors).toBe(false);
+ expect(form.resolved.name).toBe('John Doe');
+
+ });
+
+ // Pop-up inputs and forms work correctly
+ it('creates a pop-up that submits properly', async () => {
+
+ // Create the form
+ const form = new JSONSchemaForm({
+ schema: {
+ "type": "object",
+ "required": ["keywords", "experimenter"],
+ "properties": {
+ "keywords": NWBFileSchemaProperties.keywords,
+ "experimenter": NWBFileSchemaProperties.experimenter
+ }
+ },
+ })
+
+ document.body.append(form)
+
+ await form.rendered
+
+ // Validate that the results are incorrect
+ let errors = false
+ await form.validate().catch(() => errors = true)
+ expect(errors).toBe(true) // Is invalid
+
+
+ // Validate that changes to experimenter are valid
+ const experimenterInput = form.getFormElement(['experimenter'])
+ const experimenterButton = experimenterInput.shadowRoot.querySelector('nwb-button')
+ const experimenterModal = experimenterButton.onClick()
+ const experimenterNestedElement = experimenterModal.children[0].children[0]
+ const experimenterSubmitButton = experimenterModal.footer
+
+ await sleep(1000)
+
+ let modalFailed
+ try {
+ await experimenterSubmitButton.onClick()
+ modalFailed = false
+ } catch (e) {
+ modalFailed = true
+ }
+
+ expect(modalFailed).toBe(true) // Is invalid
+
+ await experimenterNestedElement.updateData(['first_name'], 'Garrett')
+ await experimenterNestedElement.updateData(['last_name'], 'Flynn')
+
+ experimenterNestedElement.requestUpdate()
+
+ await experimenterNestedElement.rendered
+
+ try {
+ await experimenterSubmitButton.onClick()
+ modalFailed = false
+ } catch (e) {
+ modalFailed = true
+ }
+
+ expect(modalFailed).toBe(false) // Is valid
+
+ // Validate that changes to keywords are valid
+ const keywordsInput = form.getFormElement(['keywords'])
+ const input = keywordsInput.shadowRoot.querySelector('input')
+ const submitButton = keywordsInput.shadowRoot.querySelector('nwb-button')
+ const list = keywordsInput.shadowRoot.querySelector('nwb-list')
+ expect(list.items.length).toBe(0) // No items
+
+ input.value = 'test'
+ await submitButton.onClick()
+
+ expect(list.items.length).toBe(1) // Has item
+ expect(input.value).toBe('') // Input is cleared
+
+ // Validate that the new structure is correct
+ const hasErrors = await form.validate(form.results).then(res => false).catch(() => true)
+
+ expect(hasErrors).toBe(false) // Is valid
+ })
+
+ // TODO: Convert an integration
+ it('triggers and resolves inter-table updates correctly', async () => {
+
+ const results = {
+ Ecephys: { // NOTE: This layer is required to place the properties at the right level for the hardcoded validation function
+ ElectrodeGroup: [{ name: 's1' }],
+ Electrodes: [{ group_name: 's1' }]
+ }
+ }
+
+ const schema = {
+ properties: {
+ Ecephys: {
+ properties: {
+ ElectrodeGroup: {
+ type: "array",
+ items: {
+ required: ["name"],
+ properties: {
+ name: {
+ type: "string"
+ },
+ },
+ type: "object",
+ },
+ },
+ Electrodes: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ group_name: {
+ type: "string",
+ },
+ },
+ }
+ },
+ }
+ }
+ }
+ }
+
+
+
+ // Add invalid electrode
+ const randomStringId = Math.random().toString(36).substring(7)
+ results.Ecephys.Electrodes.push({ group_name: randomStringId })
+
+ // Create the form
+ const form = new JSONSchemaForm({
+ schema,
+ results,
+ validateOnChange,
+ renderTable: (name, metadata, path) => {
+ if (name !== "Electrodes") return new SimpleTable(metadata);
+ else return true
+ },
+ })
+
+ document.body.append(form)
+
+ await form.rendered
+
+ // Validate that the results are incorrect
+ const errors = await form.validate().catch(() => true).catch((e) => e)
+ expect(errors).toBe(true) // Is invalid
+
+ // Update the table with the missing electrode group
+ const table = form.getFormElement(['Ecephys', 'ElectrodeGroup']) // This is a SimpleTable where rows can be added
+ const idx = await table.addRow()
+
+ const row = table.getRow(idx)
+
+ const baseRow = table.getRow(0)
+ row.forEach((cell, i) => {
+ if (cell.simpleTableInfo.col === 'name') cell.setInput(randomStringId) // Set name to random string id
+ else cell.setInput(baseRow[i].value) // Otherwise carry over info
+ })
+
+ await sleep(1000) // Wait for the ElectrodeGroup table to update properly
+ form.requestUpdate() // Re-render the form to update the Electrodes table
+
+ await form.rendered // Wait for the form to re-render and validate properly
+
+ // Validate that the new structure is correct
+ const hasErrors = await form.validate().then(() => false).catch((e) => true)
+
+ expect(hasErrors).toBe(false) // Is valid
+
+ })
+
+
+
+});
diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts
index 66b8c91d98..e3bf6e764f 100644
--- a/tests/metadata.test.ts
+++ b/tests/metadata.test.ts
@@ -8,18 +8,10 @@ import { createMockGlobalState } from './utils'
import { Validator } from 'jsonschema'
import { updateResultsFromSubjects } from '../src/electron/frontend/utils/data'
-import { JSONSchemaForm } from '../src/electron/frontend/core/components/JSONSchemaForm'
-import { validateOnChange } from "../src/electron/frontend/core/validation/index.js";
-import { SimpleTable } from '../src/electron/frontend/core/components/SimpleTable'
-
-function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
-}
var validator = new Validator();
-const NWBFileSchemaProperties = baseMetadataSchema.properties.NWBFile.properties
describe('metadata is specified correctly', () => {
@@ -45,174 +37,3 @@ test('removing all existing sessions will maintain the related subject entry on
updateResultsFromSubjects(results, subjects)
expect(Object.keys(results)).toEqual(Object.keys(copy))
})
-
-const popupSchemas = {
- "type": "object",
- "required": ["keywords", "experimenter"],
- "properties": {
- "keywords": NWBFileSchemaProperties.keywords,
- "experimenter": NWBFileSchemaProperties.experimenter
- }
-}
-
-// Pop-up inputs and forms work correctly
-test('pop-up inputs work correctly', async () => {
-
- const results = {}
-
- // Create the form
- const form = new JSONSchemaForm({ schema: popupSchemas, results })
-
- document.body.append(form)
-
- await form.rendered
-
- // Validate that the results are incorrect
- let errors = false
- await form.validate().catch(() => errors = true)
- expect(errors).toBe(true) // Is invalid
-
-
- // Validate that changes to experimenter are valid
- const experimenterInput = form.getFormElement(['experimenter'])
- const experimenterButton = experimenterInput.shadowRoot.querySelector('nwb-button')
- const experimenterModal = experimenterButton.onClick()
- const experimenterNestedElement = experimenterModal.children[0].children[0]
- const experimenterSubmitButton = experimenterModal.footer
-
- await sleep(1000)
-
- let modalFailed
- try {
- await experimenterSubmitButton.onClick()
- modalFailed = false
- } catch (e) {
- modalFailed = true
- }
-
- expect(modalFailed).toBe(true) // Is invalid
-
- experimenterNestedElement.updateData(['first_name'], 'Garrett')
- experimenterNestedElement.updateData(['last_name'], 'Flynn')
-
- experimenterNestedElement.requestUpdate()
-
- await experimenterNestedElement.rendered
-
- try {
- await experimenterSubmitButton.onClick()
- modalFailed = false
- } catch (e) {
- modalFailed = true
- }
-
- expect(modalFailed).toBe(false) // Is valid
-
- // Validate that changes to keywords are valid
- const keywordsInput = form.getFormElement(['keywords'])
- const input = keywordsInput.shadowRoot.querySelector('input')
- const submitButton = keywordsInput.shadowRoot.querySelector('nwb-button')
- const list = keywordsInput.shadowRoot.querySelector('nwb-list')
- expect(list.items.length).toBe(0) // No items
-
- input.value = 'test'
- await submitButton.onClick()
-
- expect(list.items.length).toBe(1) // Has item
- expect(input.value).toBe('') // Input is cleared
-
- // Validate that the new structure is correct
- const hasErrors = await form.validate(form.results).then(res => false).catch(() => true)
-
- expect(hasErrors).toBe(false) // Is valid
-})
-
-// TODO: Convert an integration
-test('inter-table updates are triggered', async () => {
-
- const results = {
- Ecephys: { // NOTE: This layer is required to place the properties at the right level for the hardcoded validation function
- ElectrodeGroup: [{ name: 's1' }],
- Electrodes: [{ group_name: 's1' }]
- }
- }
-
- const schema = {
- properties: {
- Ecephys: {
- properties: {
- ElectrodeGroup: {
- type: "array",
- items: {
- required: ["name"],
- properties: {
- name: {
- type: "string"
- },
- },
- type: "object",
- },
- },
- Electrodes: {
- type: "array",
- items: {
- type: "object",
- properties: {
- group_name: {
- type: "string",
- },
- },
- }
- },
- }
- }
- }
- }
-
-
-
- // Add invalid electrode
- const randomStringId = Math.random().toString(36).substring(7)
- results.Ecephys.Electrodes.push({ group_name: randomStringId })
-
- // Create the form
- const form = new JSONSchemaForm({
- schema,
- results,
- validateOnChange,
- renderTable: (name, metadata, path) => {
- if (name !== "Electrodes") return new SimpleTable(metadata);
- else return true
- },
- })
-
- document.body.append(form)
-
- await form.rendered
-
- // Validate that the results are incorrect
- const errors = await form.validate().catch(() => true).catch((e) => e)
- expect(errors).toBe(true) // Is invalid
-
- // Update the table with the missing electrode group
- const table = form.getFormElement(['Ecephys', 'ElectrodeGroup']) // This is a SimpleTable where rows can be added
- const idx = await table.addRow()
-
- const row = table.getRow(idx)
-
- const baseRow = table.getRow(0)
- row.forEach((cell, i) => {
- if (cell.simpleTableInfo.col === 'name') cell.setInput(randomStringId) // Set name to random string id
- else cell.setInput(baseRow[i].value) // Otherwise carry over info
- })
-
- await sleep(1000) // Wait for the ElectrodeGroup table to update properly
- form.requestUpdate() // Re-render the form to update the Electrodes table
-
- await form.rendered // Wait for the form to re-render and validate properly
-
- // Validate that the new structure is correct
- const hasErrors = await form.validate().then(() => false).catch((e) => true)
-
- expect(hasErrors).toBe(false) // Is valid
-})
diff --git a/tests/utils.test.ts b/tests/utils.test.ts
index a05d2fd6f2..c53dba8f8c 100644
--- a/tests/utils.test.ts
+++ b/tests/utils.test.ts
@@ -372,9 +372,9 @@ describe('Randomization Utilities', () => {
});
it('should return a string of expected length', () => {
- const result = random.getRandomString();
- expect(result.length).toBeGreaterThanOrEqual(5); // Length might vary slightly due to the nature of random
- expect(result.length).toBeLessThanOrEqual(11); // Usually the length is around 7-10 characters
+ const len = 10;
+ const result = random.getRandomString(len);
+ expect(result.length).toEqual(len); // Length is always the specified length
});
it('should return a different string each time it is called', () => {
diff --git a/vite.config.js b/vite.config.js
index e79e9e8739..5667e299b7 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -27,13 +27,6 @@ export default defineConfig({
"src/electron/frontend/utils/electron.ts",
"src/electron/frontend/utils/auto-update.ts",
- // High-Level App Configuration
- "src/electron/frontend/core/index.ts",
- "src/electron/frontend/core/pages.js",
- "src/electron/frontend/core/dependencies.js",
- "src/electron/frontend/core/globals.js",
- "src/electron/frontend/core/errors.ts",
-
// Server Communication
"src/electron/frontend/core/server",
"src/electron/frontend/utils/run.ts",
@@ -42,10 +35,30 @@ export default defineConfig({
// Pure Native Rendering Interaction
"src/electron/frontend/utils/table.ts",
+ // High-Level App Configuration
+ "src/electron/frontend/core/index.ts",
+ "src/electron/frontend/core/pages.js",
+ "src/electron/frontend/core/notifications.ts",
+ "src/electron/frontend/core/lotties.ts",
+ "src/electron/frontend/core/globals.js",
+ "src/electron/frontend/core/errors.ts",
+
+ // Dashboard-Related Components
+ "src/electron/frontend/core/components/Dashboard.js",
+ "src/electron/frontend/core/components/Main.js",
+ "src/electron/frontend/core/components/Footer.js",
+ "src/electron/frontend/core/components/NavigationSidebar.js",
+ "src/electron/frontend/core/components/StatusBar.js",
+ "src/electron/frontend/core/components/sidebar.js",
+
+ // Just rendering
+ "src/electron/frontend/core/components/CodeBlock.js",
+
// Unclear how to test
"src/electron/frontend/utils/popups.ts",
"src/electron/frontend/utils/download.ts",
"src/electron/frontend/utils/upload.ts",
+ "src/electron/frontend/core/components/FileSystemSelector.js", // Uses Electron dialog
],
},
},