Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,10 @@
// 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;
}
if(window.__TAURI__ || window.__ELECTRON__) {
window.__IS_NATIVE_SHELL__ = true;
Expand Down
107 changes: 106 additions & 1 deletion src/phoenix/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<boolean>}
*/
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.
Expand Down Expand Up @@ -689,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<void>}
*/
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<boolean>} 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){
Expand Down
11 changes: 11 additions & 0 deletions test/SpecRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@
});
}
setupElectronBootVars();
// F12 to toggle dev tools in Electron
document.addEventListener('keydown', function(e) {
if (e.key === 'F12') {
e.preventDefault();
window.electronAPI.toggleDevTools();
}
if (e.key === 'F5') {
e.preventDefault();
location.reload();
}
});
}
</script>

Expand Down
3 changes: 2 additions & 1 deletion test/SpecRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
34 changes: 27 additions & 7 deletions test/UnitTestReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,38 @@ define(function (require, exports, module) {
return '';
}

function hasCliFlag(args, flagName, shortFlag) {
return args.some(arg =>
arg === `--${flagName}` ||
arg.startsWith(`--${flagName}=`) ||
(shortFlag && arg === `-${shortFlag}`)
);
}

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', 'q')) {
window.electronAppAPI.quitApp(exitStatus);
}
});
}
}, WAIT_TIME_TO_COMPLETE_TEST_LOGGING_SEC * 1000);
}

Expand Down
2 changes: 1 addition & 1 deletion test/UnitTestSuite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
46 changes: 37 additions & 9 deletions test/index-dist-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,48 @@
<meta charset="UTF-8">
<title>Starting tests...</title>
<script type="text/javascript">
function navigateToTests(testSuiteToExec) {
if (testSuiteToExec) {
location.href = `test/SpecRunner.html?spec=All&category=${testSuiteToExec}`;
} else {
location.href = `test/SpecRunner.html`;
}
}

function parseCliArg(args, argName) {
// Parse --argName=value or --argName value from array
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith(`--${argName}=`)) {
return arg.split('=')[1];
}
if (arg === `--${argName}` && args[i + 1] && !args[i + 1].startsWith('--')) {
return args[i + 1];
}
}
return null;
}

function redirectToTestPage() {
if(window.__TAURI__) {
__TAURI__.window.appWindow.setFocus().catch(console.error).finally(()=>{
__TAURI__.cli.getMatches().then(matches=>{
if (window.__TAURI__) {
// Tauri path
__TAURI__.window.appWindow.setFocus().catch(console.error).finally(() => {
__TAURI__.cli.getMatches().then(matches => {
const testSuiteToExec = matches.args["run-tests"].value;
if(testSuiteToExec){
location.href = `test/SpecRunner.html?spec=All&category=${testSuiteToExec}`;
} else {
location.href = `test/SpecRunner.html`;
}
navigateToTests(testSuiteToExec);
}).catch(console.error);
});
} else if (window.electronAppAPI?.isElectron) {
// Electron path
window.electronAPI.focusWindow().catch(console.error).finally(() => {
window.electronAppAPI.getCliArgs().then(args => {
const testSuiteToExec = parseCliArg(args, 'run-tests');
navigateToTests(testSuiteToExec);
}).catch(console.error);
});
} else {
location.href = "test/SpecRunner.html";
// Browser fallback
navigateToTests(null);
}
}
</script>
Expand Down
5 changes: 3 additions & 2 deletions test/spec/ExtensionInstallation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion test/spec/ExtensionLoader-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {

Expand Down
8 changes: 7 additions & 1 deletion test/spec/ExtensionManager-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 7 additions & 7 deletions test/spec/LowLevelFileIO-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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}/`;
Expand All @@ -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}/`;
Expand All @@ -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();
Expand All @@ -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");
Expand All @@ -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();
Expand Down
Loading
Loading