From 4f4365904171e5e97b289e37cb9063441c990b0a Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 17 Sep 2025 10:50:42 +0530 Subject: [PATCH 1/4] fix: browser logout cleanup wasnt proper and integ tests that caught it --- src/services/login-browser.js | 2 + test/spec/login-browser-integ-test.js | 606 ++++++++++++++++++++++---- 2 files changed, 530 insertions(+), 78 deletions(-) diff --git a/src/services/login-browser.js b/src/services/login-browser.js index 159d754f7b..abe11bb340 100644 --- a/src/services/login-browser.js +++ b/src/services/login-browser.js @@ -344,6 +344,7 @@ define(function (require, exports, module) { // Always reset local state regardless of server response await _resetBrowserLogin(); + await _verifyBrowserLogin(); if (response.ok) { const result = await response.json(); @@ -373,6 +374,7 @@ define(function (require, exports, module) { } catch (error) { // Always reset local state even on network error await _resetBrowserLogin(); + await _verifyBrowserLogin(); console.error("Network error during logout:", error); const dialog = Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, diff --git a/test/spec/login-browser-integ-test.js b/test/spec/login-browser-integ-test.js index d62f537b6a..37d4e69cb3 100644 --- a/test/spec/login-browser-integ-test.js +++ b/test/spec/login-browser-integ-test.js @@ -37,6 +37,7 @@ define(function (require, exports, module) { let testWindow, LoginServiceExports, LoginBrowserExports, + ProDialogsExports, originalOpen, originalFetch; @@ -47,7 +48,8 @@ define(function (require, exports, module) { await awaitsFor( function () { return testWindow._test_login_service_exports && - testWindow._test_login_browser_exports; + testWindow._test_login_browser_exports && + testWindow._test_pro_dlg_login_exports; }, "Test exports to be available", 5000 @@ -56,12 +58,11 @@ define(function (require, exports, module) { // Access the login service exports from the test window LoginServiceExports = testWindow._test_login_service_exports; LoginBrowserExports = testWindow._test_login_browser_exports; + ProDialogsExports = testWindow._test_pro_dlg_login_exports; // Store original functions for restoration originalOpen = testWindow.open; - if (LoginServiceExports.setFetchFn) { - originalFetch = testWindow.fetch; - } + originalFetch = testWindow.fetch; // Wait for profile menu to be initialized await awaitsFor( @@ -75,23 +76,17 @@ define(function (require, exports, module) { afterAll(async function () { // Restore original functions - if (originalOpen) { - testWindow.open = originalOpen; - } + testWindow.open = originalOpen; // Restore all fetch function overrides - if (originalFetch) { - if (LoginServiceExports && LoginServiceExports.setFetchFn) { - LoginServiceExports.setFetchFn(originalFetch); - } - if (LoginBrowserExports && LoginBrowserExports.setFetchFn) { - LoginBrowserExports.setFetchFn(originalFetch); - } - } + LoginServiceExports.setFetchFn(originalFetch); + LoginBrowserExports.setFetchFn(originalFetch); + ProDialogsExports.setFetchFn(originalFetch); testWindow = null; LoginServiceExports = null; LoginBrowserExports = null; + ProDialogsExports = null; originalOpen = null; originalFetch = null; await SpecRunnerUtils.closeTestWindow(); @@ -147,72 +142,68 @@ define(function (require, exports, module) { console.log("llgT: Setting up fetch mock EARLY using login-browser exports"); console.log("llgT: LoginBrowserExports.setFetchFn available?", !!LoginBrowserExports.setFetchFn); - if (LoginBrowserExports.setFetchFn) { - // Track sign out state for proper mock responses - let userSignedOut = false; - - LoginBrowserExports.setFetchFn((url, options) => { - console.log("llgT: login-browser fetchFn called with URL:", url); - console.log("llgT: login-browser fetchFn called with options:", options); - - // Handle different endpoints - if (url.includes('/resolveBrowserSession')) { - // Login verification endpoint - if (userSignedOut) { - console.log("llgT: User is signed out, returning 401 for resolveBrowserSession"); - return Promise.resolve({ - ok: false, - status: 401, // Unauthorized - user is logged out - json: () => Promise.resolve({ - isSuccess: false - }) - }); - } else { - console.log("llgT: User is signed in, returning success for resolveBrowserSession"); - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ - isSuccess: true, - email: "test@example.com", - firstName: "Test", - lastName: "User", - customerID: "test-customer-id", - loginTime: Date.now(), - profileIcon: { - initials: "TU", - color: "#14b8a6" - } - }) - }); - } - } else if (url.includes('/signOut')) { - // Logout endpoint - set signed out state - console.log("llgT: Handling signOut endpoint call, marking user as signed out"); - userSignedOut = true; + // Track sign out state for proper mock responses + let userSignedOut = false; + + LoginBrowserExports.setFetchFn((url, options) => { + console.log("llgT: login-browser fetchFn called with URL:", url); + console.log("llgT: login-browser fetchFn called with options:", options); + + // Handle different endpoints + if (url.includes('/resolveBrowserSession')) { + // Login verification endpoint + if (userSignedOut) { + console.log("llgT: User is signed out, returning 401 for resolveBrowserSession"); return Promise.resolve({ - ok: true, - status: 200, + ok: false, + status: 401, // Unauthorized - user is logged out json: () => Promise.resolve({ - isSuccess: true + isSuccess: false }) }); } else { - // Default response for any other endpoints - console.log("llgT: Unknown endpoint, returning default response"); + console.log("llgT: User is signed in, returning success for resolveBrowserSession"); return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({ - isSuccess: true + isSuccess: true, + email: "test@example.com", + firstName: "Test", + lastName: "User", + customerID: "test-customer-id", + loginTime: Date.now(), + profileIcon: { + initials: "TU", + color: "#14b8a6" + } }) }); } - }); - console.log("llgT: login-browser fetch mock set up successfully for all endpoints"); - } else { - console.log("llgT: LoginBrowserExports.setFetchFn not available!"); - } + } else if (url.includes('/signOut')) { + // Logout endpoint - set signed out state + console.log("llgT: Handling signOut endpoint call, marking user as signed out"); + userSignedOut = true; + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ + isSuccess: true + }) + }); + } else { + // Default response for any other endpoints + console.log("llgT: Unknown endpoint, returning default response"); + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ + isSuccess: true + }) + }); + } + }); + console.log("llgT: login-browser fetch mock set up successfully for all endpoints"); // Step 4: Mock window.open to capture account URL opening let capturedURL = null; @@ -343,9 +334,6 @@ define(function (require, exports, module) { 10000 // Increase timeout to see if it's just taking longer ); - // Verify user is now signed out - expect(LoginServiceExports.LoginService.isLoggedIn()).toBe(false); - // Verify profile icon has been updated (should be empty again) const $profileIconAfterSignout = testWindow.$("#user-profile-button"); const profileIconContentAfterSignout = $profileIconAfterSignout.html(); @@ -377,15 +365,477 @@ define(function (require, exports, module) { expect(finalSignInButton.length).toBe(1); // Should have sign in button again expect(finalSignOutButton.length).toBe(0); // Should NOT have sign out button - // Close the final popup - if (testWindow.$('.modal').length > 0) { - testWindow.__PR.clickDialogButtonID(testWindow.__PR.Dialogs.DIALOG_BTN_CANCEL); - await testWindow.__PR.waitForModalDialogClosed(".modal"); + // just click outside or use popup close method + $profileIconAfterSignout.trigger('click'); // Toggle to close + }); + }); + + // Helper functions for promotion testing (browser-specific) + async function setupTrialState(daysRemaining) { + const PromotionExports = testWindow._test_promo_login_exports; + const mockNow = Date.now(); + await PromotionExports._setTrialData({ + proVersion: "3.1.0", + endDate: mockNow + (daysRemaining * PromotionExports.TRIAL_CONSTANTS.MS_PER_DAY) + }); + // Trigger entitlements changed event to update branding + const LoginService = PromotionExports.LoginService; + LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED); + } + + async function setupExpiredTrial() { + const PromotionExports = testWindow._test_promo_login_exports; + const mockNow = Date.now(); + await PromotionExports._setTrialData({ + proVersion: "3.1.0", + endDate: mockNow - PromotionExports.TRIAL_CONSTANTS.MS_PER_DAY + }); + // Trigger entitlements changed event to update branding + const LoginService = PromotionExports.LoginService; + LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED); + } + + function setupProUserMock(hasActiveSubscription = true) { + let userSignedOut = false; + + // Set fetch mock on both browser and service exports + const fetchMock = (url, options) => { + console.log("llgT: browser promo test fetchFn called with URL:", url); + + if (url.includes('/resolveBrowserSession')) { + if (userSignedOut) { + return Promise.resolve({ + ok: false, + status: 401, + json: () => Promise.resolve({ isSuccess: false }) + }); + } + const response = { + isSuccess: true, + email: "prouser@example.com", + firstName: "Pro", + lastName: "User", + customerID: "test-customer-id", + loginTime: Date.now(), + profileIcon: { + initials: "PU", + color: "#14b8a6" + } + }; + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(response) + }); + } else if (url.includes('/getAppEntitlements')) { + // Entitlements endpoint - return user's plan and entitlements + console.log("llgT: Handling getAppEntitlements call"); + if (userSignedOut) { + return Promise.resolve({ + ok: false, + status: 401, + json: () => Promise.resolve({ isSuccess: false }) + }); + } else { + const entitlementsResponse = { + isSuccess: true, + lang: "en" + }; + + if (hasActiveSubscription) { + entitlementsResponse.plan = { + paidSubscriber: true, + name: "Phoenix Pro", + validTill: Date.now() + 30 * 24 * 60 * 60 * 1000 + }; + entitlementsResponse.entitlements = { + liveEdit: { + activated: true, + validTill: Date.now() + 30 * 24 * 60 * 60 * 1000 + } + }; + } else { + entitlementsResponse.plan = { + paidSubscriber: false, + name: "Free Plan" + }; + } + + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(entitlementsResponse) + }); + } + } else if (url.includes('/signOut')) { + userSignedOut = true; + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ isSuccess: true }) + }); } else { - // If it's not a modal, just click outside or use popup close method - $profileIconAfterSignout.trigger('click'); // Toggle to close + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ isSuccess: true }) + }); + } + }; + + // Apply fetch mock to both browser exports and login service exports + LoginBrowserExports.setFetchFn(fetchMock); + LoginServiceExports.setFetchFn(fetchMock); + ProDialogsExports.setFetchFn(fetchMock); + } + + async function verifyProBranding(shouldShowPro, testDescription) { + const $brandingLink = testWindow.$("#phcode-io-main-nav"); + console.log(`llgT: Browser verifying branding for ${testDescription}, shouldShowPro: ${shouldShowPro}`); + console.log(`llgT: Browser branding link classes: ${$brandingLink.attr('class')}`); + console.log(`llgT: Browser branding link text: '${$brandingLink.text()}'`); + + if (shouldShowPro) { + await awaitsFor( + function () { + return testWindow.$("#phcode-io-main-nav").hasClass("phoenix-pro"); + }, + `Verify Pro branding to appear: ${testDescription}`, 5000 + ); + expect($brandingLink.hasClass("phoenix-pro")).toBe(true); + expect($brandingLink.text()).toContain("Phoenix Pro"); + expect($brandingLink.find(".fa-feather").length).toBe(1); + } else { + await awaitsFor( + function () { + return !testWindow.$("#phcode-io-main-nav").hasClass("phoenix-pro"); + }, + `Verify Pro branding to go away: ${testDescription}`, 5000 + ); + expect($brandingLink.hasClass("phoenix-pro")).toBe(false); + expect($brandingLink.text()).toBe("phcode.io"); + } + } + + async function verifyProfilePopupContent(expectedState, testDescription) { + await awaitsFor( + function () { + return testWindow.$('.profile-popup').length > 0; + }, + `Profile popup to appear: ${testDescription}`, + 3000 + ); + + const $popup = testWindow.$('.profile-popup'); + expect($popup.length).toBeGreaterThan(0); + + if (expectedState.isPro && !expectedState.isTrial) { + await awaitsFor( + function () { + const $planName = $popup.find('.user-plan-name'); + const planText = $planName.text(); + return planText.includes("Phoenix Pro"); + }, + `Profile popup should say phoenix pro: ${testDescription}`, 5000 + ); + const $planName = $popup.find('.user-plan-name'); + const planText = $planName.text(); + expect(planText).toContain("Phoenix Pro"); + expect(planText).not.toContain("days left"); + expect($planName.find(".fa-feather").length).toBe(1); + } else if (expectedState.isTrial) { + await awaitsFor( + function () { + const $planName = $popup.find('.user-plan-name'); + const planText = $planName.text(); + return planText.includes("Phoenix Pro"); + }, + `Profile popup should say phoenix pro trial: ${testDescription}`, 5000 + ); + const $planName = $popup.find('.user-plan-name'); + const planText = $planName.text(); + expect(planText).toContain("Phoenix Pro"); + expect(planText).toContain("days left"); + expect($planName.find(".fa-feather").length).toBe(1); + } else { + await awaitsFor( + function () { + const $planName = $popup.find('.user-plan-name'); + const planText = $planName.text(); + return !planText.includes("Phoenix Pro"); + }, + `Profile popup should say phoenix pro: ${testDescription}`, 5000 + ); + const $planName = $popup.find('.user-plan-name'); + const planText = $planName.text(); + expect(planText).not.toContain("Phoenix Pro"); + expect($planName.find(".fa-feather").length).toBe(0); + } + } + + async function cleanupTrialState() { + const PromotionExports = testWindow._test_promo_login_exports; + await PromotionExports._cleanTrialData(); + } + + async function popupToAppear(popupDescription) { + await awaitsFor( + function () { + return testWindow.$('.modal').length > 0 || testWindow.$('.profile-popup').length > 0; + }, + popupDescription, 3000 + ); + } + + async function performFullLoginFlow() { + // Mock window.open like the original test + let capturedURL = null; + let capturedTarget = null; + testWindow.open = function(url, target) { + capturedURL = url; + capturedTarget = target; + return { + focus: function() {}, + close: function() {}, + closed: false + }; + }; + + // Click profile button + const $profileButton = testWindow.$("#user-profile-button"); + $profileButton.trigger('click'); + await popupToAppear("Login popup to appear"); + + // Find and click sign in button + let popupContent = testWindow.$('.profile-popup'); + const signInButton = popupContent.find('#phoenix-signin-btn'); + signInButton.trigger('click'); + + // Verify window.open was called + expect(capturedURL).toBeDefined(); + expect(capturedURL).toContain('phcode.dev'); + expect(capturedTarget).toBe('_blank'); + + // Wait for browser login waiting dialog + await testWindow.__PR.waitForModalDialog(".browser-login-waiting-dialog"); + + // Click "Check Now" button to verify login + const checkNowButton = testWindow.$('[data-button-id="check"]'); + checkNowButton.trigger('click'); + + // Wait for login to complete + await awaitsFor( + function () { + return LoginServiceExports.LoginService.isLoggedIn(); + }, + "User to be logged in", + 5000 + ); + + // Wait for login dialog to close + await testWindow.__PR.waitForModalDialogClosed(".modal", "Login waiting dialog"); + + // Wait for profile icon to update with user data + await awaitsFor( + function () { + const $profileIcon = testWindow.$("#user-profile-button"); + const profileIconContent = $profileIcon.html(); + return profileIconContent && profileIconContent.includes('PU'); + }, + "profile icon to update with user initials", + 3000 + ); + } + + async function performFullLogoutFlow() { + // Click profile button to open popup + const $profileButton = testWindow.$("#user-profile-button"); + $profileButton.trigger('click'); + + // Wait for profile popup + await popupToAppear("profile popup to appear"); + + // Find and click sign out button + let popupContent = testWindow.$('.profile-popup'); + const signOutButton = popupContent.find('#phoenix-signout-btn'); + signOutButton.trigger('click'); + + // Wait for sign out confirmation dialog and dismiss it + await testWindow.__PR.waitForModalDialog(".modal"); + testWindow.__PR.clickDialogButtonID(testWindow.__PR.Dialogs.DIALOG_BTN_OK); + await testWindow.__PR.waitForModalDialogClosed(".modal"); + + // Wait for sign out to complete + await awaitsFor( + function () { + return !LoginServiceExports.LoginService.isLoggedIn(); + }, + "User to be signed out", + 10000 + ); + } + + describe("Browser Login Promotion Tests", function () { + + beforeEach(async function () { + // Ensure clean state before each test + if (LoginServiceExports.LoginService.isLoggedIn()) { + throw new Error("Promotion tests require user to be logged out at start. Please log out before running these tests."); } + await cleanupTrialState(); + }); + + // afterEach(async function () { + // // Clean up after each test + // try { + // if (LoginServiceExports.LoginService.isLoggedIn()) { + // await performFullLogoutFlow(); + // } + // await cleanupTrialState(); + // // Restore original fetch + // if (originalFetch) { + // LoginBrowserExports.setFetchFn(originalFetch); + // } + // } catch (error) { + // console.log("Cleanup error (ignoring):", error); + // } + // }); + + it("should show pro branding for user with pro subscription (expired trial)", async function () { + console.log("llgT: Starting browser pro user with expired trial test"); + + // Setup: Pro subscription + expired trial + setupProUserMock(true); + await setupExpiredTrial(); + + // Verify initial state (no pro branding) + await verifyProBranding(false, "no pro branding to start with"); + + // Perform login + await performFullLoginFlow(); + await verifyProBranding(true, "pro branding to appear after pro user login"); + + // Check profile popup shows pro status (not trial) + const $profileButton = testWindow.$("#user-profile-button"); + $profileButton.trigger('click'); + + // Wait for profile popup to load entitlements and update content + await verifyProfilePopupContent({isPro: true, isTrial: false}, "pro user profile popup"); + + // Close popup + $profileButton.trigger('click'); + + // Perform logout + await performFullLogoutFlow(); + + // For user with pro subscription + expired trial: + // After logout, pro branding should disappear because: + // 1. No server entitlements (logged out) + // 2. Trial is expired (0 days remaining) + await verifyProBranding(false, "Pro branding to disappear after logout"); }); + + // it("should show trial branding for user without pro subscription (active trial)", async function () { + // console.log("llgT: Starting browser trial user test"); + // + // // Setup: No pro subscription + active trial (15 days) + // setupProUserMock(false); + // await setupTrialState(15); + // + // // Verify initial state shows pro branding due to trial + // await awaitsFor( + // function () { + // const $brandingLink = testWindow.$("#phcode-io-main-nav"); + // return $brandingLink.hasClass("phoenix-pro"); + // }, + // "Trial branding to appear initially", + // 3000 + // ); + // verifyProBranding(true, "initial trial state"); + // + // // Perform login + // await performFullLoginFlow(); + // + // // Verify pro branding remains after login + // verifyProBranding(true, "after trial user login"); + // + // // Check profile popup shows trial status + // const $profileButton = testWindow.$("#user-profile-button"); + // $profileButton.trigger('click'); + // + // await awaitsFor( + // function () { + // return testWindow.$('.profile-popup').length > 0; + // }, + // "Profile popup to appear", + // 3000 + // ); + // + // verifyProfilePopupContent({isPro: true, isTrial: true}, "trial user profile popup"); + // + // // Close popup + // $profileButton.trigger('click'); + // + // // Perform logout + // await performFullLogoutFlow(); + // + // // Verify pro branding remains after logout (trial continues) + // await awaitsFor( + // function () { + // const $brandingLink = testWindow.$("#phcode-io-main-nav"); + // return $brandingLink.hasClass("phoenix-pro"); + // }, + // "Trial branding to remain after logout", + // 3000 + // ); + // verifyProBranding(true, "after trial user logout"); + // }); + // + // it("should prioritize pro subscription over trial in profile popup", async function () { + // console.log("llgT: Starting browser trial user with pro subscription test"); + // + // // Setup: Pro subscription + active trial + // setupProUserMock(true); + // await setupTrialState(10); + // + // // Perform login + // await performFullLoginFlow(); + // + // // Verify pro branding appears + // verifyProBranding(true, "after pro+trial user login"); + // + // // Check profile popup shows pro status (not trial text) + // const $profileButton = testWindow.$("#user-profile-button"); + // $profileButton.trigger('click'); + // + // await awaitsFor( + // function () { + // return testWindow.$('.profile-popup').length > 0; + // }, + // "Profile popup to appear", + // 3000 + // ); + // + // // Should show pro, not trial, since user has paid subscription + // verifyProfilePopupContent({isPro: true, isTrial: false}, "pro+trial user profile popup"); + // + // // Close popup + // $profileButton.trigger('click'); + // + // // Perform logout + // await performFullLogoutFlow(); + // + // // Verify pro branding remains due to trial (even though subscription is gone) + // await awaitsFor( + // function () { + // const $brandingLink = testWindow.$("#phcode-io-main-nav"); + // return $brandingLink.hasClass("phoenix-pro"); + // }, + // "Trial branding to remain after logout", + // 3000 + // ); + // verifyProBranding(true, "after pro+trial user logout"); + // }); }); }); }); From 8485ec89c5a457e0e006d3d83b0d8f99aab35a7f Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 17 Sep 2025 13:18:22 +0530 Subject: [PATCH 2/4] test: browser login integ tests with entitlements more coverage --- src/services/html/login-popup.html | 4 +- src/services/html/profile-popup.html | 2 +- src/services/profile-menu.js | 7 +- test/spec/login-browser-integ-test.js | 245 ++++++++++++-------------- 4 files changed, 115 insertions(+), 143 deletions(-) diff --git a/src/services/html/login-popup.html b/src/services/html/login-popup.html index fafc0c00e2..aa74adb015 100644 --- a/src/services/html/login-popup.html +++ b/src/services/html/login-popup.html @@ -1,10 +1,10 @@ -
+