From 46d0e58661ce500a27681cfab3abecbc1d5d1f1b Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 20:04:04 +0530 Subject: [PATCH 01/14] chore: f12 to open devtools in electron test runner window --- test/SpecRunner.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/SpecRunner.html b/test/SpecRunner.html index 9d910e9be..ce16c266d 100644 --- a/test/SpecRunner.html +++ b/test/SpecRunner.html @@ -207,6 +207,13 @@ }); } setupElectronBootVars(); + // F12 to toggle dev tools in Electron + document.addEventListener('keydown', function(e) { + if (e.key === 'F12') { + e.preventDefault(); + window.electronAPI.toggleDevTools(); + } + }); } From 55a79fd13aa547a42accea19bc437c82517ff9a9 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 20:26:10 +0530 Subject: [PATCH 02/14] fix: add missing electon test api injection --- src/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.html b/src/index.html index 136ec1f45..7f6159b56 100644 --- a/src/index.html +++ b/src/index.html @@ -175,6 +175,7 @@ console.warn("Phoenix is loaded in iframe, attempting to use electron APIs from window.top"); window.electronAppAPI = window.top.window.electronAppAPI; window.electronFSAPI = window.top.window.electronFSAPI; + window.electronAPI = window.top.window.electronAPI; } if(window.__TAURI__ || window.__ELECTRON__) { window.__IS_NATIVE_SHELL__ = true; From b6d257b00f5fb66a46feeafcc23827d74555f8e1 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 20:33:03 +0530 Subject: [PATCH 03/14] fix: integ tests starts running in dev mode --- src/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.html b/src/index.html index 7f6159b56..a804594e2 100644 --- a/src/index.html +++ b/src/index.html @@ -173,6 +173,7 @@ // this means that we are loaded in an iframe in the specrunner. Electron doesnt expose APIs // in its iframes, so we directly use the top most windows electron api objects for tests to run properly. console.warn("Phoenix is loaded in iframe, attempting to use electron APIs from window.top"); + window.__ELECTRON__ = true; window.electronAppAPI = window.top.window.electronAppAPI; window.electronFSAPI = window.top.window.electronFSAPI; window.electronAPI = window.top.window.electronAPI; From 65387b9a29176446e617de4f8adab9144cb8be56 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 21:20:19 +0530 Subject: [PATCH 04/14] test: test runner cli processing in electron --- test/UnitTestReporter.js | 30 +++++++++++++++++++------ test/index-dist-test.html | 46 +++++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/test/UnitTestReporter.js b/test/UnitTestReporter.js index 995b62306..e1a541e19 100644 --- a/test/UnitTestReporter.js +++ b/test/UnitTestReporter.js @@ -65,18 +65,34 @@ define(function (require, exports, module) { return ''; } + function hasCliFlag(args, flagName) { + return args.some(arg => arg === `--${flagName}` || arg.startsWith(`--${flagName}=`)); + } + function quitIfNeeded(exitStatus) { - if(!window.__TAURI__){ + const isTauri = !!window.__TAURI__; + const isElectron = !!window.electronAppAPI?.isElectron; + + if (!isTauri && !isElectron) { return; } + const WAIT_TIME_TO_COMPLETE_TEST_LOGGING_SEC = 10; console.log("Scheduled Quit in Seconds: ", WAIT_TIME_TO_COMPLETE_TEST_LOGGING_SEC); - setTimeout(()=>{ - window.__TAURI__.cli.getMatches().then(matches=>{ - if(matches && matches.args["quit-when-done"] && matches.args["quit-when-done"].occurrences) { - window.__TAURI__.process.exit(exitStatus); - } - }); + setTimeout(() => { + if (isTauri) { + window.__TAURI__.cli.getMatches().then(matches => { + if (matches && matches.args["quit-when-done"] && matches.args["quit-when-done"].occurrences) { + window.__TAURI__.process.exit(exitStatus); + } + }); + } else if (isElectron) { + window.electronAppAPI.getCliArgs().then(args => { + if (hasCliFlag(args, 'quit-when-done')) { + window.electronAppAPI.quitApp(exitStatus); + } + }); + } }, WAIT_TIME_TO_COMPLETE_TEST_LOGGING_SEC * 1000); } diff --git a/test/index-dist-test.html b/test/index-dist-test.html index b0390820b..63dbe9ba7 100644 --- a/test/index-dist-test.html +++ b/test/index-dist-test.html @@ -4,20 +4,48 @@ Starting tests... From a6ace79880f9a5aee2fa3db973308f0efcd2e919 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 21:38:08 +0530 Subject: [PATCH 05/14] chore: test framework didnt exist with -q cli flag --- test/UnitTestReporter.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/UnitTestReporter.js b/test/UnitTestReporter.js index e1a541e19..9894b543d 100644 --- a/test/UnitTestReporter.js +++ b/test/UnitTestReporter.js @@ -65,8 +65,12 @@ define(function (require, exports, module) { return ''; } - function hasCliFlag(args, flagName) { - return args.some(arg => arg === `--${flagName}` || arg.startsWith(`--${flagName}=`)); + function hasCliFlag(args, flagName, shortFlag) { + return args.some(arg => + arg === `--${flagName}` || + arg.startsWith(`--${flagName}=`) || + (shortFlag && arg === `-${shortFlag}`) + ); } function quitIfNeeded(exitStatus) { @@ -88,7 +92,7 @@ define(function (require, exports, module) { }); } else if (isElectron) { window.electronAppAPI.getCliArgs().then(args => { - if (hasCliFlag(args, 'quit-when-done')) { + if (hasCliFlag(args, 'quit-when-done', 'q')) { window.electronAppAPI.quitApp(exitStatus); } }); From 68aba25b9034047b3f45fbec0258494152f8c82a Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 22:54:54 +0530 Subject: [PATCH 06/14] fix: unit tests should work in electron as well part 1 --- test/SpecRunner.html | 4 ++++ test/spec/ExtensionInstallation-test.js | 5 +++-- test/spec/ExtensionLoader-test.js | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/test/SpecRunner.html b/test/SpecRunner.html index ce16c266d..1665f48c2 100644 --- a/test/SpecRunner.html +++ b/test/SpecRunner.html @@ -213,6 +213,10 @@ e.preventDefault(); window.electronAPI.toggleDevTools(); } + if (e.key === 'F5') { + e.preventDefault(); + location.reload(); + } }); } diff --git a/test/spec/ExtensionInstallation-test.js b/test/spec/ExtensionInstallation-test.js index 8c91e42ca..b8d2120ba 100644 --- a/test/spec/ExtensionInstallation-test.js +++ b/test/spec/ExtensionInstallation-test.js @@ -31,7 +31,8 @@ define(function (require, exports, module) { const testFilePath = SpecRunnerUtils.getTestPath("/spec/extension-test-files"); - const tempDirectory = window.__TAURI__ ? Phoenix.VFS.getTauriAssetServeDir() + "tests": SpecRunnerUtils.getTempDirectory(); + const tempDirectory = Phoenix.isNativeApp ? + Phoenix.VFS.getTauriAssetServeDir() + "tests": SpecRunnerUtils.getTempDirectory(); const extensionsRoot = tempDirectory + "/extensions"; const basicValidSrc = testFilePath + "/basic-valid-extension.zip", @@ -79,7 +80,7 @@ define(function (require, exports, module) { beforeAll(async function () { await SpecRunnerUtils.ensureExistsDirAsync(tempDirectory); - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ basicValid = tempDirectory + "/basic-valid-extension.zip"; missingNameVersion = tempDirectory + "/missing-name-version.zip"; missingNameVersion = tempDirectory + "/missing-name-version.zip"; diff --git a/test/spec/ExtensionLoader-test.js b/test/spec/ExtensionLoader-test.js index 0b06581e3..291b3efed 100644 --- a/test/spec/ExtensionLoader-test.js +++ b/test/spec/ExtensionLoader-test.js @@ -32,7 +32,7 @@ define(function (require, exports, module) { SpecRunnerUtils = require("spec/SpecRunnerUtils"); const testPathSrc = SpecRunnerUtils.getTestPath("/spec/ExtensionLoader-test-files"); - const testPath = window.__TAURI__ ? Phoenix.VFS.getTauriAssetServeDir() + "tests": SpecRunnerUtils.getTempDirectory(); + const testPath = Phoenix.isNativeApp ? Phoenix.VFS.getTauriAssetServeDir() + "tests": SpecRunnerUtils.getTempDirectory(); describe("ExtensionLoader", function () { From 5c0cf59cc63a59a207a04f5066ddd46c1083d342 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 23:04:55 +0530 Subject: [PATCH 07/14] fix: all unit tests working in electron --- test/spec/ExtensionManager-test.js | 2 +- test/spec/LowLevelFileIO-test.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/spec/ExtensionManager-test.js b/test/spec/ExtensionManager-test.js index 722982895..5322c8099 100644 --- a/test/spec/ExtensionManager-test.js +++ b/test/spec/ExtensionManager-test.js @@ -49,7 +49,7 @@ define(function (require, exports, module) { mockExtensionList = require("text!spec/ExtensionManager-test-files/mockExtensionList.json"), mockRegistry; - const testPath = window.__TAURI__ ? + const testPath = Phoenix.isNativeApp ? Phoenix.VFS.getTauriAssetServeDir() + "tests" : SpecRunnerUtils.getTempDirectory(); const testSrc = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files"); diff --git a/test/spec/LowLevelFileIO-test.js b/test/spec/LowLevelFileIO-test.js index 5fe7063fd..3ec05e1cf 100644 --- a/test/spec/LowLevelFileIO-test.js +++ b/test/spec/LowLevelFileIO-test.js @@ -797,7 +797,7 @@ define(function (require, exports, module) { describe("specialDirectories", function () { it("should have an application support directory", async function () { // these tests are here as these are absolute unchanging dir convention used by phoenix. - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ const appSupportDIR = window.fs.getTauriVirtualPath(window._tauriBootVars.appLocalDir); expect(brackets.app.getApplicationSupportDirectory().startsWith("/tauri/")).toBeTrue(); expect(brackets.app.getApplicationSupportDirectory()).toBe(appSupportDIR); @@ -807,7 +807,7 @@ define(function (require, exports, module) { }); it("should have a user documents directory", function () { // these tests are here as these are absolute unchanging dir convention used by phoenix. - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ const documentsDIR = window.fs.getTauriVirtualPath(window._tauriBootVars.documentDir); expect(brackets.app.getUserDocumentsDirectory().startsWith("/tauri/")).toBeTrue(); expect(brackets.app.getUserDocumentsDirectory()).toBe(documentsDIR); @@ -817,7 +817,7 @@ define(function (require, exports, module) { }); it("should have a user projects directory", function () { // these tests are here as these are absolute unchanging dir convention used by phoenix. - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ const documentsDIR = window.fs.getTauriVirtualPath(window._tauriBootVars.documentDir); const appName = window._tauriBootVars.appname; const userProjectsDir = `${documentsDIR}${appName}/`; @@ -829,7 +829,7 @@ define(function (require, exports, module) { }); it("should have a temp directory", function () { // these tests are here as these are absolute unchanging dir convention used by phoenix. - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ let tempDIR = window.fs.getTauriVirtualPath(window._tauriBootVars.tempDir); if(!tempDIR.endsWith("/")){ tempDIR = `${tempDIR}/`; @@ -844,7 +844,7 @@ define(function (require, exports, module) { }); it("should have extensions directory", function () { // these tests are here as these are absolute unchanging dir convention used by phoenix. - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ const appSupportDIR = window.fs.getTauriVirtualPath(window._tauriBootVars.appLocalDir); const extensionsDir = `${appSupportDIR}assets/extensions/`; expect(brackets.app.getExtensionsDirectory().startsWith("/tauri/")).toBeTrue(); @@ -855,7 +855,7 @@ define(function (require, exports, module) { }); it("should get virtual serving directory from virtual serving URL in browser", async function () { - if(window.__TAURI__){ + if(Phoenix.isNativeApp){ return; } expect(brackets.VFS.getPathForVirtualServingURL(`${window.fsServerUrl}blinker`)).toBe("/blinker"); @@ -866,7 +866,7 @@ define(function (require, exports, module) { }); it("should not get virtual serving directory from virtual serving URL in tauri", async function () { - if(!window.__TAURI__){ + if(!Phoenix.isNativeApp){ return; } expect(window.fsServerUrl).not.toBeDefined(); From 31c6708a626eb7f9467351c17fa8fc0c9c510a75 Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 30 Jan 2026 23:10:43 +0530 Subject: [PATCH 08/14] fix: test suite non clickable after test run --- test/spec/ExtensionManager-test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/spec/ExtensionManager-test.js b/test/spec/ExtensionManager-test.js index 5322c8099..2571c3328 100644 --- a/test/spec/ExtensionManager-test.js +++ b/test/spec/ExtensionManager-test.js @@ -808,6 +808,12 @@ define(function (require, exports, module) { }); }); + afterEach(function () { + // Clean up any lingering dialogs + Dialogs.cancelModalDialogIfOpen("install-extension-dialog"); + $(".modal-wrapper").remove(); + }); + it("should set flag to keep local files for new installs", async function () { var filename = "/path/to/downloaded/file.zip", file = FileSystem.getFileForPath(filename), From b711f363bee2aa6c3616a57c1cb2a2984e33e711 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 31 Jan 2026 13:04:02 +0530 Subject: [PATCH 09/14] test: electron platfrom tests --- test/UnitTestSuite.js | 1 + test/spec/Electron-platform-test.html | 46 +++ test/spec/Electron-platform-test.js | 448 ++++++++++++++++++++++++++ 3 files changed, 495 insertions(+) create mode 100644 test/spec/Electron-platform-test.html create mode 100644 test/spec/Electron-platform-test.js diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index e3f9a3e52..bba6e12ea 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -23,6 +23,7 @@ define(function (require, exports, module) { require("spec/Phoenix-platform-test"); require("spec/Tauri-platform-test"); + require("spec/Electron-platform-test"); require("spec/trust-ring-test"); require("spec/utframework-suite-test"); require("spec/Async-test"); diff --git a/test/spec/Electron-platform-test.html b/test/spec/Electron-platform-test.html new file mode 100644 index 000000000..e2122901e --- /dev/null +++ b/test/spec/Electron-platform-test.html @@ -0,0 +1,46 @@ + + + + + Test electron apis accessible + + + +sending event with electron api... + + diff --git a/test/spec/Electron-platform-test.js b/test/spec/Electron-platform-test.js new file mode 100644 index 000000000..d91e16b51 --- /dev/null +++ b/test/spec/Electron-platform-test.js @@ -0,0 +1,448 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * Original work Copyright (c) 2013 - 2021 Adobe Systems Incorporated. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global describe, it, expect, beforeEach, afterEach, awaitsFor, fs, path, jasmine, expectAsync*/ + +define(function (require, exports, module) { + if(!window.__ELECTRON__) { + return; + } + + const SpecRunnerUtils = require("spec/SpecRunnerUtils"); + + describe("unit: Electron Platform Tests", function () { + + beforeEach(async function () { + + }); + + afterEach(async function () { + + }); + + describe("asset url tests", function () { + it("Should be able to fetch files in {appLocalData}/assets folder", async function () { + const appLocalData = fs.getTauriVirtualPath(await window.electronFSAPI.appLocalDataDir()); + expect(await SpecRunnerUtils.pathExists(appLocalData, true)).toBeTrue(); + expect(appLocalData.split("/")[1]).toEql("tauri"); // should be /tauri/applocaldata/path + + // now write a test html file to the assets folder + const assetHTMLPath = `${appLocalData}/assets/a9322657236.html`; + const assetHtmlText = "Hello world random37834324"; + await SpecRunnerUtils.ensureExistsDirAsync(path.dirname(assetHTMLPath)); + await SpecRunnerUtils.createTextFileAsync(assetHTMLPath, assetHtmlText); + + const appLocalDataPlatformPath = fs.getTauriPlatformPath(assetHTMLPath); + const appLocalDataURL = window.electronAPI.convertToAssetURL(appLocalDataPlatformPath); + + const fetchedData = await ((await fetch(appLocalDataURL)).text()); + expect(fetchedData).toEqual(assetHtmlText); + + // delete test file + await SpecRunnerUtils.deletePathAsync(assetHTMLPath); + }); + + async function testAssetNotAccessibleFolder(platformPath) { + const assets = fs.getTauriVirtualPath(platformPath); + expect(assets.split("/")[1]).toEql("tauri"); // should be /tauri/applocaldata/path + + // now write a test html file to the assets folder + const assetHTMLPath = `${assets}/a9322657236.html`; + const assetHtmlText = "Hello world random37834324"; + await SpecRunnerUtils.createTextFileAsync(assetHTMLPath, assetHtmlText); + + const appLocalDataPlatformPath = fs.getTauriPlatformPath(assetHTMLPath); + const appLocalDataURL = window.electronAPI.convertToAssetURL(appLocalDataPlatformPath); + + let response; + try{ + response = await fetch(appLocalDataURL); + } catch (e) { + // Network error is expected + } + // Electron returns 403 for unauthorized paths instead of throwing + expect(!response || response.status === 403).toBeTrue(); + + // delete test file + await SpecRunnerUtils.deletePathAsync(assetHTMLPath); + } + + it("Should not be able to fetch files in documents folder", async function () { + // unfortunately for tests, this is set to appdata/testDocuments. + // we cant set this to documentDir() as in github actions, + // the user documents directory may not be accessible + await testAssetNotAccessibleFolder(window._tauriBootVars.documentDir); + }); + + it("Should not be able to fetch files in appLocalData folder", async function () { + await testAssetNotAccessibleFolder(await window.electronFSAPI.appLocalDataDir()); + }); + + it("Should NOT have electronAPI access from asset:// protocol", async function () { + // This test verifies that content loaded from asset:// URLs is sandboxed + // and does not have access to Electron APIs (matching Tauri's security posture) + const appLocalData = fs.getTauriVirtualPath(await window.electronFSAPI.appLocalDataDir()); + const securityTestPath = `${appLocalData}/assets/security-test-${Date.now()}.html`; + const SECURITY_TEST_KEY = 'ELECTRON_ASSET_SECURITY_TEST_' + Date.now(); + + // Create the security test HTML in assets folder + // This script will try to use electronAPI if available and report back + const securityTestHtml = ` +Security Test`; + + await SpecRunnerUtils.ensureExistsDirAsync(path.dirname(securityTestPath)); + await SpecRunnerUtils.createTextFileAsync(securityTestPath, securityTestHtml); + + // Get the asset:// URL for the test file + const platformPath = fs.getTauriPlatformPath(securityTestPath); + const assetURL = window.electronAPI.convertToAssetURL(platformPath); + + // Clear any previous test result + await window.electronAPI.putItem(SECURITY_TEST_KEY, null); + + // Try to open a window with the asset:// URL + let windowLabel = null; + try { + windowLabel = await window.electronAPI.createPhoenixWindow(assetURL, { + windowTitle: 'Security Test', + width: 400, + height: 300, + isExtension: true + }); + } catch (e) { + // If window creation fails for asset:// URLs, that's acceptable security behavior + console.log("Window creation blocked for asset:// URL (expected):", e.message); + } + + if (windowLabel) { + // Window was created - wait for it to load and potentially try to use APIs + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Check if the sandboxed window was able to use electronAPI + const items = await window.electronAPI.getAllItems(); + const testResult = items[SECURITY_TEST_KEY]; + + if (testResult) { + const parsed = JSON.parse(testResult); + // SECURITY CHECK: If we got a result, verify no API access was possible + // If any of these are true, it's a security vulnerability + expect(parsed.hasElectronAPI).withContext( + "SECURITY VIOLATION: asset:// window has electronAPI access" + ).toBeFalse(); + expect(parsed.hasElectronFSAPI).withContext( + "SECURITY VIOLATION: asset:// window has electronFSAPI access" + ).toBeFalse(); + expect(parsed.hasElectronAppAPI).withContext( + "SECURITY VIOLATION: asset:// window has electronAppAPI access" + ).toBeFalse(); + } + // If no result was stored, the window couldn't access APIs - test passes + + // Close the security test window by its label + try { + await window.electronAPI.closeWindowByLabel(windowLabel); + } catch (e) { + console.warn("Could not close security test window:", e); + } + } + + // Cleanup + await SpecRunnerUtils.deletePathAsync(securityTestPath); + }); + + // Unique key for inter-window communication + const ELECTRON_TEST_SIGNAL_KEY = 'ELECTRON_PLATFORM_TEST_SIGNAL'; + + function createWebView() { + return new Promise((resolve, reject)=>{ + let currentURL = new URL(location.href); + let pathParts = currentURL.pathname.split('/'); + pathParts[pathParts.length - 1] = 'spec/Electron-platform-test.html'; + currentURL.pathname = pathParts.join('/'); + + let newURL = currentURL.href; + Phoenix.app.openURLInPhoenixWindow(newURL) + .then(electronWindow =>{ + expect(electronWindow.label.startsWith("extn-")).toBeTrue(); + expect(electronWindow.isNativeWindow).toBeTrue(); + + // For Electron, we use shared storage to communicate between windows + // Poll for the signal from the child window + const pollInterval = setInterval(async () => { + try { + const items = await window.electronAPI.getAllItems(); + if (items && items[ELECTRON_TEST_SIGNAL_KEY] === electronWindow.label) { + clearInterval(pollInterval); + // Clear the signal + await window.electronAPI.putItem(ELECTRON_TEST_SIGNAL_KEY, null); + // Create a window-like object with close method + const winLabel = electronWindow.label; + resolve({ + label: winLabel, + close: async function() { + // Signal the child window to close itself + const closeKey = ELECTRON_TEST_SIGNAL_KEY + + '_CLOSE_' + winLabel; + await window.electronAPI.putItem(closeKey, true); + } + }); + } + } catch (e) { + // Ignore polling errors + } + }, 100); + + // Timeout after 10 seconds + setTimeout(() => { + clearInterval(pollInterval); + reject(new Error('Timeout waiting for child window signal')); + }, 10000); + }).catch(reject); + }); + } + + it("Should be able to spawn electron windows", async function () { + const electronWindow = await createWebView(); + await electronWindow.close(); + // Wait for window to actually close + await new Promise(resolve => setTimeout(resolve, 500)); + }); + + it("Should be able to get process ID", async function () { + const processID = await Phoenix.app.getProcessID(); + expect(processID).toEqual(jasmine.any(Number)); + }); + + const maxWindows = 25; + it(`Should be able to spawn ${maxWindows} electron windows`, async function () { + const electronWindows = []; + for(let i=0; i setTimeout(resolve, 1000)); + }, 120000); + }); + + describe("Credentials OTP API Tests", function () { + const scopeName = "testScope"; + const trustRing = window.specRunnerTestKernalModeTrust; + const TEST_TRUST_KEY_NAME = "testTrustKey"; + + function decryptCreds(creds) { + return trustRing.AESDecryptString(creds, trustRing.aesKeys.key, trustRing.aesKeys.iv); + } + + beforeEach(async function () { + // Cleanup before running tests + await window.electronAPI.deleteCredential(scopeName).catch(() => {}); + }); + + afterEach(async function () { + // Cleanup after tests + await window.electronAPI.deleteCredential(scopeName).catch(() => {}); + }); + + if(Phoenix.isTestWindowGitHubActions && Phoenix.platform === "linux"){ + // Credentials test doesn't work in GitHub actions in linux desktop as the runner cant reach key ring. + it("Should not run in github actions in linux desktop", async function () { + expect(1).toEqual(1); + }); + return; + } + + describe("Credential Storage & OTP Generation", function () { + it("Should store credentials successfully", async function () { + const randomUUID = crypto.randomUUID(); + await expectAsync( + window.electronAPI.storeCredential(scopeName, randomUUID) + ).toBeResolved(); + }); + + it("Should get credentials as encrypted string", async function () { + const randomUUID = crypto.randomUUID(); + await window.electronAPI.storeCredential(scopeName, randomUUID); + + const response = await window.electronAPI.getCredential(scopeName); + expect(response).toBeDefined(); + expect(response).not.toEqual(randomUUID); + }); + + it("Should retrieve and decrypt set credentials with kernal mode keys", async function () { + const randomUUID = crypto.randomUUID(); + await window.electronAPI.storeCredential(scopeName, randomUUID); + + const creds = await window.electronAPI.getCredential(scopeName); + expect(creds).toBeDefined(); + const decryptedString = await decryptCreds(creds); + expect(decryptedString).toEqual(randomUUID); + }); + + it("Should return an error if credentials do not exist", async function () { + const response = await window.electronAPI.getCredential(scopeName); + expect(response).toBeNull(); + }); + + it("Should delete stored credentials", async function () { + const randomUUID = crypto.randomUUID(); + await window.electronAPI.storeCredential(scopeName, randomUUID); + + // Ensure credential exists + let creds = await window.electronAPI.getCredential(scopeName); + expect(creds).toBeDefined(); + + // Delete credential + await expectAsync( + window.electronAPI.deleteCredential(scopeName) + ).toBeResolved(); + + // Ensure credential is deleted + creds = await window.electronAPI.getCredential(scopeName); + expect(creds).toBeNull(); + }); + + it("Should handle deletion of non-existent credentials gracefully", async function () { + let error; + try { + await window.electronAPI.deleteCredential(scopeName); + } catch (err) { + error = err; + } + + // The test should fail if no error was thrown + expect(error).toBeDefined(); + + // Check for OS-specific error messages + const expectedErrors = [ + "No matching entry found in secure storage", // Common error on Linux/macOS + "The specified item could not be found in the keychain", // macOS Keychain + "Element not found" // Windows Credential Manager + ]; + + const isExpectedError = expectedErrors.some(msg => error.toString().includes(msg)); + expect(isExpectedError).toBeTrue(); + }); + + it("Should overwrite existing credentials when storing with the same scope", async function () { + const oldUUID = crypto.randomUUID(); + await window.electronAPI.storeCredential(scopeName, oldUUID); + + let creds = await window.electronAPI.getCredential(scopeName); + expect(creds).toBeDefined(); + let response = await decryptCreds(creds); + expect(response).toEqual(oldUUID); + + // Store new credentials with the same scope + const newUUID = crypto.randomUUID(); + await window.electronAPI.storeCredential(scopeName, newUUID); + + creds = await window.electronAPI.getCredential(scopeName); + expect(creds).toBeDefined(); + response = await decryptCreds(creds); + expect(response).toEqual(newUUID); + }); + + // trustRing.getCredential and set tests + async function setSomeKey() { + const randomCred = crypto.randomUUID(); + await trustRing.setCredential(TEST_TRUST_KEY_NAME, randomCred); + const savedCred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); + expect(savedCred).toEqual(randomCred); + return savedCred; + } + + it("Should get and set API key in kernal mode trust ring", async function () { + await setSomeKey(); + }); + + it("Should get and set empty string API key in kernal mode trust ring", async function () { + const randomCred = ""; + await trustRing.setCredential(TEST_TRUST_KEY_NAME, randomCred); + const savedCred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); + expect(savedCred).toEqual(randomCred); + }); + + it("Should remove API key in kernal mode trust ring work as expected", async function () { + await setSomeKey(); + await trustRing.removeCredential(TEST_TRUST_KEY_NAME); + const cred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); + expect(cred).toBeNull(); + }); + + // trust key management + it("Should not be able to set trust key if one is already set", async function () { + const kv = trustRing.generateRandomKeyAndIV(); + let error; + try { + await window.electronAPI.trustWindowAesKey(kv.key, kv.iv); + } catch (err) { + error = err; + } + expect(error.toString()).toContain("Trust has already been established for this window."); + }); + + it("Should be able to remove trust key with key and iv", async function () { + await window.electronAPI.removeTrustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); + let error; + try { + await window.electronAPI.removeTrustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); + } catch (err) { + error = err; + } + expect(error.toString()).toContain("No trust association found for this window."); + // reinstate trust + await window.electronAPI.trustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); + }); + + it("Should getCredential not work without trust", async function () { + await setSomeKey(); + await window.electronAPI.removeTrustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); + let error; + try { + await trustRing.getCredential(TEST_TRUST_KEY_NAME); + } catch (err) { + error = err; + } + expect(error.toString()).toContain("Trust needs to be first established"); + // reinstate trust + await window.electronAPI.trustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); + }); + }); + }); + }); +}); From 7f72b4e1271509cea58a45d89c611cdeb724c677 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 31 Jan 2026 13:57:06 +0530 Subject: [PATCH 10/14] refactor: unified electron and tauri platfrom test working --- src/phoenix/shell.js | 40 +++ test/UnitTestSuite.js | 1 - test/spec/Electron-platform-test.html | 25 +- test/spec/Electron-platform-test.js | 448 -------------------------- test/spec/Tauri-platform-test.js | 302 +++++++++++++---- 5 files changed, 284 insertions(+), 532 deletions(-) delete mode 100644 test/spec/Electron-platform-test.js diff --git a/src/phoenix/shell.js b/src/phoenix/shell.js index f6796f512..7160a1710 100644 --- a/src/phoenix/shell.js +++ b/src/phoenix/shell.js @@ -66,6 +66,25 @@ async function _getTauriWindowLabel(prefix) { throw new Error("Could not get a free window label to create tauri window"); } +/** + * Opens a URL in a new Phoenix window. Works across all platforms (Tauri, Electron, browser). + * + * @param {string} url - The URL to open in the new window + * @param {Object} [options] - Window configuration options + * @param {string} [options.windowTitle] - Title for the window (defaults to label or URL) + * @param {boolean} [options.fullscreen] - Whether to open in fullscreen mode + * @param {boolean} [options.resizable=true] - Whether the window is resizable + * @param {number} [options.height=900] - Window height in pixels + * @param {number} [options.minHeight=600] - Minimum window height in pixels + * @param {number} [options.width=1366] - Window width in pixels + * @param {number} [options.minWidth=800] - Minimum window width in pixels + * @param {boolean} [options.acceptFirstMouse=true] - (Tauri only) Accept first mouse click + * @param {boolean} [options.preferTabs] - (Browser only) Prefer opening in a new tab + * @param {string} [options._prefixPvt] - Internal: window label prefix + * @returns {Promise<{label: string, isNativeWindow: boolean}>} Window object with `label` and `isNativeWindow` properties. + * - In Tauri/Electron: `{ label: string, isNativeWindow: true }` (Tauri returns WebviewWindow instance with these props) + * - In browser: Returns window.open() result with `isNativeWindow: false` + */ async function openURLInPhoenixWindow(url, { windowTitle, fullscreen, resizable, height, minHeight, width, minWidth, acceptFirstMouse, preferTabs, _prefixPvt = PHOENIX_EXTENSION_WINDOW_PREFIX @@ -242,6 +261,27 @@ Phoenix.app = { return window.electronAPI.focusWindow(); } }, + /** + * Closes a window by its label. Returns true if window was found and closed, false otherwise. + * @param {string} label - The window label to close + * @return {Promise} + */ + closeWindowByLabel: async function (label) { + if(!Phoenix.isNativeApp){ + throw new Error("closeWindowByLabel is not supported in browsers"); + } + if(window.__TAURI__){ + const win = window.__TAURI__.window.WebviewWindow.getByLabel(label); + if(win){ + await win.close(); + return true; + } + return false; + } else if(window.__ELECTRON__){ + return window.electronAPI.closeWindowByLabel(label); + } + return false; + }, /** * Gets the commandline argument in desktop builds and null in browser builds. * Will always return CLI of the current process only. diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index bba6e12ea..e3f9a3e52 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -23,7 +23,6 @@ define(function (require, exports, module) { require("spec/Phoenix-platform-test"); require("spec/Tauri-platform-test"); - require("spec/Electron-platform-test"); require("spec/trust-ring-test"); require("spec/utframework-suite-test"); require("spec/Async-test"); diff --git a/test/spec/Electron-platform-test.html b/test/spec/Electron-platform-test.html index e2122901e..1c24add2c 100644 --- a/test/spec/Electron-platform-test.html +++ b/test/spec/Electron-platform-test.html @@ -4,31 +4,10 @@ Test electron apis accessible Security Test`; - - await SpecRunnerUtils.ensureExistsDirAsync(path.dirname(securityTestPath)); - await SpecRunnerUtils.createTextFileAsync(securityTestPath, securityTestHtml); - - // Get the asset:// URL for the test file - const platformPath = fs.getTauriPlatformPath(securityTestPath); - const assetURL = window.electronAPI.convertToAssetURL(platformPath); - - // Clear any previous test result - await window.electronAPI.putItem(SECURITY_TEST_KEY, null); - - // Try to open a window with the asset:// URL - let windowLabel = null; - try { - windowLabel = await window.electronAPI.createPhoenixWindow(assetURL, { - windowTitle: 'Security Test', - width: 400, - height: 300, - isExtension: true - }); - } catch (e) { - // If window creation fails for asset:// URLs, that's acceptable security behavior - console.log("Window creation blocked for asset:// URL (expected):", e.message); - } - - if (windowLabel) { - // Window was created - wait for it to load and potentially try to use APIs - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Check if the sandboxed window was able to use electronAPI - const items = await window.electronAPI.getAllItems(); - const testResult = items[SECURITY_TEST_KEY]; - - if (testResult) { - const parsed = JSON.parse(testResult); - // SECURITY CHECK: If we got a result, verify no API access was possible - // If any of these are true, it's a security vulnerability - expect(parsed.hasElectronAPI).withContext( - "SECURITY VIOLATION: asset:// window has electronAPI access" - ).toBeFalse(); - expect(parsed.hasElectronFSAPI).withContext( - "SECURITY VIOLATION: asset:// window has electronFSAPI access" - ).toBeFalse(); - expect(parsed.hasElectronAppAPI).withContext( - "SECURITY VIOLATION: asset:// window has electronAppAPI access" - ).toBeFalse(); - } - // If no result was stored, the window couldn't access APIs - test passes - - // Close the security test window by its label - try { - await window.electronAPI.closeWindowByLabel(windowLabel); - } catch (e) { - console.warn("Could not close security test window:", e); - } - } - - // Cleanup - await SpecRunnerUtils.deletePathAsync(securityTestPath); - }); - - // Unique key for inter-window communication - const ELECTRON_TEST_SIGNAL_KEY = 'ELECTRON_PLATFORM_TEST_SIGNAL'; - - function createWebView() { - return new Promise((resolve, reject)=>{ - let currentURL = new URL(location.href); - let pathParts = currentURL.pathname.split('/'); - pathParts[pathParts.length - 1] = 'spec/Electron-platform-test.html'; - currentURL.pathname = pathParts.join('/'); - - let newURL = currentURL.href; - Phoenix.app.openURLInPhoenixWindow(newURL) - .then(electronWindow =>{ - expect(electronWindow.label.startsWith("extn-")).toBeTrue(); - expect(electronWindow.isNativeWindow).toBeTrue(); - - // For Electron, we use shared storage to communicate between windows - // Poll for the signal from the child window - const pollInterval = setInterval(async () => { - try { - const items = await window.electronAPI.getAllItems(); - if (items && items[ELECTRON_TEST_SIGNAL_KEY] === electronWindow.label) { - clearInterval(pollInterval); - // Clear the signal - await window.electronAPI.putItem(ELECTRON_TEST_SIGNAL_KEY, null); - // Create a window-like object with close method - const winLabel = electronWindow.label; - resolve({ - label: winLabel, - close: async function() { - // Signal the child window to close itself - const closeKey = ELECTRON_TEST_SIGNAL_KEY + - '_CLOSE_' + winLabel; - await window.electronAPI.putItem(closeKey, true); - } - }); - } - } catch (e) { - // Ignore polling errors - } - }, 100); - - // Timeout after 10 seconds - setTimeout(() => { - clearInterval(pollInterval); - reject(new Error('Timeout waiting for child window signal')); - }, 10000); - }).catch(reject); - }); - } - - it("Should be able to spawn electron windows", async function () { - const electronWindow = await createWebView(); - await electronWindow.close(); - // Wait for window to actually close - await new Promise(resolve => setTimeout(resolve, 500)); - }); - - it("Should be able to get process ID", async function () { - const processID = await Phoenix.app.getProcessID(); - expect(processID).toEqual(jasmine.any(Number)); - }); - - const maxWindows = 25; - it(`Should be able to spawn ${maxWindows} electron windows`, async function () { - const electronWindows = []; - for(let i=0; i setTimeout(resolve, 1000)); - }, 120000); - }); - - describe("Credentials OTP API Tests", function () { - const scopeName = "testScope"; - const trustRing = window.specRunnerTestKernalModeTrust; - const TEST_TRUST_KEY_NAME = "testTrustKey"; - - function decryptCreds(creds) { - return trustRing.AESDecryptString(creds, trustRing.aesKeys.key, trustRing.aesKeys.iv); - } - - beforeEach(async function () { - // Cleanup before running tests - await window.electronAPI.deleteCredential(scopeName).catch(() => {}); - }); - - afterEach(async function () { - // Cleanup after tests - await window.electronAPI.deleteCredential(scopeName).catch(() => {}); - }); - - if(Phoenix.isTestWindowGitHubActions && Phoenix.platform === "linux"){ - // Credentials test doesn't work in GitHub actions in linux desktop as the runner cant reach key ring. - it("Should not run in github actions in linux desktop", async function () { - expect(1).toEqual(1); - }); - return; - } - - describe("Credential Storage & OTP Generation", function () { - it("Should store credentials successfully", async function () { - const randomUUID = crypto.randomUUID(); - await expectAsync( - window.electronAPI.storeCredential(scopeName, randomUUID) - ).toBeResolved(); - }); - - it("Should get credentials as encrypted string", async function () { - const randomUUID = crypto.randomUUID(); - await window.electronAPI.storeCredential(scopeName, randomUUID); - - const response = await window.electronAPI.getCredential(scopeName); - expect(response).toBeDefined(); - expect(response).not.toEqual(randomUUID); - }); - - it("Should retrieve and decrypt set credentials with kernal mode keys", async function () { - const randomUUID = crypto.randomUUID(); - await window.electronAPI.storeCredential(scopeName, randomUUID); - - const creds = await window.electronAPI.getCredential(scopeName); - expect(creds).toBeDefined(); - const decryptedString = await decryptCreds(creds); - expect(decryptedString).toEqual(randomUUID); - }); - - it("Should return an error if credentials do not exist", async function () { - const response = await window.electronAPI.getCredential(scopeName); - expect(response).toBeNull(); - }); - - it("Should delete stored credentials", async function () { - const randomUUID = crypto.randomUUID(); - await window.electronAPI.storeCredential(scopeName, randomUUID); - - // Ensure credential exists - let creds = await window.electronAPI.getCredential(scopeName); - expect(creds).toBeDefined(); - - // Delete credential - await expectAsync( - window.electronAPI.deleteCredential(scopeName) - ).toBeResolved(); - - // Ensure credential is deleted - creds = await window.electronAPI.getCredential(scopeName); - expect(creds).toBeNull(); - }); - - it("Should handle deletion of non-existent credentials gracefully", async function () { - let error; - try { - await window.electronAPI.deleteCredential(scopeName); - } catch (err) { - error = err; - } - - // The test should fail if no error was thrown - expect(error).toBeDefined(); - - // Check for OS-specific error messages - const expectedErrors = [ - "No matching entry found in secure storage", // Common error on Linux/macOS - "The specified item could not be found in the keychain", // macOS Keychain - "Element not found" // Windows Credential Manager - ]; - - const isExpectedError = expectedErrors.some(msg => error.toString().includes(msg)); - expect(isExpectedError).toBeTrue(); - }); - - it("Should overwrite existing credentials when storing with the same scope", async function () { - const oldUUID = crypto.randomUUID(); - await window.electronAPI.storeCredential(scopeName, oldUUID); - - let creds = await window.electronAPI.getCredential(scopeName); - expect(creds).toBeDefined(); - let response = await decryptCreds(creds); - expect(response).toEqual(oldUUID); - - // Store new credentials with the same scope - const newUUID = crypto.randomUUID(); - await window.electronAPI.storeCredential(scopeName, newUUID); - - creds = await window.electronAPI.getCredential(scopeName); - expect(creds).toBeDefined(); - response = await decryptCreds(creds); - expect(response).toEqual(newUUID); - }); - - // trustRing.getCredential and set tests - async function setSomeKey() { - const randomCred = crypto.randomUUID(); - await trustRing.setCredential(TEST_TRUST_KEY_NAME, randomCred); - const savedCred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); - expect(savedCred).toEqual(randomCred); - return savedCred; - } - - it("Should get and set API key in kernal mode trust ring", async function () { - await setSomeKey(); - }); - - it("Should get and set empty string API key in kernal mode trust ring", async function () { - const randomCred = ""; - await trustRing.setCredential(TEST_TRUST_KEY_NAME, randomCred); - const savedCred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); - expect(savedCred).toEqual(randomCred); - }); - - it("Should remove API key in kernal mode trust ring work as expected", async function () { - await setSomeKey(); - await trustRing.removeCredential(TEST_TRUST_KEY_NAME); - const cred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); - expect(cred).toBeNull(); - }); - - // trust key management - it("Should not be able to set trust key if one is already set", async function () { - const kv = trustRing.generateRandomKeyAndIV(); - let error; - try { - await window.electronAPI.trustWindowAesKey(kv.key, kv.iv); - } catch (err) { - error = err; - } - expect(error.toString()).toContain("Trust has already been established for this window."); - }); - - it("Should be able to remove trust key with key and iv", async function () { - await window.electronAPI.removeTrustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); - let error; - try { - await window.electronAPI.removeTrustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); - } catch (err) { - error = err; - } - expect(error.toString()).toContain("No trust association found for this window."); - // reinstate trust - await window.electronAPI.trustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); - }); - - it("Should getCredential not work without trust", async function () { - await setSomeKey(); - await window.electronAPI.removeTrustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); - let error; - try { - await trustRing.getCredential(TEST_TRUST_KEY_NAME); - } catch (err) { - error = err; - } - expect(error.toString()).toContain("Trust needs to be first established"); - // reinstate trust - await window.electronAPI.trustWindowAesKey(trustRing.aesKeys.key, trustRing.aesKeys.iv); - }); - }); - }); - }); -}); diff --git a/test/spec/Tauri-platform-test.js b/test/spec/Tauri-platform-test.js index f7cd73350..37bcdd075 100644 --- a/test/spec/Tauri-platform-test.js +++ b/test/spec/Tauri-platform-test.js @@ -22,13 +22,67 @@ /*global describe, it, expect, beforeEach, afterEach, fs, path, jasmine, expectAsync*/ define(function (require, exports, module) { - if(!window.__TAURI__) { + // Platform detection + const isElectron = !!window.__ELECTRON__; + const isTauri = !!window.__TAURI__; + + if (!isElectron && !isTauri) { return; } - const SpecRunnerUtils = require("spec/SpecRunnerUtils"); + const SpecRunnerUtils = require("spec/SpecRunnerUtils"); + + // Platform abstraction helpers - same tests, different API calls + const platform = { + name: isElectron ? 'Electron' : 'Tauri', + + // Path APIs + appLocalDataDir: () => isElectron + ? window.electronFSAPI.appLocalDataDir() + : window.__TAURI__.path.appLocalDataDir(), + + documentDir: () => isElectron + ? window._tauriBootVars.documentDir // Same source for both + : window._tauriBootVars.documentDir, + + // Asset URL conversion + convertToAssetURL: (platformPath) => isElectron + ? window.electronAPI.convertToAssetURL(platformPath) + : window.__TAURI__.tauri.convertFileSrc(platformPath), + + // Credential APIs + storeCredential: (scopeName, secretVal) => isElectron + ? window.electronAPI.storeCredential(scopeName, secretVal) + : window.__TAURI__.invoke("store_credential", { scopeName, secretVal }), + + getCredential: (scopeName) => isElectron + ? window.electronAPI.getCredential(scopeName) + : window.__TAURI__.invoke("get_credential", { scopeName }), + + deleteCredential: (scopeName) => isElectron + ? window.electronAPI.deleteCredential(scopeName) + : window.__TAURI__.invoke("delete_credential", { scopeName }), - describe("unit: Tauri Platform Tests", function () { + // Trust ring APIs + trustWindowAesKey: (keyIv) => isElectron + ? window.electronAPI.trustWindowAesKey(keyIv.key, keyIv.iv) + : window.__TAURI__.tauri.invoke("trust_window_aes_key", keyIv), + + removeTrustWindowAesKey: (keyIv) => isElectron + ? window.electronAPI.removeTrustWindowAesKey(keyIv.key, keyIv.iv) + : window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", keyIv), + + // Window management - returns platform-specific window object + // For window spawning tests, we use a helper HTML file + getTestHtmlPath: () => isElectron + ? 'spec/Electron-platform-test.html' + : 'spec/Tauri-platform-test.html', + + // Close window by label (uses platform-agnostic Phoenix.app API) + closeWindow: (windowObj) => Phoenix.app.closeWindowByLabel(windowObj.label) + }; + + describe(`unit: ${platform.name} Platform Tests`, function () { beforeEach(async function () { @@ -40,7 +94,7 @@ define(function (require, exports, module) { describe("asset url tests", function () { it("Should be able to fetch files in {appLocalData}/assets folder", async function () { - const appLocalData = fs.getTauriVirtualPath(await window.__TAURI__.path.appLocalDataDir()); + const appLocalData = fs.getTauriVirtualPath(await platform.appLocalDataDir()); expect(await SpecRunnerUtils.pathExists(appLocalData, true)).toBeTrue(); expect(appLocalData.split("/")[1]).toEql("tauri"); // should be /tauri/applocaldata/path @@ -51,7 +105,7 @@ define(function (require, exports, module) { await SpecRunnerUtils.createTextFileAsync(assetHTMLPath, assetHtmlText); const appLocalDataPlatformPath = fs.getTauriPlatformPath(assetHTMLPath); - const appLocalDataURL = window.__TAURI__.tauri.convertFileSrc(appLocalDataPlatformPath); + const appLocalDataURL = platform.convertToAssetURL(appLocalDataPlatformPath); const fetchedData = await ((await fetch(appLocalDataURL)).text()); expect(fetchedData).toEqual(assetHtmlText); @@ -70,15 +124,21 @@ define(function (require, exports, module) { await SpecRunnerUtils.createTextFileAsync(assetHTMLPath, assetHtmlText); const appLocalDataPlatformPath = fs.getTauriPlatformPath(assetHTMLPath); - const appLocalDataURL = window.__TAURI__.tauri.convertFileSrc(appLocalDataPlatformPath); - - let err; - try{ - await fetch(appLocalDataURL); + const appLocalDataURL = platform.convertToAssetURL(appLocalDataPlatformPath); + + // Tauri throws an error, Electron returns 403 response + let accessDenied = false; + try { + const response = await fetch(appLocalDataURL); + // Electron returns 403 for unauthorized access + if (!response.ok) { + accessDenied = true; + } } catch (e) { - err = e; + // Tauri throws an error + accessDenied = true; } - expect(err).toBeDefined(); + expect(accessDenied).withContext("Asset URL should not be accessible outside assets folder").toBeTrue(); // delete test file await SpecRunnerUtils.deletePathAsync(assetHTMLPath); @@ -88,35 +148,155 @@ define(function (require, exports, module) { // unfortunately for tests, this is set to appdata/testDocuments. // we cant set this to await window.__TAURI__.path.documentDir() as in github actions, // the user documents directory is not defined in rust and throws. - await testAssetNotAccessibleFolder(window._tauriBootVars.documentDir); + await testAssetNotAccessibleFolder(platform.documentDir()); }); it("Should not be able to fetch files in appLocalData folder", async function () { - await testAssetNotAccessibleFolder(await window.__TAURI__.path.appLocalDataDir()); + await testAssetNotAccessibleFolder(await platform.appLocalDataDir()); }); + // Electron-specific security test: verify asset:// URLs don't have API access + if (isElectron) { + it("Should NOT have electronAPI access from asset:// protocol", async function () { + // This test verifies that content loaded from asset:// URLs is sandboxed + // and does not have access to Electron APIs (matching Tauri's security posture) + const appLocalData = fs.getTauriVirtualPath(await platform.appLocalDataDir()); + const securityTestPath = `${appLocalData}/assets/security-test-${Date.now()}.html`; + const SECURITY_TEST_KEY = 'ELECTRON_ASSET_SECURITY_TEST_' + Date.now(); + + // Create the security test HTML in assets folder + // This script will try to use electronAPI if available and report back + const securityTestHtml = ` +Security Test`; + + await SpecRunnerUtils.ensureExistsDirAsync(path.dirname(securityTestPath)); + await SpecRunnerUtils.createTextFileAsync(securityTestPath, securityTestHtml); + + // Get the asset:// URL for the test file + const platformPath = fs.getTauriPlatformPath(securityTestPath); + const assetURL = platform.convertToAssetURL(platformPath); + + // Clear any previous test result + await window.electronAPI.putItem(SECURITY_TEST_KEY, null); + + // Try to open a window with the asset:// URL + let windowLabel = null; + try { + windowLabel = await window.electronAPI.createPhoenixWindow(assetURL, { + windowTitle: 'Security Test', + width: 400, + height: 300, + isExtension: true + }); + } catch (e) { + // If window creation fails for asset:// URLs, that's acceptable security behavior + console.log("Window creation blocked for asset:// URL (expected):", e.message); + } + + if (windowLabel) { + // Window was created - wait for it to load and potentially try to use APIs + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Check if the sandboxed window was able to use electronAPI + const items = await window.electronAPI.getAllItems(); + const testResult = items[SECURITY_TEST_KEY]; + + if (testResult) { + const parsed = JSON.parse(testResult); + // SECURITY CHECK: If we got a result, verify no API access was possible + // If any of these are true, it's a security vulnerability + expect(parsed.hasElectronAPI).withContext( + "SECURITY VIOLATION: asset:// window has electronAPI access" + ).toBeFalse(); + expect(parsed.hasElectronFSAPI).withContext( + "SECURITY VIOLATION: asset:// window has electronFSAPI access" + ).toBeFalse(); + expect(parsed.hasElectronAppAPI).withContext( + "SECURITY VIOLATION: asset:// window has electronAppAPI access" + ).toBeFalse(); + } + // If no result was stored, the window couldn't access APIs - test passes + + // Close the security test window by its label + try { + await Phoenix.app.closeWindowByLabel(windowLabel); + } catch (e) { + console.warn("Could not close security test window:", e); + } + } + + // Cleanup + await SpecRunnerUtils.deletePathAsync(securityTestPath); + }); + } + function createWebView() { - return new Promise((resolve, reject)=>{ + return new Promise((resolve, reject) => { let currentURL = new URL(location.href); let pathParts = currentURL.pathname.split('/'); - pathParts[pathParts.length - 1] = 'spec/Tauri-platform-test.html'; + pathParts[pathParts.length - 1] = platform.getTestHtmlPath(); currentURL.pathname = pathParts.join('/'); let newURL = currentURL.href; - Phoenix.app.openURLInPhoenixWindow(newURL) - .then(tauriWindow =>{ - expect(tauriWindow.label.startsWith("extn-")).toBeTrue(); - tauriWindow.listen('TAURI_API_WORKING', function () { - resolve(tauriWindow); + + if (isElectron) { + // For Electron, use the event system (mirrors Tauri) + // We need to handle race: event might fire before or after window reference is available + let electronWindow = null; + let eventReceived = false; + + const tryResolve = () => { + if (electronWindow && eventReceived) { + resolve(electronWindow); + } + }; + + const unlisten = window.electronAPI.onWindowEvent('PLATFORM_API_WORKING', () => { + unlisten(); + eventReceived = true; + tryResolve(); + }); + + Phoenix.app.openURLInPhoenixWindow(newURL) + .then(win => { + expect(win.label.startsWith("extn-")).toBeTrue(); + expect(win.isNativeWindow).toBeTrue(); + electronWindow = win; + tryResolve(); + }).catch(err => { + unlisten(); + reject(err); }); - }).catch(reject); + } else { + // For Tauri, use the event system + Phoenix.app.openURLInPhoenixWindow(newURL) + .then(tauriWindow => { + expect(tauriWindow.label.startsWith("extn-")).toBeTrue(); + tauriWindow.listen('TAURI_API_WORKING', function () { + resolve(tauriWindow); + }); + }).catch(reject); + } }); - } - it("Should be able to spawn tauri windows", async function () { - const tauriWindow = await createWebView(); - await tauriWindow.close(); + it("Should be able to spawn windows", async function () { + const nativeWindow = await createWebView(); + await platform.closeWindow(nativeWindow); }); it("Should be able to get process ID", async function () { @@ -125,14 +305,16 @@ define(function (require, exports, module) { }); const maxWindows = 25; - it(`Should be able to spawn ${maxWindows} tauri windows`, async function () { - const tauriWindows = []; - for(let i=0; i setTimeout(resolve, 1000)); }, 120000); }); @@ -147,12 +329,12 @@ define(function (require, exports, module) { beforeEach(async function () { // Cleanup before running tests - await window.__TAURI__.invoke("delete_credential", { scopeName }).catch(() => {}); + await platform.deleteCredential(scopeName).catch(() => {}); }); afterEach(async function () { // Cleanup after tests - await window.__TAURI__.invoke("delete_credential", { scopeName }).catch(() => {}); + await platform.deleteCredential(scopeName).catch(() => {}); }); if(Phoenix.isTestWindowGitHubActions && Phoenix.platform === "linux"){ @@ -167,56 +349,56 @@ define(function (require, exports, module) { it("Should store credentials successfully", async function () { const randomUUID = crypto.randomUUID(); await expectAsync( - window.__TAURI__.invoke("store_credential", { scopeName, secretVal: randomUUID }) + platform.storeCredential(scopeName, randomUUID) ).toBeResolved(); }); it("Should get credentials as encrypted string", async function () { const randomUUID = crypto.randomUUID(); - await window.__TAURI__.invoke("store_credential", { scopeName, secretVal: randomUUID }); + await platform.storeCredential(scopeName, randomUUID); - const response = await window.__TAURI__.invoke("get_credential", { scopeName }); + const response = await platform.getCredential(scopeName); expect(response).toBeDefined(); expect(response).not.toEqual(randomUUID); }); it("Should retrieve and decrypt set credentials with kernal mode keys", async function () { const randomUUID = crypto.randomUUID(); - await window.__TAURI__.invoke("store_credential", { scopeName, secretVal: randomUUID }); + await platform.storeCredential(scopeName, randomUUID); - const creds = await window.__TAURI__.invoke("get_credential", { scopeName }); + const creds = await platform.getCredential(scopeName); expect(creds).toBeDefined(); const decryptedString = await decryptCreds(creds); expect(decryptedString).toEqual(randomUUID); }); - it("Should return an error if credentials do not exist", async function () { - const response = await window.__TAURI__.invoke("get_credential", { scopeName }); + it("Should return null if credentials do not exist", async function () { + const response = await platform.getCredential(scopeName); expect(response).toBeNull(); }); it("Should delete stored credentials", async function () { const randomUUID = crypto.randomUUID(); - await window.__TAURI__.invoke("store_credential", { scopeName, secretVal: randomUUID }); + await platform.storeCredential(scopeName, randomUUID); // Ensure credential exists - let creds = await window.__TAURI__.invoke("get_credential", { scopeName }); + let creds = await platform.getCredential(scopeName); expect(creds).toBeDefined(); // Delete credential await expectAsync( - window.__TAURI__.invoke("delete_credential", { scopeName }) + platform.deleteCredential(scopeName) ).toBeResolved(); // Ensure credential is deleted - creds = await window.__TAURI__.invoke("get_credential", { scopeName }); + creds = await platform.getCredential(scopeName); expect(creds).toBeNull(); }); it("Should handle deletion of non-existent credentials gracefully", async function () { let error; try { - await window.__TAURI__.invoke("delete_credential", { scopeName }); + await platform.deleteCredential(scopeName); } catch (err) { error = err; } @@ -231,24 +413,24 @@ define(function (require, exports, module) { "Element not found" // Windows Credential Manager ]; - const isExpectedError = expectedErrors.some(msg => error.includes(msg)); + const isExpectedError = expectedErrors.some(msg => error.toString().includes(msg)); expect(isExpectedError).toBeTrue(); }); it("Should overwrite existing credentials when storing with the same scope", async function () { const oldUUID = crypto.randomUUID(); - await window.__TAURI__.invoke("store_credential", { scopeName, secretVal: oldUUID }); + await platform.storeCredential(scopeName, oldUUID); - let creds = await window.__TAURI__.invoke("get_credential", { scopeName }); + let creds = await platform.getCredential(scopeName); expect(creds).toBeDefined(); let response = await decryptCreds(creds); expect(response).toEqual(oldUUID); // Store new credentials with the same scope const newUUID = crypto.randomUUID(); - await window.__TAURI__.invoke("store_credential", { scopeName, secretVal: newUUID }); + await platform.storeCredential(scopeName, newUUID); - creds = await window.__TAURI__.invoke("get_credential", { scopeName }); + creds = await platform.getCredential(scopeName); expect(creds).toBeDefined(); response = await decryptCreds(creds); expect(response).toEqual(newUUID); @@ -286,38 +468,38 @@ define(function (require, exports, module) { const kv = trustRing.generateRandomKeyAndIV(); let error; try { - await window.__TAURI__.tauri.invoke("trust_window_aes_key", kv); + await platform.trustWindowAesKey(kv); } catch (err) { error = err; } - expect(error).toContain("Trust has already been established for this window."); + expect(error.toString()).toContain("Trust has already been established for this window."); }); it("Should be able to remove trust key with key and iv", async function () { - await window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", trustRing.aesKeys); + await platform.removeTrustWindowAesKey(trustRing.aesKeys); let error; try { - await window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", trustRing.aesKeys); + await platform.removeTrustWindowAesKey(trustRing.aesKeys); } catch (err) { error = err; } - expect(error).toContain("No trust association found for this window."); + expect(error.toString()).toContain("No trust association found for this window."); // reinstate trust - await window.__TAURI__.tauri.invoke("trust_window_aes_key", trustRing.aesKeys); + await platform.trustWindowAesKey(trustRing.aesKeys); }); it("Should getCredential not work without trust", async function () { await setSomeKey(); - await window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", trustRing.aesKeys); + await platform.removeTrustWindowAesKey(trustRing.aesKeys); let error; try { await trustRing.getCredential(TEST_TRUST_KEY_NAME); } catch (err) { error = err; } - expect(error).toContain("Trust needs to be first established"); + expect(error.toString()).toContain("Trust needs to be first established"); // reinstate trust - await window.__TAURI__.tauri.invoke("trust_window_aes_key", trustRing.aesKeys); + await platform.trustWindowAesKey(trustRing.aesKeys); }); }); }); From 297cfc27d0aef056e73b4b01727b3b1643751c0b Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 31 Jan 2026 13:59:43 +0530 Subject: [PATCH 11/14] refactor: file name to Native-platform-test.js instead of tauri --- test/UnitTestSuite.js | 2 +- test/spec/{Tauri-platform-test.js => Native-platform-test.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/spec/{Tauri-platform-test.js => Native-platform-test.js} (100%) diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index e3f9a3e52..f2dd6c590 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -22,7 +22,7 @@ define(function (require, exports, module) { require("spec/Phoenix-platform-test"); - require("spec/Tauri-platform-test"); + require("test/spec/Native-platform-test"); require("spec/trust-ring-test"); require("spec/utframework-suite-test"); require("spec/Async-test"); diff --git a/test/spec/Tauri-platform-test.js b/test/spec/Native-platform-test.js similarity index 100% rename from test/spec/Tauri-platform-test.js rename to test/spec/Native-platform-test.js From b59addacdbaa496077ef4622196088010ce67d28 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 31 Jan 2026 14:14:51 +0530 Subject: [PATCH 12/14] feat: native platfrom window event emitting in Phoenix.app and tests --- src/phoenix/shell.js | 67 +++++++- test/spec/Native-platform-test.js | 146 +++++++++++++----- ...tml => native-platform-electron-test.html} | 0 ...t.html => native-platform-tauri-test.html} | 3 +- 4 files changed, 177 insertions(+), 39 deletions(-) rename test/spec/{Electron-platform-test.html => native-platform-electron-test.html} (100%) rename test/spec/{Tauri-platform-test.html => native-platform-tauri-test.html} (55%) diff --git a/src/phoenix/shell.js b/src/phoenix/shell.js index 7160a1710..c9e3b4ed0 100644 --- a/src/phoenix/shell.js +++ b/src/phoenix/shell.js @@ -729,7 +729,72 @@ Phoenix.app = { getTimeSinceStartup: function () { return Date.now() - Phoenix.startTime; // milliseconds elapsed since app start }, - language: navigator.language + language: navigator.language, + /** + * Broadcast an event to all windows (excludes sender). + * @param {string} eventName - Name of the event + * @param {*} payload - Event data + * @returns {Promise} + */ + emitToAllWindows: async function (eventName, payload) { + if (!Phoenix.isNativeApp) { + throw new Error("emitToAllWindows is not supported in browsers"); + } + if (window.__TAURI__) { + return window.__TAURI__.event.emit(eventName, payload); + } + if (window.__ELECTRON__) { + return window.electronAPI.emitToAllWindows(eventName, payload); + } + }, + /** + * Send an event to a specific window by label. + * @param {string} targetLabel - Window label to send to + * @param {string} eventName - Name of the event + * @param {*} payload - Event data + * @returns {Promise} True if window found and event sent + */ + emitToWindow: async function (targetLabel, eventName, payload) { + if (!Phoenix.isNativeApp) { + throw new Error("emitToWindow is not supported in browsers"); + } + if (window.__TAURI__) { + // Tauri doesn't have direct window-to-window emit, use global emit + // The listener filters by source if needed + return window.__TAURI__.event.emit(eventName, payload); + } + if (window.__ELECTRON__) { + return window.electronAPI.emitToWindow(targetLabel, eventName, payload); + } + return false; + }, + /** + * Listen for events from other windows. + * @param {string} eventName - Name of the event to listen for + * @param {Function} callback - Called with (payload) when event received + * @returns {Function} Unlisten function to remove the listener + */ + onWindowEvent: function (eventName, callback) { + if (!Phoenix.isNativeApp) { + throw new Error("onWindowEvent is not supported in browsers"); + } + if (window.__TAURI__) { + let unlisten = null; + window.__TAURI__.event.listen(eventName, (event) => { + callback(event.payload); + }).then(fn => { unlisten = fn; }); + // Return a function that will unlisten when called + return () => { + if (unlisten) { + unlisten(); + } + }; + } + if (window.__ELECTRON__) { + return window.electronAPI.onWindowEvent(eventName, callback); + } + return () => {}; // No-op for unsupported platforms + } }; if(!window.appshell){ diff --git a/test/spec/Native-platform-test.js b/test/spec/Native-platform-test.js index 37bcdd075..0e4ca767c 100644 --- a/test/spec/Native-platform-test.js +++ b/test/spec/Native-platform-test.js @@ -75,8 +75,8 @@ define(function (require, exports, module) { // Window management - returns platform-specific window object // For window spawning tests, we use a helper HTML file getTestHtmlPath: () => isElectron - ? 'spec/Electron-platform-test.html' - : 'spec/Tauri-platform-test.html', + ? 'spec/native-platform-electron-test.html' + : 'spec/native-platform-tauri-test.html', // Close window by label (uses platform-agnostic Phoenix.app API) closeWindow: (windowObj) => Phoenix.app.closeWindowByLabel(windowObj.label) @@ -253,44 +253,32 @@ define(function (require, exports, module) { let newURL = currentURL.href; - if (isElectron) { - // For Electron, use the event system (mirrors Tauri) - // We need to handle race: event might fire before or after window reference is available - let electronWindow = null; - let eventReceived = false; + // Use unified event API for both platforms + let nativeWindow = null; + let eventReceived = false; - const tryResolve = () => { - if (electronWindow && eventReceived) { - resolve(electronWindow); - } - }; - - const unlisten = window.electronAPI.onWindowEvent('PLATFORM_API_WORKING', () => { - unlisten(); - eventReceived = true; + const tryResolve = () => { + if (nativeWindow && eventReceived) { + resolve(nativeWindow); + } + }; + + const unlisten = Phoenix.app.onWindowEvent('PLATFORM_API_WORKING', () => { + unlisten(); + eventReceived = true; + tryResolve(); + }); + + Phoenix.app.openURLInPhoenixWindow(newURL) + .then(win => { + expect(win.label.startsWith("extn-")).toBeTrue(); + expect(win.isNativeWindow).toBeTrue(); + nativeWindow = win; tryResolve(); + }).catch(err => { + unlisten(); + reject(err); }); - - Phoenix.app.openURLInPhoenixWindow(newURL) - .then(win => { - expect(win.label.startsWith("extn-")).toBeTrue(); - expect(win.isNativeWindow).toBeTrue(); - electronWindow = win; - tryResolve(); - }).catch(err => { - unlisten(); - reject(err); - }); - } else { - // For Tauri, use the event system - Phoenix.app.openURLInPhoenixWindow(newURL) - .then(tauriWindow => { - expect(tauriWindow.label.startsWith("extn-")).toBeTrue(); - tauriWindow.listen('TAURI_API_WORKING', function () { - resolve(tauriWindow); - }); - }).catch(reject); - } }); } @@ -318,6 +306,90 @@ define(function (require, exports, module) { }, 120000); }); + describe("Inter-window Event API Tests", function () { + // Note: emitToAllWindows excludes the sender, so we test cross-window communication + // using spawned windows that emit PLATFORM_API_WORKING event + + it("Should receive events from spawned windows using unified API", async function () { + let eventReceived = false; + let receivedPayload = null; + const unlisten = Phoenix.app.onWindowEvent('PLATFORM_API_WORKING', (payload) => { + eventReceived = true; + receivedPayload = payload; + }); + + // Small delay for listener registration (Tauri's listen is async) + await new Promise(resolve => setTimeout(resolve, 100)); + + let currentURL = new URL(location.href); + let pathParts = currentURL.pathname.split('/'); + pathParts[pathParts.length - 1] = platform.getTestHtmlPath(); + currentURL.pathname = pathParts.join('/'); + + const win = await Phoenix.app.openURLInPhoenixWindow(currentURL.href); + expect(win.label.startsWith("extn-")).toBeTrue(); + + // Wait for the spawned window to emit the event + await new Promise(resolve => setTimeout(resolve, 1000)); + + expect(eventReceived).toBeTrue(); + expect(receivedPayload).toBeDefined(); + + unlisten(); + await platform.closeWindow(win); + }); + + it("Should unlisten properly and not receive events after unlisten", async function () { + let callCount = 0; + const unlisten = Phoenix.app.onWindowEvent('PLATFORM_API_WORKING', () => { + callCount++; + }); + + // Small delay for listener registration + await new Promise(resolve => setTimeout(resolve, 100)); + + // Spawn first window - should receive event + let currentURL = new URL(location.href); + let pathParts = currentURL.pathname.split('/'); + pathParts[pathParts.length - 1] = platform.getTestHtmlPath(); + currentURL.pathname = pathParts.join('/'); + + const win1 = await Phoenix.app.openURLInPhoenixWindow(currentURL.href); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(callCount).toBeGreaterThanOrEqual(1); + const countAfterFirst = callCount; + + // Unlisten + unlisten(); + await new Promise(resolve => setTimeout(resolve, 100)); + + // Spawn second window - should NOT receive event + const win2 = await Phoenix.app.openURLInPhoenixWindow(currentURL.href); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(callCount).toEqual(countAfterFirst); // Count should not increase + + await platform.closeWindow(win1); + await platform.closeWindow(win2); + }); + + it("Should not throw when emitting events", async function () { + // Basic sanity test that emit APIs don't throw + await expectAsync( + Phoenix.app.emitToAllWindows('TEST_EVENT', { test: true }) + ).toBeResolved(); + + await expectAsync( + Phoenix.app.emitToWindow('nonexistent-window', 'TEST_EVENT', { test: true }) + ).toBeResolved(); + }); + + it("Should return unlisten function from onWindowEvent", function () { + const unlisten = Phoenix.app.onWindowEvent('TEST_EVENT', () => {}); + expect(typeof unlisten).toEqual('function'); + unlisten(); // Should not throw + }); + }); + describe("Credentials OTP API Tests", function () { const scopeName = "testScope"; const trustRing = window.specRunnerTestKernalModeTrust; diff --git a/test/spec/Electron-platform-test.html b/test/spec/native-platform-electron-test.html similarity index 100% rename from test/spec/Electron-platform-test.html rename to test/spec/native-platform-electron-test.html diff --git a/test/spec/Tauri-platform-test.html b/test/spec/native-platform-tauri-test.html similarity index 55% rename from test/spec/Tauri-platform-test.html rename to test/spec/native-platform-tauri-test.html index 14732a90e..55035470a 100644 --- a/test/spec/Tauri-platform-test.html +++ b/test/spec/native-platform-tauri-test.html @@ -4,7 +4,8 @@ Test tauri apis accessible From 5b5b01b844a16377e63c04823d6ada51b87b0b87 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 31 Jan 2026 17:49:11 +0530 Subject: [PATCH 13/14] fix: should always use en language in specrunner window --- test/SpecRunner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/SpecRunner.js b/test/SpecRunner.js index 2fa1677ed..0c17e0ace 100644 --- a/test/SpecRunner.js +++ b/test/SpecRunner.js @@ -44,7 +44,8 @@ require.config({ "thirdparty/preact": "preact-compat", "thirdparty/preact-test-utils": "preact-test-utils" } - } + }, + locale: "en" // force English (US) for consistent test strings }); window.logger = { From a0b43726ed2250d667a2443ec3482e696d857389 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 31 Jan 2026 23:59:02 +0530 Subject: [PATCH 14/14] fix: all tests working in electron --- test/spec/Storage-integ-test.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/spec/Storage-integ-test.js b/test/spec/Storage-integ-test.js index 925a65cc2..2d18cbb67 100644 --- a/test/spec/Storage-integ-test.js +++ b/test/spec/Storage-integ-test.js @@ -122,7 +122,7 @@ define(function (require, exports, module) { expect(val).toEql(expectedValue); }); - it("Should be able to create lmdb dumps in tauri", async function () { + it("Should be able to create lmdb dumps in native app", async function () { if(!Phoenix.isNativeApp){ return; } @@ -137,7 +137,14 @@ define(function (require, exports, module) { }); const dumpFileLocation = await window.storageNodeConnector.execPeer("dumpDBToFile"); - const dumpFileText = await window.__TAURI__.fs.readTextFile(dumpFileLocation); + let dumpFileText; + if (window.__TAURI__) { + dumpFileText = await window.__TAURI__.fs.readTextFile(dumpFileLocation); + } else if (window.__ELECTRON__) { + const data = await window.electronFSAPI.fsReadFile(dumpFileLocation); + const decoder = new TextDecoder("utf-8"); + dumpFileText = decoder.decode(data); + } const dumpObj = JSON.parse(dumpFileText); expect(dumpObj[key]).toEql(expectedValue); });