diff --git a/.github/workflows/example_data_cache.yml b/.github/workflows/example_data_cache.yml index e27070078c..5a2b6d9b82 100644 --- a/.github/workflows/example_data_cache.yml +++ b/.github/workflows/example_data_cache.yml @@ -21,7 +21,7 @@ jobs: fail-fast: false matrix: python-version: ["3.12"] - os: [ubuntu-latest, macos-latest, macos-13] #, windows-latest] + os: [ubuntu-latest, macos-latest, macos-13, windows-latest] steps: diff --git a/.github/workflows/testing_pipelines.yml b/.github/workflows/testing_pipelines.yml index 1a4ca41d6f..d2eb48e9d9 100644 --- a/.github/workflows/testing_pipelines.yml +++ b/.github/workflows/testing_pipelines.yml @@ -26,8 +26,8 @@ jobs: - os: macos-13 # Mac x64 runner label: environments/environment-MAC-intel.yml -# - os: windows-latest -# label: environments/environment-Windows.yml + - os: windows-latest + label: environments/environment-Windows.yml steps: @@ -95,33 +95,18 @@ jobs: path: ./behavior_testing_data key: behavior-datasets-${{ matrix.os }}-${{ steps.behavior.outputs.HASH_behavior_DATASET }} - - name: Save working directory to environment file - run: echo "GIN_DATA_DIR=$(pwd)" >> .env - if: runner.os != 'Windows' - - - name: Save working directory to environment file (Windows) - run: echo GIN_DATA_DIR=%cd% >> .env - shell: bash - if: runner.os == 'Windows' - - # Display environment file for debugging - - name: Print environment file - run: cat .env - if: runner.os != 'Windows' - - - name: Print environment file - run: type .env - shell: bash - if: runner.os == 'Windows' - # Run pipeline tests - if: matrix.os != 'ubuntu-latest' name: Run tests run: npm run test:pipelines + env: + GIN_DATA_DIRECTORY: ${{ github.workspace }} - if: matrix.os == 'ubuntu-latest' name: Run tests with xvfb run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:pipelines + env: + GIN_DATA_DIRECTORY: ${{ github.workspace }} - name: Archive Pipeline Test Screenshots if: always() diff --git a/package.json b/package.json index 81aea6245c..64f15c4ec8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "test:app": "vitest run --exclude \"**/pipelines.test.ts\"", "test:tutorial": "vitest tutorial", "test:pipelines": "vitest pipelines", + "test:progress": "vitest progress", + "test:metadata": "vitest metadata", "test:server": "pytest src/pyflask/tests/ -s -vv", "wait5s": "node -e \"setTimeout(() => process.exit(0),5000)\"", "test:executable": "concurrently -n EXE,TEST --kill-others --success first \"node tests/testPyinstallerExecutable.js --port 3434 --forever\" \"npm run wait5s && pytest src/pyflask/tests/ -s --target http://localhost:3434\"", diff --git a/src/electron/frontend/core/components/pages/guided-mode/data/GuidedPathExpansion.js b/src/electron/frontend/core/components/pages/guided-mode/data/GuidedPathExpansion.js index 20e4e07cc6..af89aea7da 100644 --- a/src/electron/frontend/core/components/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/electron/frontend/core/components/pages/guided-mode/data/GuidedPathExpansion.js @@ -381,76 +381,71 @@ export class GuidedPathExpansionPage extends Page { const interfaceName = parentPath.slice(-1)[0]; - if (fs) { - const baseDir = form.getFormElement([...parentPath, "base_directory"]); - if (name === "format_string_path") { - if (value && baseDir && !baseDir.value) { - return [ - { - message: html`A base directory must be provided to locate your files.`, - type: "error", - }, - ]; - } + const baseDir = form.getFormElement([...parentPath, "base_directory"]); + if (name === "format_string_path") { + if (value && baseDir && !baseDir.value) { + return [ + { + message: html`A base directory must be provided to locate your files.`, + type: "error", + }, + ]; + } - const base_directory = [...parentPath, "base_directory"].reduce( - (acc, key) => acc[key], - this.form.resolved - ); + const base_directory = [...parentPath, "base_directory"].reduce( + (acc, key) => acc[key], + this.form.resolved + ); - if (!base_directory) return true; // Do not calculate if base is not found + if (!base_directory) return true; // Do not calculate if base is not found - const entry = { base_directory }; + const entry = { base_directory }; - if (value.split(".").length > 1) entry.file_path = value; - else entry.folder_path = value; + if (value.split(".").length > 1) entry.file_path = value; + else entry.folder_path = value; - const results = await run( - `neuroconv/locate`, - { [interfaceName]: entry }, - { swal: false } - ).catch((error) => { + const results = await run(`neuroconv/locate`, { [interfaceName]: entry }, { swal: false }).catch( + (error) => { this.notify(error.message, "error"); throw error; - }); + } + ); - const resolved = []; + const resolved = []; - for (let sub in results) { - for (let ses in results[sub]) { - const source_data = results[sub][ses].source_data[interfaceName]; - const path = source_data.file_path ?? source_data.folder_path; - resolved.push(path.slice(base_directory.length + 1)); - } + for (let sub in results) { + for (let ses in results[sub]) { + const source_data = results[sub][ses].source_data[interfaceName]; + const path = source_data.file_path ?? source_data.folder_path; + resolved.push(path.slice(base_directory.length + 1)); } + } - if (resolved.length === 0) - return [ - { - message: html`No source files found using the provided information.`, - type: "warning", - }, - ]; - + if (resolved.length === 0) return [ { - message: html`

- Source Files Found for - ${interfaceName} -

- ${base_directory} - ${new List({ - items: resolved.map((path) => { - return { value: path }; - }), - editable: false, - })}`, - type: "info", + message: html`No source files found using the provided information.`, + type: "warning", }, ]; - } + + return [ + { + message: html`

+ Source Files Found for ${interfaceName} +

+ ${base_directory} + ${new List({ + items: resolved.map((path) => { + return { value: path }; + }), + editable: false, + })}`, + type: "info", + }, + ]; } }, })); diff --git a/src/electron/frontend/core/components/pages/settings/SettingsPage.js b/src/electron/frontend/core/components/pages/settings/SettingsPage.js index 732ba905de..d5ea234eab 100644 --- a/src/electron/frontend/core/components/pages/settings/SettingsPage.js +++ b/src/electron/frontend/core/components/pages/settings/SettingsPage.js @@ -15,15 +15,9 @@ import { merge, setUndefinedIfNotDeclared } from "../utils"; import { notyf } from "../../../dependencies.js"; import { homeDirectory, testDataFolderPath } from "../../../globals.js"; -import { - SERVER_FILE_PATH, - electron, - path, - port, - fs, - onUpdateAvailable, - onUpdateProgress, -} from "../../../../utils/electron.js"; +import { SERVER_FILE_PATH, electron, path, port, fs } from "../../../../utils/electron.js"; + +import { onUpdateAvailable, onUpdateProgress } from "../../../../utils/auto-update.js"; import saveSVG from "../../../../assets/icons/save.svg?raw"; import folderSVG from "../../../../assets/icons/folder_open.svg?raw"; diff --git a/src/electron/frontend/core/globals.js b/src/electron/frontend/core/globals.js index fa3fc0e26e..0ca85c1a23 100644 --- a/src/electron/frontend/core/globals.js +++ b/src/electron/frontend/core/globals.js @@ -1,9 +1,11 @@ -import { app, path, crypto, isElectron } from "../utils/electron.js"; +import { os, path, crypto, isElectron, isTestEnvironment } from "../utils/electron.js"; import paths from "../../../paths.config.json" assert { type: "json" }; import supportedInterfaces from "../../../supported_interfaces.json" assert { type: "json" }; +export { isTestEnvironment }; + export const joinPath = (...args) => (path ? path.join(...args) : args.filter((str) => str).join("/")); export let runOnLoad = (fn) => { @@ -17,24 +19,27 @@ export const reloadPageToHome = () => { }; // Clear all query params // Filesystem Management -const root = globalThis?.process?.env?.VITEST ? joinPath(paths.root, ".test") : paths.root; -export const homeDirectory = app?.getPath("home") ?? ""; -export const appDirectory = homeDirectory ? joinPath(homeDirectory, root) : ""; -export const guidedProgressFilePath = appDirectory ? joinPath(appDirectory, ...paths.subfolders.progress) : ""; +const root = isTestEnvironment ? joinPath(paths.root, ".test") : paths.root; + +export const homeDirectory = os ? os.homedir() : "/"; + +export const appDirectory = joinPath(homeDirectory, root); + +export const guidedProgressFilePath = joinPath(appDirectory, ...paths.subfolders.progress); -export const previewSaveFolderPath = appDirectory ? joinPath(appDirectory, ...paths.subfolders.preview) : ""; -export const conversionSaveFolderPath = appDirectory ? joinPath(appDirectory, ...paths.subfolders.conversions) : ""; +export const previewSaveFolderPath = joinPath(appDirectory, ...paths.subfolders.preview); +export const conversionSaveFolderPath = joinPath(appDirectory, ...paths.subfolders.conversions); -export const testDataFolderPath = appDirectory ? joinPath(appDirectory, ...paths.subfolders.testdata) : ""; +export const testDataFolderPath = joinPath(appDirectory, ...paths.subfolders.testdata); // Encryption const IV_LENGTH = 16; const KEY_LENGTH = 32; -export const ENCRYPTION_KEY = appDirectory +export const ENCRYPTION_KEY = isElectron ? Buffer.concat([Buffer.from(appDirectory), Buffer.alloc(KEY_LENGTH)], KEY_LENGTH) - : null; + : ""; -export const ENCRYPTION_IV = crypto ? crypto.randomBytes(IV_LENGTH) : null; +export const ENCRYPTION_IV = isElectron ? crypto.randomBytes(IV_LENGTH) : ""; // Storybook export const isStorybook = window.location.href.includes("iframe.html"); diff --git a/src/electron/frontend/core/index.ts b/src/electron/frontend/core/index.ts index 08415c3e10..c7d0a48973 100644 --- a/src/electron/frontend/core/index.ts +++ b/src/electron/frontend/core/index.ts @@ -1,5 +1,7 @@ import "./pages.js" import { isElectron, electron } from '../utils/electron.js' +import { isTestEnvironment } from './globals.js' + const { ipcRenderer } = electron; import { Dashboard } from './components/Dashboard.js' @@ -55,8 +57,6 @@ async function isOnline() { statusBar.items[1].status = true - const isTestEnvironment = globalThis?.process?.env?.VITEST - if (isTestEnvironment) return notyf.open({ diff --git a/src/electron/frontend/core/progress/index.js b/src/electron/frontend/core/progress/index.js index 761ab85348..aede8554b0 100644 --- a/src/electron/frontend/core/progress/index.js +++ b/src/electron/frontend/core/progress/index.js @@ -8,6 +8,7 @@ import { ENCRYPTION_KEY, ENCRYPTION_IV, } from "../globals.js"; + import { fs, crypto } from "../../utils/electron.js"; import { joinPath, runOnLoad } from "../globals"; @@ -87,8 +88,7 @@ class GlobalAppConfig { save() { const encoded = encodeObject(this.data); - if (fs) fs.writeFileSync(this.path, JSON.stringify(encoded, null, 2)); - else localStorage.setItem(this.path, JSON.stringify(encoded)); + fs.writeFileSync(this.path, JSON.stringify(encoded, null, 2)); } } @@ -115,7 +115,7 @@ export const save = (page, overrides = {}) => { }; export const getEntries = () => { - if (fs && !fs.existsSync(guidedProgressFilePath)) fs.mkdirSync(guidedProgressFilePath, { recursive: true }); //Check if progress folder exists. If not, create it. + if (!fs.existsSync(guidedProgressFilePath)) fs.mkdirSync(guidedProgressFilePath, { recursive: true }); //Check if progress folder exists. If not, create it. const progressFiles = fs ? fs.readdirSync(guidedProgressFilePath) : Object.keys(localStorage); return progressFiles.filter((path) => path.slice(-5) === ".json"); }; diff --git a/src/electron/frontend/core/progress/operations.js b/src/electron/frontend/core/progress/operations.js index 8d60e238ef..3154e349d8 100644 --- a/src/electron/frontend/core/progress/operations.js +++ b/src/electron/frontend/core/progress/operations.js @@ -7,17 +7,13 @@ export const remove = (name) => { const progressFilePathToDelete = joinPath(guidedProgressFilePath, name + ".json"); //delete the progress file - if (fs) { - if (fs.existsSync(progressFilePathToDelete)) fs.unlinkSync(progressFilePathToDelete); - } else localStorage.removeItem(progressFilePathToDelete); + if (fs.existsSync(progressFilePathToDelete)) fs.unlinkSync(progressFilePathToDelete); - if (fs) { - // delete default preview location - fs.rmSync(joinPath(previewSaveFolderPath, name), { recursive: true, force: true }); + // delete default preview location + fs.rmSync(joinPath(previewSaveFolderPath, name), { recursive: true, force: true }); - // delete default conversion location - fs.rmSync(joinPath(conversionSaveFolderPath, name), { recursive: true, force: true }); - } + // delete default conversion location + fs.rmSync(joinPath(conversionSaveFolderPath, name), { recursive: true, force: true }); return true; }; diff --git a/src/electron/frontend/core/progress/update.js b/src/electron/frontend/core/progress/update.js index c621427b67..f07ea80b19 100644 --- a/src/electron/frontend/core/progress/update.js +++ b/src/electron/frontend/core/progress/update.js @@ -15,11 +15,7 @@ export const rename = (newDatasetName, previousDatasetName) => { // update old progress file with new dataset name const oldProgressFilePath = `${guidedProgressFilePath}/${previousDatasetName}.json`; const newProgressFilePath = `${guidedProgressFilePath}/${newDatasetName}.json`; - if (fs) fs.renameSync(oldProgressFilePath, newProgressFilePath); - else { - localStorage.setItem(newProgressFilePath, localStorage.getItem(oldProgressFilePath)); - localStorage.removeItem(oldProgressFilePath); - } + fs.renameSync(oldProgressFilePath, newProgressFilePath); } else throw new Error("No previous project name provided"); }; @@ -56,9 +52,9 @@ export const updateFile = (projectName, callback) => { var guidedFilePath = joinPath(guidedProgressFilePath, projectName + ".json"); + console.log(guidedProgressFilePath); + // Save the file through the available mechanisms - if (fs) { - if (!fs.existsSync(guidedProgressFilePath)) fs.mkdirSync(guidedProgressFilePath, { recursive: true }); //create progress folder if one does not exist - fs.writeFileSync(guidedFilePath, JSON.stringify(data, null, 2)); - } else localStorage.setItem(guidedFilePath, JSON.stringify(data)); + if (!fs.existsSync(guidedProgressFilePath)) fs.mkdirSync(guidedProgressFilePath, { recursive: true }); //create progress folder if one does not exist + fs.writeFileSync(guidedFilePath, JSON.stringify(data, null, 2)); }; diff --git a/src/electron/frontend/core/server/index.ts b/src/electron/frontend/core/server/index.ts index e8b69b2292..aecc5b3f18 100644 --- a/src/electron/frontend/core/server/index.ts +++ b/src/electron/frontend/core/server/index.ts @@ -1,6 +1,8 @@ -import { isElectron, electron, app, port } from '../../utils/electron.js' +import { isElectron, electron, app } from '../../utils/electron.js' const { ipcRenderer } = electron; +import { isTestEnvironment } from '../globals.js' + import { notyf, } from '../dependencies.js' @@ -35,7 +37,6 @@ export async function pythonServerOpened() { if (openPythonStatusNotyf) notyf.dismiss(openPythonStatusNotyf) - const isTestEnvironment = globalThis?.process?.env?.VITEST if (isTestEnvironment) return openPythonStatusNotyf = notyf.open({ diff --git a/src/electron/frontend/utils/auto-update.js b/src/electron/frontend/utils/auto-update.js new file mode 100644 index 0000000000..4b4452ebde --- /dev/null +++ b/src/electron/frontend/utils/auto-update.js @@ -0,0 +1,25 @@ +let updateAvailable = false; +const updateAvailableCallbacks = []; +export const onUpdateAvailable = (callback) => { + if (updateAvailable) callback(updateAvailable); + else updateAvailableCallbacks.push(callback); +}; + +let updateProgress = null; + +const updateProgressCallbacks = []; +export const onUpdateProgress = (callback) => { + if (updateProgress) callback(updateProgress); + else updateProgressCallbacks.push(callback); +}; + +export const registerUpdateProgress = (info) => { + updateProgress = info; + updateProgressCallbacks.forEach((cb) => cb(info)); +}; + +export const registerUpdate = (info) => { + updateAvailable = info; + document.body.setAttribute("data-update-available", JSON.stringify(info)); + updateAvailableCallbacks.forEach((cb) => cb(info)); +}; diff --git a/src/electron/frontend/utils/electron.js b/src/electron/frontend/utils/electron.js index e8c6f2e4a0..8531cfdb0a 100644 --- a/src/electron/frontend/utils/electron.js +++ b/src/electron/frontend/utils/electron.js @@ -1,86 +1,52 @@ +import { registerUpdate, registerUpdateProgress } from "./auto-update.js"; import { updateURLParams } from "./url.js"; -var userAgent = navigator.userAgent.toLowerCase(); -export const isElectron = userAgent.indexOf(" electron/") > -1; - -export let port = 4242; -export let SERVER_FILE_PATH = ""; -export const electron = globalThis.electron ?? {}; // ipcRenderer, remote, shell, etc. -export let fs = null; -export let os = null; -export let remote = {}; -export let app = null; -export let path = null; -export let log = null; -export let crypto = null; +export const isTestEnvironment = globalThis?.process?.env?.VITEST; -let updateAvailable = false; -const updateAvailableCallbacks = []; -export const onUpdateAvailable = (callback) => { - if (updateAvailable) callback(updateAvailable); - else updateAvailableCallbacks.push(callback); -}; +const userAgent = navigator.userAgent.toLowerCase(); +export const isElectron = userAgent.indexOf(" electron/") > -1; -let updateProgress = null; +const hasNodeAccess = isElectron || isTestEnvironment; -const updateProgressCallbacks = []; -export const onUpdateProgress = (callback) => { - if (updateProgress) callback(updateProgress); - else updateProgressCallbacks.push(callback); -}; +export const electron = globalThis.electron ?? {}; // ipcRenderer, remote, shell, etc. -export const registerUpdateProgress = (info) => { - updateProgress = info; - updateProgressCallbacks.forEach((cb) => cb(info)); -}; +// Node Modules +export const fs = hasNodeAccess && require("fs-extra"); // File System +export const os = hasNodeAccess && require("os"); +export const crypto = hasNodeAccess && require("crypto"); +export const path = hasNodeAccess && require("path"); -const registerUpdate = (info) => { - updateAvailable = info; - document.body.setAttribute("data-update-available", JSON.stringify(info)); - updateAvailableCallbacks.forEach((cb) => cb(info)); -}; +// Remote Electron Modules +export const remote = isElectron ? require("@electron/remote") : {}; +export const app = remote.app; -// Used in tests -try { - crypto = require("crypto"); -} catch {} +// Electron Information +export const port = isElectron ? electron.ipcRenderer.sendSync("get-port") : 4242; +export const SERVER_FILE_PATH = isElectron ? electron.ipcRenderer.sendSync("get-server-file-path") : ""; +// Link the renderer to the main process if (isElectron) { - try { - fs = require("fs-extra"); // File System - os = require("os"); - crypto = require("crypto"); - remote = require("@electron/remote"); - app = remote.app; - - electron.ipcRenderer.on("fileOpened", (info, filepath) => { - updateURLParams({ file: filepath }); - const dashboard = document.querySelector("nwb-dashboard"); - const activePage = dashboard.getAttribute("activePage"); - if (activePage === "preview") dashboard.requestUpdate(); - else dashboard.setAttribute("activePage", "preview"); - }); - - ["log", "warn", "error"].forEach((method) => - electron.ipcRenderer.on(`console.${method}`, (_, ...args) => console[method](`[main-process]:`, ...args)) - ); - - electron.ipcRenderer.on(`checking-for-update`, (_, ...args) => console.log(`[Update]:`, ...args)); + electron.ipcRenderer.on("fileOpened", (info, filepath) => { + updateURLParams({ file: filepath }); + const dashboard = document.querySelector("nwb-dashboard"); + const activePage = dashboard.getAttribute("activePage"); + if (activePage === "preview") dashboard.requestUpdate(); + else dashboard.setAttribute("activePage", "preview"); + }); - electron.ipcRenderer.on(`update-available`, (_, info) => (info ? registerUpdate(info) : "")); + ["log", "warn", "error"].forEach((method) => + electron.ipcRenderer.on(`console.${method}`, (_, ...args) => console[method](`[main-process]:`, ...args)) + ); - electron.ipcRenderer.on(`update-progress`, (_, info) => registerUpdateProgress(info)); - electron.ipcRenderer.on(`update-complete`, (_, ...args) => console.log(`[Update]:`, ...args)); + console.log("User OS:", os.type(), os.platform(), "version:", os.release()); - electron.ipcRenderer.on(`update-error`, (_, ...args) => console.log(`[Update]:`, ...args)); + // Update Handling + electron.ipcRenderer.on(`checking-for-update`, (_, ...args) => console.log(`[Update]:`, ...args)); - port = electron.ipcRenderer.sendSync("get-port"); - console.log("User OS:", os.type(), os.platform(), "version:", os.release()); + electron.ipcRenderer.on(`update-available`, (_, info) => (info ? registerUpdate(info) : "")); - SERVER_FILE_PATH = electron.ipcRenderer.sendSync("get-server-file-path"); + electron.ipcRenderer.on(`update-progress`, (_, info) => registerUpdateProgress(info)); + electron.ipcRenderer.on(`update-complete`, (_, ...args) => console.log(`[Update]:`, ...args)); - path = require("path"); - } catch (error) { - console.error("Electron API access failed —", error); - } -} else console.warn("Electron API is blocked for web builds"); + electron.ipcRenderer.on(`update-error`, (_, ...args) => console.log(`[Update]:`, ...args)); +} diff --git a/tests/e2e/pipelines.test.ts b/tests/e2e/pipelines.test.ts index 3c0f41ea88..c775916bc4 100644 --- a/tests/e2e/pipelines.test.ts +++ b/tests/e2e/pipelines.test.ts @@ -15,7 +15,7 @@ import { sleep } from '../puppeteer'; // NOTE: We assume the user has put the GIN data in ~/NWB_GUIDE/test-data/GIN -const testGINPath = process.env.GIN_DATA_DIR ?? join(homedir(), paths.root, 'test-data', 'GIN') +const testGINPath = process.env.GIN_DATA_DIRECTORY ?? join(homedir(), paths.root, 'test-data', 'GIN') console.log('Using test GIN data at:', testGINPath) const pipelineDescribeFn = existsSync(testGINPath) ? describe : describe.skip diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index 0bb326f31a..4773b4d5ed 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -7,13 +7,12 @@ import baseMetadataSchema from '../src/schemas/base-metadata.schema' import { createMockGlobalState } from './utils' import { Validator } from 'jsonschema' -import { tempPropertyKey, textToArray } from '../src/electron/frontend/core/components/forms/utils' +import { textToArray } from '../src/electron/frontend/core/components/forms/utils' import { updateResultsFromSubjects } from '../src/electron/frontend/core/components/pages/guided-mode/setup/utils' 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' -import { JSONSchemaInput } from '../src/electron/frontend/core/components/JSONSchemaInput.js' function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));