Skip to content

Commit 72c39a7

Browse files
committed
feat: native platfrom window event emitting in Phoenix.app and tests
1 parent 7b6ae82 commit 72c39a7

4 files changed

Lines changed: 177 additions & 39 deletions

File tree

src/phoenix/shell.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,72 @@ Phoenix.app = {
729729
getTimeSinceStartup: function () {
730730
return Date.now() - Phoenix.startTime; // milliseconds elapsed since app start
731731
},
732-
language: navigator.language
732+
language: navigator.language,
733+
/**
734+
* Broadcast an event to all windows (excludes sender).
735+
* @param {string} eventName - Name of the event
736+
* @param {*} payload - Event data
737+
* @returns {Promise<void>}
738+
*/
739+
emitToAllWindows: async function (eventName, payload) {
740+
if (!Phoenix.isNativeApp) {
741+
throw new Error("emitToAllWindows is not supported in browsers");
742+
}
743+
if (window.__TAURI__) {
744+
return window.__TAURI__.event.emit(eventName, payload);
745+
}
746+
if (window.__ELECTRON__) {
747+
return window.electronAPI.emitToAllWindows(eventName, payload);
748+
}
749+
},
750+
/**
751+
* Send an event to a specific window by label.
752+
* @param {string} targetLabel - Window label to send to
753+
* @param {string} eventName - Name of the event
754+
* @param {*} payload - Event data
755+
* @returns {Promise<boolean>} True if window found and event sent
756+
*/
757+
emitToWindow: async function (targetLabel, eventName, payload) {
758+
if (!Phoenix.isNativeApp) {
759+
throw new Error("emitToWindow is not supported in browsers");
760+
}
761+
if (window.__TAURI__) {
762+
// Tauri doesn't have direct window-to-window emit, use global emit
763+
// The listener filters by source if needed
764+
return window.__TAURI__.event.emit(eventName, payload);
765+
}
766+
if (window.__ELECTRON__) {
767+
return window.electronAPI.emitToWindow(targetLabel, eventName, payload);
768+
}
769+
return false;
770+
},
771+
/**
772+
* Listen for events from other windows.
773+
* @param {string} eventName - Name of the event to listen for
774+
* @param {Function} callback - Called with (payload) when event received
775+
* @returns {Function} Unlisten function to remove the listener
776+
*/
777+
onWindowEvent: function (eventName, callback) {
778+
if (!Phoenix.isNativeApp) {
779+
throw new Error("onWindowEvent is not supported in browsers");
780+
}
781+
if (window.__TAURI__) {
782+
let unlisten = null;
783+
window.__TAURI__.event.listen(eventName, (event) => {
784+
callback(event.payload);
785+
}).then(fn => { unlisten = fn; });
786+
// Return a function that will unlisten when called
787+
return () => {
788+
if (unlisten) {
789+
unlisten();
790+
}
791+
};
792+
}
793+
if (window.__ELECTRON__) {
794+
return window.electronAPI.onWindowEvent(eventName, callback);
795+
}
796+
return () => {}; // No-op for unsupported platforms
797+
}
733798
};
734799

735800
if(!window.appshell){

test/spec/Native-platform-test.js

Lines changed: 109 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ define(function (require, exports, module) {
7575
// Window management - returns platform-specific window object
7676
// For window spawning tests, we use a helper HTML file
7777
getTestHtmlPath: () => isElectron
78-
? 'spec/Electron-platform-test.html'
79-
: 'spec/Tauri-platform-test.html',
78+
? 'spec/native-platform-electron-test.html'
79+
: 'spec/native-platform-tauri-test.html',
8080

8181
// Close window by label (uses platform-agnostic Phoenix.app API)
8282
closeWindow: (windowObj) => Phoenix.app.closeWindowByLabel(windowObj.label)
@@ -253,44 +253,32 @@ define(function (require, exports, module) {
253253

254254
let newURL = currentURL.href;
255255

256-
if (isElectron) {
257-
// For Electron, use the event system (mirrors Tauri)
258-
// We need to handle race: event might fire before or after window reference is available
259-
let electronWindow = null;
260-
let eventReceived = false;
256+
// Use unified event API for both platforms
257+
let nativeWindow = null;
258+
let eventReceived = false;
261259

262-
const tryResolve = () => {
263-
if (electronWindow && eventReceived) {
264-
resolve(electronWindow);
265-
}
266-
};
267-
268-
const unlisten = window.electronAPI.onWindowEvent('PLATFORM_API_WORKING', () => {
269-
unlisten();
270-
eventReceived = true;
260+
const tryResolve = () => {
261+
if (nativeWindow && eventReceived) {
262+
resolve(nativeWindow);
263+
}
264+
};
265+
266+
const unlisten = Phoenix.app.onWindowEvent('PLATFORM_API_WORKING', () => {
267+
unlisten();
268+
eventReceived = true;
269+
tryResolve();
270+
});
271+
272+
Phoenix.app.openURLInPhoenixWindow(newURL)
273+
.then(win => {
274+
expect(win.label.startsWith("extn-")).toBeTrue();
275+
expect(win.isNativeWindow).toBeTrue();
276+
nativeWindow = win;
271277
tryResolve();
278+
}).catch(err => {
279+
unlisten();
280+
reject(err);
272281
});
273-
274-
Phoenix.app.openURLInPhoenixWindow(newURL)
275-
.then(win => {
276-
expect(win.label.startsWith("extn-")).toBeTrue();
277-
expect(win.isNativeWindow).toBeTrue();
278-
electronWindow = win;
279-
tryResolve();
280-
}).catch(err => {
281-
unlisten();
282-
reject(err);
283-
});
284-
} else {
285-
// For Tauri, use the event system
286-
Phoenix.app.openURLInPhoenixWindow(newURL)
287-
.then(tauriWindow => {
288-
expect(tauriWindow.label.startsWith("extn-")).toBeTrue();
289-
tauriWindow.listen('TAURI_API_WORKING', function () {
290-
resolve(tauriWindow);
291-
});
292-
}).catch(reject);
293-
}
294282
});
295283
}
296284

@@ -318,6 +306,90 @@ define(function (require, exports, module) {
318306
}, 120000);
319307
});
320308

309+
describe("Inter-window Event API Tests", function () {
310+
// Note: emitToAllWindows excludes the sender, so we test cross-window communication
311+
// using spawned windows that emit PLATFORM_API_WORKING event
312+
313+
it("Should receive events from spawned windows using unified API", async function () {
314+
let eventReceived = false;
315+
let receivedPayload = null;
316+
const unlisten = Phoenix.app.onWindowEvent('PLATFORM_API_WORKING', (payload) => {
317+
eventReceived = true;
318+
receivedPayload = payload;
319+
});
320+
321+
// Small delay for listener registration (Tauri's listen is async)
322+
await new Promise(resolve => setTimeout(resolve, 100));
323+
324+
let currentURL = new URL(location.href);
325+
let pathParts = currentURL.pathname.split('/');
326+
pathParts[pathParts.length - 1] = platform.getTestHtmlPath();
327+
currentURL.pathname = pathParts.join('/');
328+
329+
const win = await Phoenix.app.openURLInPhoenixWindow(currentURL.href);
330+
expect(win.label.startsWith("extn-")).toBeTrue();
331+
332+
// Wait for the spawned window to emit the event
333+
await new Promise(resolve => setTimeout(resolve, 1000));
334+
335+
expect(eventReceived).toBeTrue();
336+
expect(receivedPayload).toBeDefined();
337+
338+
unlisten();
339+
await platform.closeWindow(win);
340+
});
341+
342+
it("Should unlisten properly and not receive events after unlisten", async function () {
343+
let callCount = 0;
344+
const unlisten = Phoenix.app.onWindowEvent('PLATFORM_API_WORKING', () => {
345+
callCount++;
346+
});
347+
348+
// Small delay for listener registration
349+
await new Promise(resolve => setTimeout(resolve, 100));
350+
351+
// Spawn first window - should receive event
352+
let currentURL = new URL(location.href);
353+
let pathParts = currentURL.pathname.split('/');
354+
pathParts[pathParts.length - 1] = platform.getTestHtmlPath();
355+
currentURL.pathname = pathParts.join('/');
356+
357+
const win1 = await Phoenix.app.openURLInPhoenixWindow(currentURL.href);
358+
await new Promise(resolve => setTimeout(resolve, 1000));
359+
expect(callCount).toBeGreaterThanOrEqual(1);
360+
const countAfterFirst = callCount;
361+
362+
// Unlisten
363+
unlisten();
364+
await new Promise(resolve => setTimeout(resolve, 100));
365+
366+
// Spawn second window - should NOT receive event
367+
const win2 = await Phoenix.app.openURLInPhoenixWindow(currentURL.href);
368+
await new Promise(resolve => setTimeout(resolve, 1000));
369+
expect(callCount).toEqual(countAfterFirst); // Count should not increase
370+
371+
await platform.closeWindow(win1);
372+
await platform.closeWindow(win2);
373+
});
374+
375+
it("Should not throw when emitting events", async function () {
376+
// Basic sanity test that emit APIs don't throw
377+
await expectAsync(
378+
Phoenix.app.emitToAllWindows('TEST_EVENT', { test: true })
379+
).toBeResolved();
380+
381+
await expectAsync(
382+
Phoenix.app.emitToWindow('nonexistent-window', 'TEST_EVENT', { test: true })
383+
).toBeResolved();
384+
});
385+
386+
it("Should return unlisten function from onWindowEvent", function () {
387+
const unlisten = Phoenix.app.onWindowEvent('TEST_EVENT', () => {});
388+
expect(typeof unlisten).toEqual('function');
389+
unlisten(); // Should not throw
390+
});
391+
});
392+
321393
describe("Credentials OTP API Tests", function () {
322394
const scopeName = "testScope";
323395
const trustRing = window.specRunnerTestKernalModeTrust;
File renamed without changes.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
<meta charset="UTF-8">
55
<title>Test tauri apis accessible</title>
66
<script>
7-
window.__TAURI__.event.emit('TAURI_API_WORKING', { data: 'Hello from Window!' });
7+
// Use same event name as Electron for unified API testing
8+
window.__TAURI__.event.emit('PLATFORM_API_WORKING', { data: 'Hello from Window!' });
89
</script>
910
</head>
1011
<body>

0 commit comments

Comments
 (0)