-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[PM-24795] When logged out, "(empty)" options appear under File #19982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jengstrom-bw
merged 7 commits into
main
from
vault/pm-24795/when-logged-out-empty-options-appear-under-File
Apr 10, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f57a554
update when log out and lock menu are enabled
jengstrom-bw 7a5a38f
fixes issue when logging out with vault locked, the log out and lock β¦
jengstrom-bw 682b385
Merge branch 'main' into vault/pm-24795/when-logged-out-empty-optionsβ¦
jengstrom-bw ec4b892
Merge branch 'main' into vault/pm-24795/when-logged-out-empty-optionsβ¦
jengstrom-bw 8ed5ab8
Merge branch 'main' into vault/pm-24795/when-logged-out-empty-optionsβ¦
jengstrom-bw 9d2dade
Adds tests for
jengstrom-bw 38dc57c
Removes extraneous visible
jengstrom-bw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,352 @@ | ||
| import { BrowserWindow, dialog, MenuItem } from "electron"; | ||
|
|
||
| import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; | ||
| import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; | ||
|
|
||
| import * as utils from "../../utils"; | ||
| import { UpdaterMain } from "../updater.main"; | ||
|
|
||
| import { FirstMenu } from "./menu.first"; | ||
| import { MenuAccount } from "./menu.updater"; | ||
|
|
||
| jest.mock("electron", () => ({ | ||
| BrowserWindow: jest.fn(), | ||
| dialog: { showMessageBox: jest.fn() }, | ||
| MenuItem: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock("../../utils", () => ({ | ||
| isMacAppStore: jest.fn().mockReturnValue(false), | ||
| isWindowsStore: jest.fn().mockReturnValue(false), | ||
| isSnapStore: jest.fn().mockReturnValue(false), | ||
| })); | ||
|
|
||
| function makeAccount(overrides: Partial<MenuAccount> = {}): MenuAccount { | ||
| return { | ||
| userId: "user1", | ||
| email: "user@example.com", | ||
| isAuthenticated: true, | ||
| isLocked: false, | ||
| isLockable: true, | ||
| hasMasterPassword: true, | ||
| multiClientPasswordManagement: false, | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| function makeMenu( | ||
| accounts: { [userId: string]: MenuAccount } = {}, | ||
| isLocked = false, | ||
| isLockable = true, | ||
| ): FirstMenu { | ||
| const i18nService = { t: (s: string) => s } as unknown as I18nService; | ||
| const messagingService = { send: jest.fn() } as unknown as MessagingService; | ||
| const updater = { checkForUpdate: jest.fn() } as unknown as UpdaterMain; | ||
| const window = {} as BrowserWindow; | ||
| return new FirstMenu( | ||
| i18nService, | ||
| messagingService, | ||
| updater, | ||
| window, | ||
| accounts, | ||
| isLocked, | ||
| isLockable, | ||
| ); | ||
| } | ||
|
|
||
| describe("FirstMenu", () => { | ||
| describe("hasAccounts", () => { | ||
| it("returns false when accounts is null", () => { | ||
| const menu = makeMenu(null as any); | ||
| expect((menu as any).hasAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when accounts is empty", () => { | ||
| const menu = makeMenu({}); | ||
| expect((menu as any).hasAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns true when at least one account exists", () => { | ||
| const menu = makeMenu({ user1: makeAccount() }); | ||
| expect((menu as any).hasAccounts).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("hasLockableAccounts", () => { | ||
| it("returns false when accounts is null", () => { | ||
| const menu = makeMenu(null as any); | ||
| expect((menu as any).hasLockableAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when no accounts are lockable", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isLockable: false }) }); | ||
| expect((menu as any).hasLockableAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when lockable account is not authenticated", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isLockable: true, isAuthenticated: false }) }); | ||
| expect((menu as any).hasLockableAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when lockable authenticated account is already locked", () => { | ||
| const menu = makeMenu({ | ||
| user1: makeAccount({ isLockable: true, isAuthenticated: true, isLocked: true }), | ||
| }); | ||
| expect((menu as any).hasLockableAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns true when at least one lockable, authenticated, unlocked account exists", () => { | ||
| const menu = makeMenu({ | ||
| user1: makeAccount({ isLockable: true, isAuthenticated: true, isLocked: false }), | ||
| }); | ||
| expect((menu as any).hasLockableAccounts).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("hasAuthenticatedAccounts", () => { | ||
| it("returns false when accounts is null", () => { | ||
| const menu = makeMenu(null as any); | ||
| expect((menu as any).hasAuthenticatedAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when no accounts are authenticated", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isAuthenticated: false }) }); | ||
| expect((menu as any).hasAuthenticatedAccounts).toBe(false); | ||
| }); | ||
|
|
||
| it("returns true when at least one account is authenticated", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isAuthenticated: true }) }); | ||
| expect((menu as any).hasAuthenticatedAccounts).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("checkForUpdates", () => { | ||
| const mockedIsMacAppStore = jest.mocked(utils.isMacAppStore); | ||
| const mockedIsWindowsStore = jest.mocked(utils.isWindowsStore); | ||
| const mockedIsSnapStore = jest.mocked(utils.isSnapStore); | ||
|
|
||
| it("is visible when not in any app store", () => { | ||
| mockedIsMacAppStore.mockReturnValue(false); | ||
| mockedIsWindowsStore.mockReturnValue(false); | ||
| mockedIsSnapStore.mockReturnValue(false); | ||
| const menu = makeMenu(); | ||
| expect((menu as any).checkForUpdates.visible).toBe(true); | ||
| }); | ||
|
|
||
| it("is not visible in mac app store", () => { | ||
| mockedIsMacAppStore.mockReturnValue(true); | ||
| mockedIsWindowsStore.mockReturnValue(false); | ||
| mockedIsSnapStore.mockReturnValue(false); | ||
| const menu = makeMenu(); | ||
| expect((menu as any).checkForUpdates.visible).toBe(false); | ||
| }); | ||
|
|
||
| it("is not visible in windows store", () => { | ||
| mockedIsMacAppStore.mockReturnValue(false); | ||
| mockedIsWindowsStore.mockReturnValue(true); | ||
| mockedIsSnapStore.mockReturnValue(false); | ||
| const menu = makeMenu(); | ||
| expect((menu as any).checkForUpdates.visible).toBe(false); | ||
| }); | ||
|
|
||
| it("is not visible in snap store", () => { | ||
| mockedIsMacAppStore.mockReturnValue(false); | ||
| mockedIsWindowsStore.mockReturnValue(false); | ||
| mockedIsSnapStore.mockReturnValue(true); | ||
| const menu = makeMenu(); | ||
| expect((menu as any).checkForUpdates.visible).toBe(false); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| mockedIsMacAppStore.mockReturnValue(false); | ||
| mockedIsWindowsStore.mockReturnValue(false); | ||
| mockedIsSnapStore.mockReturnValue(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("settings", () => { | ||
| it("is enabled when not locked", () => { | ||
| const menu = makeMenu({}, false); | ||
| expect((menu as any).settings.enabled).toBe(true); | ||
| }); | ||
|
|
||
| it("is disabled when locked", () => { | ||
| const menu = makeMenu({}, true); | ||
| expect((menu as any).settings.enabled).toBe(false); | ||
| }); | ||
|
|
||
| it("sends openSettings message on click", () => { | ||
| const menu = makeMenu(); | ||
| const messagingService = (menu as any)._messagingService as jest.Mocked<MessagingService>; | ||
| (menu as any).settings.click(); | ||
| expect(messagingService.send).toHaveBeenCalledWith("openSettings", undefined); | ||
| }); | ||
| }); | ||
|
|
||
| describe("lock", () => { | ||
| it("is enabled when lockable accounts exist", () => { | ||
| const menu = makeMenu({ user1: makeAccount() }); | ||
| expect((menu as any).lock.enabled).toBe(true); | ||
| }); | ||
|
|
||
| it("is disabled when no lockable accounts exist", () => { | ||
| const menu = makeMenu({}); | ||
| expect((menu as any).lock.enabled).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("lockSubmenu", () => { | ||
| it("returns empty array when no accounts", () => { | ||
| const menu = makeMenu({}); | ||
| expect((menu as any).lockSubmenu).toEqual([]); | ||
| }); | ||
|
|
||
| it("excludes accounts that are not lockable", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isLockable: false }) }); | ||
| expect((menu as any).lockSubmenu).toHaveLength(0); | ||
| }); | ||
|
|
||
| it("excludes accounts that are not authenticated", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isAuthenticated: false }) }); | ||
| expect((menu as any).lockSubmenu).toHaveLength(0); | ||
| }); | ||
|
|
||
| it("includes lockable, authenticated accounts", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ email: "a@b.com", userId: "user1" }) }); | ||
| const submenu = (menu as any).lockSubmenu; | ||
| expect(submenu).toHaveLength(1); | ||
| expect(submenu[0].label).toBe("a@b.com"); | ||
| expect(submenu[0].id).toBe("lockNow_user1"); | ||
| }); | ||
|
|
||
| it("sets enabled to false for already-locked accounts", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isLocked: true }) }); | ||
| const submenu = (menu as any).lockSubmenu; | ||
| expect(submenu).toHaveLength(1); | ||
| expect(submenu[0].enabled).toBe(false); | ||
| }); | ||
|
|
||
| it("sets enabled to true for unlocked accounts", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isLocked: false }) }); | ||
| const submenu = (menu as any).lockSubmenu; | ||
| expect(submenu[0].enabled).toBe(true); | ||
| }); | ||
|
|
||
| it("sends lockVault message with userId on click", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ userId: "user1" }) }); | ||
| const messagingService = (menu as any)._messagingService as jest.Mocked<MessagingService>; | ||
| const submenu = (menu as any).lockSubmenu; | ||
| submenu[0].click(); | ||
| expect(messagingService.send).toHaveBeenCalledWith("lockVault", { userId: "user1" }); | ||
| }); | ||
|
|
||
| it("includes multiple accounts", () => { | ||
| const menu = makeMenu({ | ||
| user1: makeAccount({ userId: "user1", email: "a@b.com" }), | ||
| user2: makeAccount({ userId: "user2", email: "c@d.com" }), | ||
| }); | ||
| expect((menu as any).lockSubmenu).toHaveLength(2); | ||
| }); | ||
| }); | ||
|
|
||
| describe("lockAll", () => { | ||
| it("is enabled when lockable accounts exist", () => { | ||
| const menu = makeMenu({ user1: makeAccount() }); | ||
| expect((menu as any).lockAll.enabled).toBe(true); | ||
| }); | ||
|
|
||
| it("is disabled when no lockable accounts exist", () => { | ||
| const menu = makeMenu({}); | ||
| expect((menu as any).lockAll.enabled).toBe(false); | ||
| }); | ||
|
|
||
| it("sends lockAllVaults message on click", () => { | ||
| const menu = makeMenu({ user1: makeAccount() }); | ||
| const messagingService = (menu as any)._messagingService as jest.Mocked<MessagingService>; | ||
| (menu as any).lockAll.click(); | ||
| expect(messagingService.send).toHaveBeenCalledWith("lockAllVaults", undefined); | ||
| }); | ||
| }); | ||
|
|
||
| describe("logOut", () => { | ||
| it("is enabled when authenticated accounts exist", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isAuthenticated: true }) }); | ||
| expect((menu as any).logOut.enabled).toBe(true); | ||
| }); | ||
|
|
||
| it("is disabled when no authenticated accounts exist", () => { | ||
| const menu = makeMenu({}); | ||
| expect((menu as any).logOut.enabled).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("logOutSubmenu", () => { | ||
| it("returns empty array when no accounts", () => { | ||
| const menu = makeMenu({}); | ||
| expect((menu as any).logOutSubmenu).toEqual([]); | ||
| }); | ||
|
|
||
| it("includes an entry per account", () => { | ||
| const menu = makeMenu({ | ||
| user1: makeAccount({ userId: "user1", email: "a@b.com" }), | ||
| user2: makeAccount({ userId: "user2", email: "c@d.com" }), | ||
| }); | ||
| expect((menu as any).logOutSubmenu).toHaveLength(2); | ||
| }); | ||
|
|
||
| it("sets visible to true for authenticated accounts", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isAuthenticated: true }) }); | ||
| const submenu = (menu as any).logOutSubmenu; | ||
| expect(submenu[0].visible).toBe(true); | ||
| }); | ||
|
|
||
| it("sets visible to false for unauthenticated accounts", () => { | ||
| const menu = makeMenu({ user1: makeAccount({ isAuthenticated: false }) }); | ||
| const submenu = (menu as any).logOutSubmenu; | ||
| expect(submenu[0].visible).toBe(false); | ||
| }); | ||
|
|
||
| it("sends logout message when dialog confirms", async () => { | ||
| const menu = makeMenu({ user1: makeAccount({ userId: "user1" }) }); | ||
| const messagingService = (menu as any)._messagingService as jest.Mocked<MessagingService>; | ||
| (dialog.showMessageBox as jest.Mock).mockResolvedValue({ response: 0 }); | ||
|
|
||
| const submenu = (menu as any).logOutSubmenu; | ||
| await submenu[0].click(); | ||
|
|
||
| expect(messagingService.send).toHaveBeenCalledWith("logout", { userId: "user1" }); | ||
| }); | ||
|
|
||
| it("does not send logout message when dialog is cancelled", async () => { | ||
| const menu = makeMenu({ user1: makeAccount({ userId: "user1" }) }); | ||
| const messagingService = (menu as any)._messagingService as jest.Mocked<MessagingService>; | ||
| (dialog.showMessageBox as jest.Mock).mockResolvedValue({ response: 1 }); | ||
|
|
||
| const submenu = (menu as any).logOutSubmenu; | ||
| await submenu[0].click(); | ||
|
|
||
| expect(messagingService.send).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("checkForUpdate", () => { | ||
| it("calls updater.checkForUpdate and re-enables the menu item", async () => { | ||
| const menu = makeMenu(); | ||
| const updater = (menu as any)._updater as jest.Mocked<UpdaterMain>; | ||
| updater.checkForUpdate = jest.fn().mockResolvedValue(undefined); | ||
|
|
||
| const menuItem = { enabled: true } as MenuItem; | ||
| await (menu as any).checkForUpdate(menuItem); | ||
|
|
||
| expect(updater.checkForUpdate).toHaveBeenCalledWith(true); | ||
| expect(menuItem.enabled).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("separator", () => { | ||
| it("has type separator", () => { | ||
| const menu = makeMenu(); | ||
| expect((menu as any).separator).toEqual({ type: "separator" }); | ||
| }); | ||
| }); | ||
| }); |
|
shane-melton marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Thanks!