From 0097dfc870382a5aaa818ffa57c57684df74fcad Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 28 Dec 2025 22:26:52 +0530 Subject: [PATCH 1/8] chore: update strings --- src/nls/root/strings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7b0be0991..7b6c06499 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1650,7 +1650,6 @@ define({ "CONTACT_SUPPORT": "Contact Support", "SIGN_OUT": "Sign Out", "SIGN_IN": "Sign In", - "SIGN_IN_WITH_PRO": "Sign in with Pro", "ACCOUNT_DETAILS": "Account Details", "LOGIN_REFRESH": "Check Login Status", "SIGN_IN_WAITING_TITLE": "Waiting for Sign In", From a97d879fe0b9d10c9c36d262e85aa3cf347f900a Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 11:39:32 +0530 Subject: [PATCH 2/8] refactor: simplify inapp banner code --- .../InAppNotifications/banner.js | 43 +++++++++++------ .../InAppNotifications/utils.js | 47 ------------------- src/utils/Metrics.js | 2 +- .../Extn-InAppNotifications-integ-test.js | 28 +---------- 4 files changed, 30 insertions(+), 90 deletions(-) delete mode 100644 src/extensionsIntegrated/InAppNotifications/utils.js diff --git a/src/extensionsIntegrated/InAppNotifications/banner.js b/src/extensionsIntegrated/InAppNotifications/banner.js index 8d228037a..d6c01f669 100644 --- a/src/extensionsIntegrated/InAppNotifications/banner.js +++ b/src/extensionsIntegrated/InAppNotifications/banner.js @@ -30,7 +30,7 @@ define(function (require, exports, module) { PreferencesManager = require("preferences/PreferencesManager"), ExtensionUtils = require("utils/ExtensionUtils"), Metrics = require("utils/Metrics"), - utils = require("./utils"), + semver = require("thirdparty/semver.browser"), NotificationBarHtml = require("text!./htmlContent/notificationContainer.html"); ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); @@ -44,6 +44,26 @@ define(function (require, exports, module) { PreferencesManager.stateManager.definePreference(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE, "object", {}); + function _isValidForThisVersion(versionFilter) { + return semver.satisfies(brackets.metadata.apiVersion, versionFilter); + } + + // platformFilter is a string subset of + // "mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all" + function _isValidForThisPlatform(platformFilter) { + platformFilter = platformFilter.split(","); + if(platformFilter.includes("all") + || (platformFilter.includes(brackets.platform) && Phoenix.isNativeApp) // win linux and mac is only for tauri and not for browser in platform + || (platformFilter.includes("allDesktop") && Phoenix.isNativeApp) + || (platformFilter.includes("firefox") && Phoenix.browser.desktop.isFirefox && !Phoenix.isNativeApp) + || (platformFilter.includes("chrome") && Phoenix.browser.desktop.isChromeBased && !Phoenix.isNativeApp) + || (platformFilter.includes("safari") && Phoenix.browser.desktop.isSafari && !Phoenix.isNativeApp) + || (platformFilter.includes("allBrowser") && !Phoenix.isNativeApp)){ + return true; + } + return false; + } + /** * If there are multiple notifications, thew will be shown one after the other and not all at once. * A sample notifications is as follows: @@ -86,21 +106,18 @@ define(function (require, exports, module) { for(const notificationID of Object.keys(notifications)){ if(!_InAppBannerShownAndDone[notificationID]) { const notification = notifications[notificationID]; - if(!utils.isValidForThisVersion(notification.FOR_VERSIONS)){ + if(!_isValidForThisVersion(notification.FOR_VERSIONS)){ continue; } - if(!utils.isValidForThisPlatform(notification.PLATFORM)){ + if(!_isValidForThisPlatform(notification.PLATFORM)){ continue; } - if(!notification.HTML_CONTENT.includes(NOTIFICATION_ACK_CLASS) - && !notification.DANGER_SHOW_ON_EVERY_BOOT){ + if(!notification.DANGER_SHOW_ON_EVERY_BOOT){ // One time notification. mark as shown and never show again + // all notifications are one time, we track metrics for each notification separately _markAsShownAndDone(notificationID); } await showBannerAndWaitForDismiss(notification.HTML_CONTENT, notificationID); - if(!notification.DANGER_SHOW_ON_EVERY_BOOT){ - _markAsShownAndDone(notificationID); - } } } } @@ -185,25 +202,21 @@ define(function (require, exports, module) { $closeIcon = $notificationBar.find('.close-icon'); $notificationContent.append($htmlContent); - Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID, - "shown"); + Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-shown", notificationID); // Click handlers on actionable elements if ($closeIcon.length > 0) { $closeIcon.click(function () { cleanNotificationBanner(); - Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID, - "closeClick"); + Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-close", notificationID); !resolved && resolve($htmlContent); resolved = true; }); } $notificationBar.find(`.${NOTIFICATION_ACK_CLASS}`).click(function() { - // Your click event handler logic here cleanNotificationBanner(); - Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID, - "ackClick"); + Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-ack", notificationID); !resolved && resolve($htmlContent); resolved = true; }); diff --git a/src/extensionsIntegrated/InAppNotifications/utils.js b/src/extensionsIntegrated/InAppNotifications/utils.js deleted file mode 100644 index d9c64cf08..000000000 --- a/src/extensionsIntegrated/InAppNotifications/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * GNU AGPL-3.0 License - * - * Copyright (c) 2021 - present core.ai . All rights reserved. - * Original work Copyright (c) 2018 - 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. - * - */ - -define(function (require, exports, module) { - const semver = require("thirdparty/semver.browser"); - function isValidForThisVersion(versionFilter) { - return semver.satisfies(brackets.metadata.apiVersion, versionFilter); - } - - // platformFilter is a string subset of - // "mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all" - function isValidForThisPlatform(platformFilter) { - platformFilter = platformFilter.split(","); - if(platformFilter.includes("all") - || (platformFilter.includes(brackets.platform) && Phoenix.isNativeApp) // win linux and mac is only for tauri and not for browser in platform - || (platformFilter.includes("allDesktop") && Phoenix.isNativeApp) - || (platformFilter.includes("firefox") && Phoenix.browser.desktop.isFirefox && !Phoenix.isNativeApp) - || (platformFilter.includes("chrome") && Phoenix.browser.desktop.isChromeBased && !Phoenix.isNativeApp) - || (platformFilter.includes("safari") && Phoenix.browser.desktop.isSafari && !Phoenix.isNativeApp) - || (platformFilter.includes("allBrowser") && !Phoenix.isNativeApp)){ - return true; - } - return false; - } - - // api - exports.isValidForThisVersion = isValidForThisVersion; - exports.isValidForThisPlatform = isValidForThisPlatform; -}); diff --git a/src/utils/Metrics.js b/src/utils/Metrics.js index da4876105..25d7c139c 100644 --- a/src/utils/Metrics.js +++ b/src/utils/Metrics.js @@ -104,7 +104,7 @@ define(function (require, exports, module) { PROJECT: "project", THEMES: "themes", EXTENSIONS: "extensions", - NOTIFICATIONS: "notifications", + NOTIFICATIONS: "notify", UI: "UI", UI_MENU: "UIMenu", UI_DIALOG: "ui-dialog", diff --git a/test/spec/Extn-InAppNotifications-integ-test.js b/test/spec/Extn-InAppNotifications-integ-test.js index d8be2e422..9f946a3dc 100644 --- a/test/spec/Extn-InAppNotifications-integ-test.js +++ b/test/spec/Extn-InAppNotifications-integ-test.js @@ -165,13 +165,9 @@ define(function (require, exports, module) { expect(testWindow.$(id).length).toEqual(1); }); - it("Should show notification if not acknowledged with close click", async function () { + it("Should show notification only once", async function () { banner.cleanNotificationBanner(); const {notification, id} = getRandomNotification("all", false, true); - banner._renderNotifications(notification); - - // clear notification without clicking close - banner.cleanNotificationBanner(); // show the same banner again banner._renderNotifications(notification); @@ -186,27 +182,5 @@ define(function (require, exports, module) { banner._renderNotifications(notification); expect(testWindow.$(id).length).toEqual(0); }); - - it("Should show notification if not acknowledged with click on item with notification ack class", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("all", false, true); - banner._renderNotifications(notification); - - // clear notification without clicking close - banner.cleanNotificationBanner(); - - // show the same banner again - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(1); - - // now close the notification by clicking the close icon - testWindow.$(".notification_ack").click(); - expect(testWindow.$(id).length).toEqual(0); - - await awaits(300); - // acknowledged banner should not show the same banner again - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(0); - }); }); }); From 7f08f3b403e222cb4297f30e59af0a4574cea32a Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 12:10:48 +0530 Subject: [PATCH 3/8] chore: support for custom filter for in app notifications and tests --- src/assets/notifications/dev/root/toast.json | 2 - .../InAppNotifications/banner.js | 35 ++++++++ .../Extn-InAppNotifications-integ-test.js | 89 ++++++++++++++++++- 3 files changed, 123 insertions(+), 3 deletions(-) delete mode 100644 src/assets/notifications/dev/root/toast.json diff --git a/src/assets/notifications/dev/root/toast.json b/src/assets/notifications/dev/root/toast.json deleted file mode 100644 index 7a73a41bf..000000000 --- a/src/assets/notifications/dev/root/toast.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/src/extensionsIntegrated/InAppNotifications/banner.js b/src/extensionsIntegrated/InAppNotifications/banner.js index d6c01f669..211969036 100644 --- a/src/extensionsIntegrated/InAppNotifications/banner.js +++ b/src/extensionsIntegrated/InAppNotifications/banner.js @@ -35,6 +35,9 @@ define(function (require, exports, module) { ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); + let latestBannerJSON; + let customFilterCallback; + // duration of one day in milliseconds const ONE_DAY = 1000 * 60 * 60 * 24; const IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE = "InAppNotificationsBannerShown"; @@ -64,6 +67,14 @@ define(function (require, exports, module) { return false; } + /** + * Registers a custom filter callback function for notifications + * @param {Function} cfbn - async function that filters notifications + */ + function registerCustomFilter(cfbn) { + customFilterCallback = cfbn; + } + /** * If there are multiple notifications, thew will be shown one after the other and not all at once. * A sample notifications is as follows: @@ -112,6 +123,9 @@ define(function (require, exports, module) { if(!_isValidForThisPlatform(notification.PLATFORM)){ continue; } + if(customFilterCallback && !(await customFilterCallback(notification, notificationID))){ + continue; + } if(!notification.DANGER_SHOW_ON_EVERY_BOOT){ // One time notification. mark as shown and never show again // all notifications are one time, we track metrics for each notification separately @@ -136,6 +150,12 @@ define(function (require, exports, module) { return null; } return response.json(); + }) + .then(json => { + if (json !== null) { + latestBannerJSON = json; + } + return json; }); } @@ -167,6 +187,15 @@ define(function (require, exports, module) { }); } + /** + * Re-renders notifications using the latest cached banner JSON + */ + function reRenderNotifications() { + if(latestBannerJSON) { + _renderNotifications(latestBannerJSON); + } + } + /** * Removes and cleans up the notification bar from DOM @@ -232,8 +261,14 @@ define(function (require, exports, module) { setInterval(_fetchAndRenderNotifications, ONE_DAY); }); + exports.registerCustomFilter = registerCustomFilter; + exports.reRenderNotifications = reRenderNotifications; + if(Phoenix.isTestWindow){ exports.cleanNotificationBanner = cleanNotificationBanner; exports._renderNotifications = _renderNotifications; + exports._setBannerCache = function(notifications) { + latestBannerJSON = notifications; + }; } }); diff --git a/test/spec/Extn-InAppNotifications-integ-test.js b/test/spec/Extn-InAppNotifications-integ-test.js index 9f946a3dc..28cd5c68f 100644 --- a/test/spec/Extn-InAppNotifications-integ-test.js +++ b/test/spec/Extn-InAppNotifications-integ-test.js @@ -19,7 +19,7 @@ * */ -/*global describe, it, expect, beforeAll, afterAll, awaits, Phoenix */ +/*global describe, it, expect, beforeAll, afterAll, awaits, awaitsFor */ define(function (require, exports, module) { // Recommended to avoid reloading the integration test window Phoenix instance for each test. @@ -40,6 +40,12 @@ define(function (require, exports, module) { await SpecRunnerUtils.loadProjectInTestWindow(testPath); }, 30000); + async function _waitForBannerShown() { + await awaitsFor(function () { + return testWindow.$('#notification-bar').is(":visible"); + }, "banner to be shown"); + } + afterAll(async function () { testWindow = null; // comment out below line if you want to debug the test window post running tests @@ -182,5 +188,86 @@ define(function (require, exports, module) { banner._renderNotifications(notification); expect(testWindow.$(id).length).toEqual(0); }); + + it("Should apply custom filter to block notification", async function () { + banner.cleanNotificationBanner(); + banner.registerCustomFilter(async () => false); + + const {notification, id} = getRandomNotification("all", true); + banner._renderNotifications(notification); + await awaits(50); + + expect(testWindow.$('#notification-bar').is(":visible")).toBe(false); + expect(testWindow.$(id).length).toEqual(0); + + // Cleanup: remove custom filter + banner.registerCustomFilter(null); + }); + + it("Should apply custom filter to allow notification", async function () { + banner.cleanNotificationBanner(); + banner.registerCustomFilter(async () => true); + + const {notification, id} = getRandomNotification("all", true); + banner._renderNotifications(notification); + await _waitForBannerShown(); + + expect(testWindow.$(id).length).toEqual(1); + + // Cleanup + banner.registerCustomFilter(null); + banner.cleanNotificationBanner(); + }); + + it("Should pass correct parameters to custom filter", async function () { + banner.cleanNotificationBanner(); + let receivedNotification, receivedID; + + const {notification} = getRandomNotification("all", true); + const expectedID = Object.keys(notification)[0]; + + banner.registerCustomFilter(async (notif, notifID) => { + receivedNotification = notif; + receivedID = notifID; + return true; + }); + + banner._renderNotifications(notification); + await _waitForBannerShown(); + + expect(receivedID).toEqual(expectedID); + expect(receivedNotification).toEqual(notification[expectedID]); + + // Cleanup + banner.registerCustomFilter(null); + banner.cleanNotificationBanner(); + }); + + it("Should apply custom filter on reRenderNotifications", async function () { + banner.cleanNotificationBanner(); + + const {notification, id} = getRandomNotification("all", true); + + // Set cache and render + banner._setBannerCache(notification); + banner._renderNotifications(notification); + await _waitForBannerShown(); + expect(testWindow.$(id).length).toEqual(1); + + banner.cleanNotificationBanner(); + + // Set filter to block + banner.registerCustomFilter(async () => false); + + // Re-render should not show notification due to filter + banner.reRenderNotifications(); + await awaits(50); + expect(testWindow.$('#notification-bar').is(":visible")).toBe(false); + expect(testWindow.$(id).length).toEqual(0); + + // Cleanup + banner.registerCustomFilter(null); + banner.cleanNotificationBanner(); + }); }); }); From 4ace00550a28771c7ce088d2f0314c73e2f9a9d8 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 12:13:25 +0530 Subject: [PATCH 4/8] chore: update deps --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index dcc6cd8ce..6b115d5c4 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "ef3d7518e411d9ca6c9938af2bc0d5cd8d791e34" + "commitID": "427d40ce3b176fa707ac0ab04ab9b58a4d0b4706" } } From fd719c960f74864069e877ba7a93f769b60734bc Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 12:43:55 +0530 Subject: [PATCH 5/8] chore: support for pro flags in app notifications --- src/assets/notifications/dev/root/banner.json | 3 +- .../InAppNotifications/banner.js | 14 ++-- .../Extn-InAppNotifications-integ-test.js | 69 +++++++++++++++++-- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/assets/notifications/dev/root/banner.json b/src/assets/notifications/dev/root/banner.json index 45f2b5f0c..f9bb7a0ea 100644 --- a/src/assets/notifications/dev/root/banner.json +++ b/src/assets/notifications/dev/root/banner.json @@ -1,8 +1,9 @@ { "WELCOME_DEVELOPER": { + "PRO_EDITION_ONLY" : false, "DANGER_SHOW_ON_EVERY_BOOT" : false, "HTML_CONTENT": "
Welcome to Phoenix Code dev community! Click here to chat with our Discord Community.
", "FOR_VERSIONS": ">=3.0.0", "PLATFORM" : "all" } -} \ No newline at end of file +} diff --git a/src/extensionsIntegrated/InAppNotifications/banner.js b/src/extensionsIntegrated/InAppNotifications/banner.js index 211969036..bce96db0a 100644 --- a/src/extensionsIntegrated/InAppNotifications/banner.js +++ b/src/extensionsIntegrated/InAppNotifications/banner.js @@ -80,6 +80,7 @@ define(function (require, exports, module) { * A sample notifications is as follows: * { * "SAMPLE_NOTIFICATION_NAME": { + * "PRO_EDITION_ONLY" : false, * "DANGER_SHOW_ON_EVERY_BOOT" : false, * "HTML_CONTENT": "", * "FOR_VERSIONS": "1.x || >=2.5.0 || 5.0.0 - 7.2.3", @@ -91,16 +92,17 @@ define(function (require, exports, module) { * or there is an html element with class `notification_ack`. * * 1. `SAMPLE_NOTIFICATION_NAME` : This is a unique ID. It is used to check if the notification was shown to user. - * 2. `DANGER_SHOW_ON_EVERY_BOOT` : (Default false) Setting this to true will cause the + * 2. `PRO_EDITION_ONLY` : (Default false) Setting this to true will not show the notification on community editions + * 3. `DANGER_SHOW_ON_EVERY_BOOT` : (Default false) Setting this to true will cause the * notification to be shown on every boot. This is bad ux and only be used if there is a critical security issue * that we want the version not to be used. - * 3. `HTML_CONTENT`: The actual html content to show to the user. It can have an optional `notification_ack` class. + * 4. `HTML_CONTENT`: The actual html content to show to the user. It can have an optional `notification_ack` class. * Setting this class will cause the notification to be shown once a day until the user explicitly clicks * on any html element with class `notification_ack` or explicitly click the close button. * If such a class is not present, then the notification is shown only once ever. - * 4. `FOR_VERSIONS` : [Semver compatible version filter](https://www.npmjs.com/package/semver). + * 5. `FOR_VERSIONS` : [Semver compatible version filter](https://www.npmjs.com/package/semver). * The notification will be shown to all versions satisfying this. - * 5. `PLATFORM`: A comma seperated list of all platforms in which the message will be shown. + * 6. `PLATFORM`: A comma seperated list of all platforms in which the message will be shown. * allowed values are: `mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all` * @param notifications * @returns {false|*} @@ -113,6 +115,7 @@ define(function (require, exports, module) { const _InAppBannerShownAndDone = PreferencesManager.getViewState( IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE); + const isProEdition = Phoenix.pro && Phoenix.pro.commitID; for(const notificationID of Object.keys(notifications)){ if(!_InAppBannerShownAndDone[notificationID]) { @@ -126,6 +129,9 @@ define(function (require, exports, module) { if(customFilterCallback && !(await customFilterCallback(notification, notificationID))){ continue; } + if(!isProEdition && notification.PRO_EDITION_ONLY){ + continue; + } if(!notification.DANGER_SHOW_ON_EVERY_BOOT){ // One time notification. mark as shown and never show again // all notifications are one time, we track metrics for each notification separately diff --git a/test/spec/Extn-InAppNotifications-integ-test.js b/test/spec/Extn-InAppNotifications-integ-test.js index 28cd5c68f..04e74cfe5 100644 --- a/test/spec/Extn-InAppNotifications-integ-test.js +++ b/test/spec/Extn-InAppNotifications-integ-test.js @@ -19,7 +19,7 @@ * */ -/*global describe, it, expect, beforeAll, afterAll, awaits, awaitsFor */ +/*global describe, it, expect, beforeAll, beforeEach, afterEach, afterAll, awaits, awaitsFor */ define(function (require, exports, module) { // Recommended to avoid reloading the integration test window Phoenix instance for each test. @@ -28,7 +28,7 @@ define(function (require, exports, module) { const testPath = SpecRunnerUtils.getTestPath("/spec/JSUtils-test-files"); - let testWindow, banner; + let testWindow, banner, originalPhoenixPro; describe("integration:In App notification banner integration tests", function () { @@ -40,6 +40,16 @@ define(function (require, exports, module) { await SpecRunnerUtils.loadProjectInTestWindow(testPath); }, 30000); + beforeEach(function () { + // Save original Phoenix.pro before each test + originalPhoenixPro = testWindow.Phoenix.pro; + }); + + afterEach(function () { + // Restore Phoenix.pro after each test (even if test fails) + testWindow.Phoenix.pro = originalPhoenixPro; + }); + async function _waitForBannerShown() { await awaitsFor(function () { return testWindow.$('#notification-bar').is(":visible"); @@ -52,16 +62,20 @@ define(function (require, exports, module) { await SpecRunnerUtils.closeTestWindow(); }, 30000); - function getRandomNotification(platform, showOnEveryBoot=false, ack = false) { + function getRandomNotification(platform, showOnEveryBoot=false, ack = false, proOnly = false) { const notification = {}; const id = crypto.randomUUID(); const ackClass = ack? "notification_ack" : ''; - notification[id] = { + const notificationObj = { "DANGER_SHOW_ON_EVERY_BOOT": showOnEveryBoot, "HTML_CONTENT": `
random notification ${platform} with id ${id}, DANGER_SHOW_ON_EVERY_BOOT: ${showOnEveryBoot}, ack:${ack}
`, "FOR_VERSIONS": ">=3.0.0", "PLATFORM": platform || "all" }; + if (proOnly) { + notificationObj.PRO_EDITION_ONLY = true; + } + notification[id] = notificationObj; return {notification, id: `#${id}`}; } @@ -189,6 +203,53 @@ define(function (require, exports, module) { expect(testWindow.$(id).length).toEqual(0); }); + it("Should show PRO_EDITION_ONLY notification in pro edition", async function () { + banner.cleanNotificationBanner(); + + // Mock pro edition + testWindow.Phoenix.pro = { commitID: "test-pro-commit" }; + + const {notification, id} = getRandomNotification("all", true, false, true); + banner._renderNotifications(notification); + + expect(testWindow.$(id).length).toEqual(1); + + banner.cleanNotificationBanner(); + }); + + it("Should not show PRO_EDITION_ONLY notification in community edition", async function () { + banner.cleanNotificationBanner(); + + // Mock community edition + testWindow.Phoenix.pro = null; + + const {notification, id} = getRandomNotification("all", true, false, true); + banner._renderNotifications(notification); + await awaits(50); + + expect(testWindow.$(id).length).toEqual(0); + + banner.cleanNotificationBanner(); + }); + + it("Should show non-PRO_EDITION_ONLY notification in all editions", async function () { + const {notification, id} = getRandomNotification("all", true, false, false); + + // Test in pro edition + banner.cleanNotificationBanner(); + testWindow.Phoenix.pro = { commitID: "test-pro-commit" }; + banner._renderNotifications(notification); + expect(testWindow.$(id).length).toEqual(1); + banner.cleanNotificationBanner(); + expect(testWindow.$(id).length).toEqual(0); + + // Test in community edition + testWindow.Phoenix.pro = null; + banner._renderNotifications(notification); + expect(testWindow.$(id).length).toEqual(1); + banner.cleanNotificationBanner(); + }); + it("Should apply custom filter to block notification", async function () { banner.cleanNotificationBanner(); banner.registerCustomFilter(async () => false); From f7991e708e587d04d72f3e70ff3de6e172cd0a9a Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 13:22:03 +0530 Subject: [PATCH 6/8] fix: race, only one banner should be on screen at a time even in rerender --- .../InAppNotifications/banner.js | 21 +++++++-- .../Extn-InAppNotifications-integ-test.js | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/extensionsIntegrated/InAppNotifications/banner.js b/src/extensionsIntegrated/InAppNotifications/banner.js index bce96db0a..97c3eb785 100644 --- a/src/extensionsIntegrated/InAppNotifications/banner.js +++ b/src/extensionsIntegrated/InAppNotifications/banner.js @@ -19,7 +19,7 @@ * */ -/*global Phoenix*/ +/*global*/ /** * module for displaying in-app banner notifications * @@ -31,10 +31,14 @@ define(function (require, exports, module) { ExtensionUtils = require("utils/ExtensionUtils"), Metrics = require("utils/Metrics"), semver = require("thirdparty/semver.browser"), - NotificationBarHtml = require("text!./htmlContent/notificationContainer.html"); + NotificationBarHtml = require("text!./htmlContent/notificationContainer.html"), + ExtensionInterface = require("utils/ExtensionInterface"); ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); + const IN_APP_NOTIFICATION_INTERFACE = "Extn.Phoenix.inAppNotification"; + ExtensionInterface.registerExtensionInterface(IN_APP_NOTIFICATION_INTERFACE, exports); + let latestBannerJSON; let customFilterCallback; @@ -193,13 +197,22 @@ define(function (require, exports, module) { }); } + let reRenderInProgress = Promise.resolve(); + /** * Re-renders notifications using the latest cached banner JSON + * Ensures renders are strictly serialized */ function reRenderNotifications() { - if(latestBannerJSON) { - _renderNotifications(latestBannerJSON); + if (!latestBannerJSON) { + return Promise.resolve(); } + + reRenderInProgress = reRenderInProgress + .catch(() => {}) // prevent lock break on error + .then(() => _renderNotifications(latestBannerJSON)); + + return reRenderInProgress; } diff --git a/test/spec/Extn-InAppNotifications-integ-test.js b/test/spec/Extn-InAppNotifications-integ-test.js index 04e74cfe5..68e9ada6f 100644 --- a/test/spec/Extn-InAppNotifications-integ-test.js +++ b/test/spec/Extn-InAppNotifications-integ-test.js @@ -330,5 +330,48 @@ define(function (require, exports, module) { banner.registerCustomFilter(null); banner.cleanNotificationBanner(); }); + + it("Should serialize multiple concurrent reRenderNotifications calls", async function () { + banner.cleanNotificationBanner(); + + const {notification} = getRandomNotification("all", true); + let renderCount = 0; + + // Set cache + banner._setBannerCache(notification); + + // Register filter to track render calls + banner.registerCustomFilter(async () => { + renderCount++; + return true; + }); + + // Make 3 concurrent calls + const promise1 = banner.reRenderNotifications(); + const promise2 = banner.reRenderNotifications(); + const promise3 = banner.reRenderNotifications(); + + // First render: wait for banner, close it, wait for promise to resolve + await _waitForBannerShown(); + expect(renderCount).toEqual(1); + testWindow.$('.close-icon').click(); + await promise1; + + // Second render: wait for banner, close it, wait for promise to resolve + await _waitForBannerShown(); + expect(renderCount).toEqual(2); + testWindow.$('.close-icon').click(); + await promise2; + + // Third render: wait for banner, close it, wait for promise to resolve + await _waitForBannerShown(); + expect(renderCount).toEqual(3); + testWindow.$('.close-icon').click(); + await promise3; + + // Cleanup + banner.registerCustomFilter(null); + banner.cleanNotificationBanner(); + }); }); }); From 5208706287d5e26b83877a5f42ef6e5337ce8f52 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 13:29:16 +0530 Subject: [PATCH 7/8] test: race, multiple rerenders should only show the same notification once --- .../Extn-InAppNotifications-integ-test.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/spec/Extn-InAppNotifications-integ-test.js b/test/spec/Extn-InAppNotifications-integ-test.js index 68e9ada6f..d5c3c04b2 100644 --- a/test/spec/Extn-InAppNotifications-integ-test.js +++ b/test/spec/Extn-InAppNotifications-integ-test.js @@ -373,5 +373,44 @@ define(function (require, exports, module) { banner.registerCustomFilter(null); banner.cleanNotificationBanner(); }); + + it("Should show notification only once even with multiple concurrent reRenderNotifications calls", async function () { + banner.cleanNotificationBanner(); + + const {notification} = getRandomNotification("all", false); // NOT DANGER_SHOW_ON_EVERY_BOOT + let renderCount = 0; + + // Set cache + banner._setBannerCache(notification); + + // Register filter to track render calls + banner.registerCustomFilter(async () => { + renderCount++; + return true; + }); + + // Make 3 concurrent calls + const promise1 = banner.reRenderNotifications(); + const promise2 = banner.reRenderNotifications(); + const promise3 = banner.reRenderNotifications(); + + // Only first render should show banner + await _waitForBannerShown(); + expect(renderCount).toEqual(1); + testWindow.$('.close-icon').click(); + await promise1; + + // Second and third should resolve without showing banner + await awaits(50); + await promise2; + await promise3; + + // Filter should only have been called once + expect(renderCount).toEqual(1); + expect(testWindow.$('#notification-bar').is(":visible")).toBe(false); + + // Cleanup + banner.registerCustomFilter(null); + }); }); }); From 80576c9ae62ad8c0c372cea9f0c48265afacff12 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 29 Dec 2025 15:45:20 +0530 Subject: [PATCH 8/8] refactor: remove app notification code as its in the pro extension now --- src/assets/notifications/dev/root/banner.json | 1 - .../InAppNotifications/banner.js | 293 ------------ .../htmlContent/notificationContainer.html | 7 - .../InAppNotifications/main.js | 28 -- .../InAppNotifications/styles/styles.css | 55 --- src/extensionsIntegrated/loader.js | 1 - test/UnitTestSuite.js | 1 - .../Extn-InAppNotifications-integ-test.js | 416 ------------------ 8 files changed, 802 deletions(-) delete mode 100644 src/extensionsIntegrated/InAppNotifications/banner.js delete mode 100644 src/extensionsIntegrated/InAppNotifications/htmlContent/notificationContainer.html delete mode 100644 src/extensionsIntegrated/InAppNotifications/main.js delete mode 100644 src/extensionsIntegrated/InAppNotifications/styles/styles.css delete mode 100644 test/spec/Extn-InAppNotifications-integ-test.js diff --git a/src/assets/notifications/dev/root/banner.json b/src/assets/notifications/dev/root/banner.json index f9bb7a0ea..dec52ecbd 100644 --- a/src/assets/notifications/dev/root/banner.json +++ b/src/assets/notifications/dev/root/banner.json @@ -1,6 +1,5 @@ { "WELCOME_DEVELOPER": { - "PRO_EDITION_ONLY" : false, "DANGER_SHOW_ON_EVERY_BOOT" : false, "HTML_CONTENT": "
Welcome to Phoenix Code dev community! Click here to chat with our Discord Community.
", "FOR_VERSIONS": ">=3.0.0", diff --git a/src/extensionsIntegrated/InAppNotifications/banner.js b/src/extensionsIntegrated/InAppNotifications/banner.js deleted file mode 100644 index 97c3eb785..000000000 --- a/src/extensionsIntegrated/InAppNotifications/banner.js +++ /dev/null @@ -1,293 +0,0 @@ -/* - * GNU AGPL-3.0 License - * - * Copyright (c) 2021 - present core.ai . All rights reserved. - * Original work Copyright (c) 2018 - 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*/ -/** - * module for displaying in-app banner notifications - * - */ -define(function (require, exports, module) { - - const AppInit = require("utils/AppInit"), - PreferencesManager = require("preferences/PreferencesManager"), - ExtensionUtils = require("utils/ExtensionUtils"), - Metrics = require("utils/Metrics"), - semver = require("thirdparty/semver.browser"), - NotificationBarHtml = require("text!./htmlContent/notificationContainer.html"), - ExtensionInterface = require("utils/ExtensionInterface"); - - ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); - - const IN_APP_NOTIFICATION_INTERFACE = "Extn.Phoenix.inAppNotification"; - ExtensionInterface.registerExtensionInterface(IN_APP_NOTIFICATION_INTERFACE, exports); - - let latestBannerJSON; - let customFilterCallback; - - // duration of one day in milliseconds - const ONE_DAY = 1000 * 60 * 60 * 24; - const IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE = "InAppNotificationsBannerShown"; - const NOTIFICATION_ACK_CLASS = "notification_ack"; - - // Init default last notification number - PreferencesManager.stateManager.definePreference(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE, - "object", {}); - - function _isValidForThisVersion(versionFilter) { - return semver.satisfies(brackets.metadata.apiVersion, versionFilter); - } - - // platformFilter is a string subset of - // "mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all" - function _isValidForThisPlatform(platformFilter) { - platformFilter = platformFilter.split(","); - if(platformFilter.includes("all") - || (platformFilter.includes(brackets.platform) && Phoenix.isNativeApp) // win linux and mac is only for tauri and not for browser in platform - || (platformFilter.includes("allDesktop") && Phoenix.isNativeApp) - || (platformFilter.includes("firefox") && Phoenix.browser.desktop.isFirefox && !Phoenix.isNativeApp) - || (platformFilter.includes("chrome") && Phoenix.browser.desktop.isChromeBased && !Phoenix.isNativeApp) - || (platformFilter.includes("safari") && Phoenix.browser.desktop.isSafari && !Phoenix.isNativeApp) - || (platformFilter.includes("allBrowser") && !Phoenix.isNativeApp)){ - return true; - } - return false; - } - - /** - * Registers a custom filter callback function for notifications - * @param {Function} cfbn - async function that filters notifications - */ - function registerCustomFilter(cfbn) { - customFilterCallback = cfbn; - } - - /** - * If there are multiple notifications, thew will be shown one after the other and not all at once. - * A sample notifications is as follows: - * { - * "SAMPLE_NOTIFICATION_NAME": { - * "PRO_EDITION_ONLY" : false, - * "DANGER_SHOW_ON_EVERY_BOOT" : false, - * "HTML_CONTENT": "", - * "FOR_VERSIONS": "1.x || >=2.5.0 || 5.0.0 - 7.2.3", - * "PLATFORM" : "allDesktop" - * }, - * "ANOTHER_SAMPLE_NOTIFICATION_NAME": {etc} - * } - * By default, a notification is shown only once except if `DANGER_SHOW_ON_EVERY_BOOT` is set - * or there is an html element with class `notification_ack`. - * - * 1. `SAMPLE_NOTIFICATION_NAME` : This is a unique ID. It is used to check if the notification was shown to user. - * 2. `PRO_EDITION_ONLY` : (Default false) Setting this to true will not show the notification on community editions - * 3. `DANGER_SHOW_ON_EVERY_BOOT` : (Default false) Setting this to true will cause the - * notification to be shown on every boot. This is bad ux and only be used if there is a critical security issue - * that we want the version not to be used. - * 4. `HTML_CONTENT`: The actual html content to show to the user. It can have an optional `notification_ack` class. - * Setting this class will cause the notification to be shown once a day until the user explicitly clicks - * on any html element with class `notification_ack` or explicitly click the close button. - * If such a class is not present, then the notification is shown only once ever. - * 5. `FOR_VERSIONS` : [Semver compatible version filter](https://www.npmjs.com/package/semver). - * The notification will be shown to all versions satisfying this. - * 6. `PLATFORM`: A comma seperated list of all platforms in which the message will be shown. - * allowed values are: `mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all` - * @param notifications - * @returns {false|*} - * @private - */ - async function _renderNotifications(notifications) { - if(!notifications) { - return; // nothing to show here - } - - const _InAppBannerShownAndDone = PreferencesManager.getViewState( - IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE); - const isProEdition = Phoenix.pro && Phoenix.pro.commitID; - - for(const notificationID of Object.keys(notifications)){ - if(!_InAppBannerShownAndDone[notificationID]) { - const notification = notifications[notificationID]; - if(!_isValidForThisVersion(notification.FOR_VERSIONS)){ - continue; - } - if(!_isValidForThisPlatform(notification.PLATFORM)){ - continue; - } - if(customFilterCallback && !(await customFilterCallback(notification, notificationID))){ - continue; - } - if(!isProEdition && notification.PRO_EDITION_ONLY){ - continue; - } - if(!notification.DANGER_SHOW_ON_EVERY_BOOT){ - // One time notification. mark as shown and never show again - // all notifications are one time, we track metrics for each notification separately - _markAsShownAndDone(notificationID); - } - await showBannerAndWaitForDismiss(notification.HTML_CONTENT, notificationID); - } - } - } - - function _markAsShownAndDone(notificationID) { - const _InAppBannersShownAndDone = PreferencesManager.getViewState(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE); - _InAppBannersShownAndDone[notificationID] = true; - PreferencesManager.setViewState(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE, - _InAppBannersShownAndDone); - } - - function fetchJSON(url) { - return fetch(url) - .then(response => { - if (!response.ok) { - return null; - } - return response.json(); - }) - .then(json => { - if (json !== null) { - latestBannerJSON = json; - } - return json; - }); - } - - let inProgress = false; - function _fetchAndRenderNotifications() { - if(inProgress){ - return; - } - inProgress = true; - const locale = brackets.getLocale(); // en-US default - const fetchURL = `${brackets.config.app_notification_url}${locale}/banner.json`; - const defaultFetchURL = `${brackets.config.app_notification_url}root/banner.json`; - // Fetch data from fetchURL first - fetchJSON(fetchURL) - .then(fetchedJSON => { - // Check if fetchedJSON is empty or undefined - if (fetchedJSON === null) { - // Fetch data from defaultFetchURL if fetchURL didn't provide data - return fetchJSON(defaultFetchURL); - } - return fetchedJSON; - }) - .then(_renderNotifications) // Call the render function with the fetched JSON data - .catch(error => { - console.error(`Error fetching and rendering banner.json`, error); - }) - .finally(()=>{ - inProgress = false; - }); - } - - let reRenderInProgress = Promise.resolve(); - - /** - * Re-renders notifications using the latest cached banner JSON - * Ensures renders are strictly serialized - */ - function reRenderNotifications() { - if (!latestBannerJSON) { - return Promise.resolve(); - } - - reRenderInProgress = reRenderInProgress - .catch(() => {}) // prevent lock break on error - .then(() => _renderNotifications(latestBannerJSON)); - - return reRenderInProgress; - } - - - /** - * Removes and cleans up the notification bar from DOM - */ - function cleanNotificationBanner() { - const $notificationBar = $('#notification-bar'); - if ($notificationBar.length > 0) { - $notificationBar.remove(); - } - } - - /** - * Displays the Notification Bar UI - * - */ - async function showBannerAndWaitForDismiss(htmlStr, notificationID) { - let resolved = false; - return new Promise((resolve)=>{ - const $htmlContent = $(``), - $notificationBarElement = $(NotificationBarHtml); - - // Remove any SCRIPT tag to avoid secuirity issues - $htmlContent.find('script').remove(); - - // Remove any STYLE tag to avoid styling impact on Brackets DOM - $htmlContent.find('style').remove(); - - cleanNotificationBanner(); //Remove an already existing notification bar, if any - $notificationBarElement.prependTo(".content"); - - var $notificationBar = $('#notification-bar'), - $notificationContent = $notificationBar.find('.content-container'), - $closeIcon = $notificationBar.find('.close-icon'); - - $notificationContent.append($htmlContent); - Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-shown", notificationID); - - // Click handlers on actionable elements - if ($closeIcon.length > 0) { - $closeIcon.click(function () { - cleanNotificationBanner(); - Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-close", notificationID); - !resolved && resolve($htmlContent); - resolved = true; - }); - } - - $notificationBar.find(`.${NOTIFICATION_ACK_CLASS}`).click(function() { - cleanNotificationBanner(); - Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-ack", notificationID); - !resolved && resolve($htmlContent); - resolved = true; - }); - }); - } - - - AppInit.appReady(function () { - if(Phoenix.isTestWindow) { - return; - } - _fetchAndRenderNotifications(); - setInterval(_fetchAndRenderNotifications, ONE_DAY); - }); - - exports.registerCustomFilter = registerCustomFilter; - exports.reRenderNotifications = reRenderNotifications; - - if(Phoenix.isTestWindow){ - exports.cleanNotificationBanner = cleanNotificationBanner; - exports._renderNotifications = _renderNotifications; - exports._setBannerCache = function(notifications) { - latestBannerJSON = notifications; - }; - } -}); diff --git a/src/extensionsIntegrated/InAppNotifications/htmlContent/notificationContainer.html b/src/extensionsIntegrated/InAppNotifications/htmlContent/notificationContainer.html deleted file mode 100644 index b420dee0e..000000000 --- a/src/extensionsIntegrated/InAppNotifications/htmlContent/notificationContainer.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
-
-
- -
-
diff --git a/src/extensionsIntegrated/InAppNotifications/main.js b/src/extensionsIntegrated/InAppNotifications/main.js deleted file mode 100644 index d9bc23ef0..000000000 --- a/src/extensionsIntegrated/InAppNotifications/main.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * GNU AGPL-3.0 License - * - * Copyright (c) 2021 - present core.ai . All rights reserved. - * Original work Copyright (c) 2018 - 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. - * - */ - -/** - * module for displaying in-app notifications - * - */ -define(function (require, exports, module) { - require("./banner"); -}); diff --git a/src/extensionsIntegrated/InAppNotifications/styles/styles.css b/src/extensionsIntegrated/InAppNotifications/styles/styles.css deleted file mode 100644 index b34d42e1c..000000000 --- a/src/extensionsIntegrated/InAppNotifications/styles/styles.css +++ /dev/null @@ -1,55 +0,0 @@ -#notification-bar { - display: block; - box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.53); - padding: 5px 0px; - width: 100%; - min-height: 39px; - position: absolute; - z-index: 16; - left: 0px; - bottom: 25px; - outline: none; - overflow: hidden; - color: rgb(51, 51, 51); - background-color: rgb(223, 226, 226); -} - -.dark #notification-bar { - color: #ccc; - background: #2c2c2c; -} - -#notification-bar .content-container { - padding: 5px 10px; - float: left; - width: 100%; -} - -#notification-bar .close-icon-container { - height: auto; - position: absolute; - float: right; - text-align: center; - width: auto; - min-width: 66px; - right: 20px; - top: 10px; -} - -#notification-bar .close-icon-container .close-icon { - display: block; - font-size: 18px; - line-height: 18px; - text-decoration: none; - width: 18px; - height: 18px; - background-color: transparent; - border: none; - padding: 0px; /*This is needed to center the icon*/ - float: right; -} - -.dark #notification-bar .close-icon-container .close-icon { - color: #ccc; -} - diff --git a/src/extensionsIntegrated/loader.js b/src/extensionsIntegrated/loader.js index a93c82ef1..c98a86242 100644 --- a/src/extensionsIntegrated/loader.js +++ b/src/extensionsIntegrated/loader.js @@ -33,7 +33,6 @@ define(function (require, exports, module) { require("./RemoteFileAdapter/main"); require("./QuickOpen/main"); require("./Phoenix/main"); - require("./InAppNotifications/main"); require("./NoDistractions/main"); require("./Phoenix-live-preview/main"); require("./NavigationAndHistory/main"); diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index 396cbede2..e3f9a3e52 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -122,7 +122,6 @@ define(function (require, exports, module) { require("spec/LocalizationUtils-test"); require("spec/ScrollTrackHandler-integ-test"); // Integrated extension tests - require("spec/Extn-InAppNotifications-integ-test"); require("spec/Extn-RemoteFileAdapter-integ-test"); require("spec/Extn-NavigationAndHistory-integ-test"); require("spec/Extn-RecentProjects-integ-test"); diff --git a/test/spec/Extn-InAppNotifications-integ-test.js b/test/spec/Extn-InAppNotifications-integ-test.js deleted file mode 100644 index d5c3c04b2..000000000 --- a/test/spec/Extn-InAppNotifications-integ-test.js +++ /dev/null @@ -1,416 +0,0 @@ -/* - * GNU AGPL-3.0 License - * - * Copyright (c) 2021 - present core.ai . All rights reserved. - * Original work Copyright (c) 2012 - 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, beforeAll, beforeEach, afterEach, afterAll, awaits, awaitsFor */ - -define(function (require, exports, module) { - // Recommended to avoid reloading the integration test window Phoenix instance for each test. - - const SpecRunnerUtils = require("spec/SpecRunnerUtils"); - - const testPath = SpecRunnerUtils.getTestPath("/spec/JSUtils-test-files"); - - let testWindow, banner, originalPhoenixPro; - - - describe("integration:In App notification banner integration tests", function () { - - beforeAll(async function () { - testWindow = await SpecRunnerUtils.createTestWindowAndRun(); - banner = testWindow.require("extensionsIntegrated/InAppNotifications/banner"); - - await SpecRunnerUtils.loadProjectInTestWindow(testPath); - }, 30000); - - beforeEach(function () { - // Save original Phoenix.pro before each test - originalPhoenixPro = testWindow.Phoenix.pro; - }); - - afterEach(function () { - // Restore Phoenix.pro after each test (even if test fails) - testWindow.Phoenix.pro = originalPhoenixPro; - }); - - async function _waitForBannerShown() { - await awaitsFor(function () { - return testWindow.$('#notification-bar').is(":visible"); - }, "banner to be shown"); - } - - afterAll(async function () { - testWindow = null; - // comment out below line if you want to debug the test window post running tests - await SpecRunnerUtils.closeTestWindow(); - }, 30000); - - function getRandomNotification(platform, showOnEveryBoot=false, ack = false, proOnly = false) { - const notification = {}; - const id = crypto.randomUUID(); - const ackClass = ack? "notification_ack" : ''; - const notificationObj = { - "DANGER_SHOW_ON_EVERY_BOOT": showOnEveryBoot, - "HTML_CONTENT": `
random notification ${platform} with id ${id}, DANGER_SHOW_ON_EVERY_BOOT: ${showOnEveryBoot}, ack:${ack}
`, - "FOR_VERSIONS": ">=3.0.0", - "PLATFORM": platform || "all" - }; - if (proOnly) { - notificationObj.PRO_EDITION_ONLY = true; - } - notification[id] = notificationObj; - return {notification, id: `#${id}`}; - } - - it("Should show notification only once", async function () { - const {notification, id} = getRandomNotification(); - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(1); - - banner.cleanNotificationBanner(); - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(0); - }); - - function verifyPlatform(platform) { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification(platform); - banner._renderNotifications(notification); - - const isCurrentPlatform = (Phoenix.platform === platform && Phoenix.isNativeApp); - expect(testWindow.$(id).length).toEqual(isCurrentPlatform ? 1 : 0); - } - - it("Should show notification only in windows tauri", async function () { - verifyPlatform("win"); - }); - - it("Should show notification only in linux tauri", async function () { - verifyPlatform("linux"); - }); - - it("Should show notification only in mac tauri", async function () { - verifyPlatform("mac"); - }); - - it("Should show notification only in any desktop tauri", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("allDesktop"); - banner._renderNotifications(notification); - - expect(testWindow.$(id).length).toEqual(Phoenix.isNativeApp ? 1 : 0); - }); - - it("Should show notification only in any win,linux,mac tauri", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("win,linux,mac"); - banner._renderNotifications(notification); - - expect(testWindow.$(id).length).toEqual(Phoenix.isNativeApp ? 1 : 0); - }); - - //firefox,chrome,safari,allBrowser, all - function verifyBrowser(platform) { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification(platform); - banner._renderNotifications(notification); - - let currentPlatform = "chrome"; - if(Phoenix.browser.desktop.isFirefox){ - currentPlatform = "firefox"; - } else if(Phoenix.browser.desktop.isSafari){ - currentPlatform = "safari"; - } - const isCurrentPlatform = (currentPlatform === platform && !Phoenix.isNativeApp); - expect(testWindow.$(id).length).toEqual(isCurrentPlatform ? 1 : 0); - } - - it("Should show notification only in firefox non tauri", async function () { - verifyBrowser("firefox"); - }); - - it("Should show notification only in chrome non tauri", async function () { - verifyBrowser("chrome"); - }); - - it("Should show notification only in safari non tauri", async function () { - verifyBrowser("safari"); - }); - - it("Should show notification only in any browser non tauri", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("allBrowser"); - banner._renderNotifications(notification); - - expect(testWindow.$(id).length).toEqual(!Phoenix.isNativeApp ? 1 : 0); - }); - - it("Should show notification only in any firefox,chrome,safari tauri", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("firefox,chrome,safari"); - banner._renderNotifications(notification); - - expect(testWindow.$(id).length).toEqual(!Phoenix.isNativeApp ? 1 : 0); - }); - - it("Should show notification on every boot", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("all", true); - banner._renderNotifications(notification); - - // now close the notification by clicking the close icon - testWindow.$(".close-icon").click(); - expect(testWindow.$(id).length).toEqual(0); - - await awaits(300); - // show the same banner again - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(1); - }); - - it("Should show notification only once", async function () { - banner.cleanNotificationBanner(); - const {notification, id} = getRandomNotification("all", false, true); - - // show the same banner again - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(1); - - // now close the notification by clicking the close icon - testWindow.$(".close-icon").click(); - expect(testWindow.$(id).length).toEqual(0); - - await awaits(300); - // acknowledged banner should not show the same banner again - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(0); - }); - - it("Should show PRO_EDITION_ONLY notification in pro edition", async function () { - banner.cleanNotificationBanner(); - - // Mock pro edition - testWindow.Phoenix.pro = { commitID: "test-pro-commit" }; - - const {notification, id} = getRandomNotification("all", true, false, true); - banner._renderNotifications(notification); - - expect(testWindow.$(id).length).toEqual(1); - - banner.cleanNotificationBanner(); - }); - - it("Should not show PRO_EDITION_ONLY notification in community edition", async function () { - banner.cleanNotificationBanner(); - - // Mock community edition - testWindow.Phoenix.pro = null; - - const {notification, id} = getRandomNotification("all", true, false, true); - banner._renderNotifications(notification); - await awaits(50); - - expect(testWindow.$(id).length).toEqual(0); - - banner.cleanNotificationBanner(); - }); - - it("Should show non-PRO_EDITION_ONLY notification in all editions", async function () { - const {notification, id} = getRandomNotification("all", true, false, false); - - // Test in pro edition - banner.cleanNotificationBanner(); - testWindow.Phoenix.pro = { commitID: "test-pro-commit" }; - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(1); - banner.cleanNotificationBanner(); - expect(testWindow.$(id).length).toEqual(0); - - // Test in community edition - testWindow.Phoenix.pro = null; - banner._renderNotifications(notification); - expect(testWindow.$(id).length).toEqual(1); - banner.cleanNotificationBanner(); - }); - - it("Should apply custom filter to block notification", async function () { - banner.cleanNotificationBanner(); - banner.registerCustomFilter(async () => false); - - const {notification, id} = getRandomNotification("all", true); - banner._renderNotifications(notification); - await awaits(50); - - expect(testWindow.$('#notification-bar').is(":visible")).toBe(false); - expect(testWindow.$(id).length).toEqual(0); - - // Cleanup: remove custom filter - banner.registerCustomFilter(null); - }); - - it("Should apply custom filter to allow notification", async function () { - banner.cleanNotificationBanner(); - banner.registerCustomFilter(async () => true); - - const {notification, id} = getRandomNotification("all", true); - banner._renderNotifications(notification); - await _waitForBannerShown(); - - expect(testWindow.$(id).length).toEqual(1); - - // Cleanup - banner.registerCustomFilter(null); - banner.cleanNotificationBanner(); - }); - - it("Should pass correct parameters to custom filter", async function () { - banner.cleanNotificationBanner(); - let receivedNotification, receivedID; - - const {notification} = getRandomNotification("all", true); - const expectedID = Object.keys(notification)[0]; - - banner.registerCustomFilter(async (notif, notifID) => { - receivedNotification = notif; - receivedID = notifID; - return true; - }); - - banner._renderNotifications(notification); - await _waitForBannerShown(); - - expect(receivedID).toEqual(expectedID); - expect(receivedNotification).toEqual(notification[expectedID]); - - // Cleanup - banner.registerCustomFilter(null); - banner.cleanNotificationBanner(); - }); - - it("Should apply custom filter on reRenderNotifications", async function () { - banner.cleanNotificationBanner(); - - const {notification, id} = getRandomNotification("all", true); - - // Set cache and render - banner._setBannerCache(notification); - banner._renderNotifications(notification); - await _waitForBannerShown(); - expect(testWindow.$(id).length).toEqual(1); - - banner.cleanNotificationBanner(); - - // Set filter to block - banner.registerCustomFilter(async () => false); - - // Re-render should not show notification due to filter - banner.reRenderNotifications(); - await awaits(50); - expect(testWindow.$('#notification-bar').is(":visible")).toBe(false); - expect(testWindow.$(id).length).toEqual(0); - - // Cleanup - banner.registerCustomFilter(null); - banner.cleanNotificationBanner(); - }); - - it("Should serialize multiple concurrent reRenderNotifications calls", async function () { - banner.cleanNotificationBanner(); - - const {notification} = getRandomNotification("all", true); - let renderCount = 0; - - // Set cache - banner._setBannerCache(notification); - - // Register filter to track render calls - banner.registerCustomFilter(async () => { - renderCount++; - return true; - }); - - // Make 3 concurrent calls - const promise1 = banner.reRenderNotifications(); - const promise2 = banner.reRenderNotifications(); - const promise3 = banner.reRenderNotifications(); - - // First render: wait for banner, close it, wait for promise to resolve - await _waitForBannerShown(); - expect(renderCount).toEqual(1); - testWindow.$('.close-icon').click(); - await promise1; - - // Second render: wait for banner, close it, wait for promise to resolve - await _waitForBannerShown(); - expect(renderCount).toEqual(2); - testWindow.$('.close-icon').click(); - await promise2; - - // Third render: wait for banner, close it, wait for promise to resolve - await _waitForBannerShown(); - expect(renderCount).toEqual(3); - testWindow.$('.close-icon').click(); - await promise3; - - // Cleanup - banner.registerCustomFilter(null); - banner.cleanNotificationBanner(); - }); - - it("Should show notification only once even with multiple concurrent reRenderNotifications calls", async function () { - banner.cleanNotificationBanner(); - - const {notification} = getRandomNotification("all", false); // NOT DANGER_SHOW_ON_EVERY_BOOT - let renderCount = 0; - - // Set cache - banner._setBannerCache(notification); - - // Register filter to track render calls - banner.registerCustomFilter(async () => { - renderCount++; - return true; - }); - - // Make 3 concurrent calls - const promise1 = banner.reRenderNotifications(); - const promise2 = banner.reRenderNotifications(); - const promise3 = banner.reRenderNotifications(); - - // Only first render should show banner - await _waitForBannerShown(); - expect(renderCount).toEqual(1); - testWindow.$('.close-icon').click(); - await promise1; - - // Second and third should resolve without showing banner - await awaits(50); - await promise2; - await promise3; - - // Filter should only have been called once - expect(renderCount).toEqual(1); - expect(testWindow.$('#notification-bar').is(":visible")).toBe(false); - - // Cleanup - banner.registerCustomFilter(null); - }); - }); -});