diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts
index b07e06234d3..c093f1a3b00 100644
--- a/apps/browser/src/autofill/background/tabs.background.ts
+++ b/apps/browser/src/autofill/background/tabs.background.ts
@@ -1,7 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-
import MainBackground from "../../background/main.background";
import { OverlayBackground } from "./abstractions/overlay.background";
@@ -14,7 +10,7 @@ export default class TabsBackground {
private overlayBackground: OverlayBackground,
) {}
- private focusedWindowId: number;
+ private focusedWindowId: number = -1;
/**
* Initializes the window and tab listeners.
@@ -90,14 +86,6 @@ export default class TabsBackground {
changeInfo: chrome.tabs.TabChangeInfo,
tab: chrome.tabs.Tab,
) => {
- const overlayImprovementsFlag = await this.main.configService.getFeatureFlag(
- FeatureFlag.InlineMenuPositioningImprovements,
- );
- const removePageDetailsStatus = new Set(["loading", "unloaded"]);
- if (!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) {
- this.overlayBackground.removePageDetails(tabId);
- }
-
if (this.focusedWindowId > 0 && tab.windowId !== this.focusedWindowId) {
return;
}
diff --git a/apps/browser/src/autofill/content/components/buttons/close-button.ts b/apps/browser/src/autofill/content/components/buttons/close-button.ts
index c32d0c130e3..05a12d4f453 100644
--- a/apps/browser/src/autofill/content/components/buttons/close-button.ts
+++ b/apps/browser/src/autofill/content/components/buttons/close-button.ts
@@ -35,5 +35,6 @@ const closeButtonStyles = (theme: Theme) => css`
> svg {
width: 20px;
height: 20px;
+ vertical-align: middle;
}
`;
diff --git a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts
index cf9a561ee39..e3c7e0d54e6 100644
--- a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts
+++ b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts
@@ -44,7 +44,7 @@ export function OptionSelectionButton({
`;
}
-const iconSize = "15px";
+const iconSize = "16px";
const selectionButtonStyles = ({
disabled,
@@ -94,7 +94,8 @@ const selectionButtonStyles = ({
> svg {
max-width: ${iconSize};
- height: fit-content;
+ max-height: ${iconSize};
+ height: auto;
}
`;
diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts
index 2d386d34d6a..aaa4b11d8a2 100644
--- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts
+++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts
@@ -19,13 +19,13 @@ export function CipherAction({
? BadgeButton({
buttonAction: handleAction,
// @TODO localize
- buttonText: "Update item",
+ buttonText: "Update",
theme,
})
: EditButton({
buttonAction: handleAction,
// @TODO localize
- buttonText: "Edit item",
+ buttonText: "Edit",
theme,
});
}
diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts
index e4fe012a678..5e78fd5658a 100644
--- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts
+++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts
@@ -4,7 +4,7 @@ import { html, TemplateResult } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { themes } from "../../../content/components/constants/styles";
-import { Business, Users } from "../../../content/components/icons";
+import { Business, Family } from "../../../content/components/icons";
import { OrganizationCategories, OrganizationCategory } from "./types";
@@ -13,7 +13,7 @@ const cipherIndicatorIconsMap: Record<
(args: { color: string; theme: Theme }) => TemplateResult
> = {
[OrganizationCategories.business]: Business,
- [OrganizationCategories.family]: Users,
+ [OrganizationCategories.family]: Family,
};
export function CipherInfoIndicatorIcons({
diff --git a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts
index 8df68d79b6e..1b08f261eb6 100644
--- a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts
+++ b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts
@@ -12,8 +12,13 @@ export function BrandIconContainer({ iconLink, theme }: { iconLink?: URL; theme:
}
const brandIconContainerStyles = css`
+ display: flex;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+
> svg {
- width: 20px;
- height: fit-content;
+ width: auto;
+ height: 100%;
}
`;
diff --git a/apps/browser/src/autofill/content/components/icons/collection.ts b/apps/browser/src/autofill/content/components/icons/collection-shared.ts
similarity index 65%
rename from apps/browser/src/autofill/content/components/icons/collection.ts
rename to apps/browser/src/autofill/content/components/icons/collection-shared.ts
index fb2c58647c5..de366b88e92 100644
--- a/apps/browser/src/autofill/content/components/icons/collection.ts
+++ b/apps/browser/src/autofill/content/components/icons/collection-shared.ts
@@ -4,14 +4,14 @@ import { html } from "lit";
import { IconProps } from "../common-types";
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
-export function Collection({ color, disabled, theme }: IconProps) {
+export function CollectionShared({ color, disabled, theme }: IconProps) {
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
return html`
+ `;
+}
diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts
index de39b70ab24..65ec6301ac4 100644
--- a/apps/browser/src/autofill/content/components/icons/index.ts
+++ b/apps/browser/src/autofill/content/components/icons/index.ts
@@ -3,12 +3,12 @@ export { AngleUp } from "./angle-up";
export { BrandIconContainer } from "./brand-icon-container";
export { Business } from "./business";
export { Close } from "./close";
-export { Collection } from "./collection";
+export { CollectionShared } from "./collection-shared";
export { ExclamationTriangle } from "./exclamation-triangle";
export { ExternalLink } from "./external-link";
+export { Family } from "./family";
export { Folder } from "./folder";
export { Globe } from "./globe";
export { PencilSquare } from "./pencil-square";
export { Shield } from "./shield";
export { User } from "./user";
-export { Users } from "./users";
diff --git a/apps/browser/src/autofill/content/components/icons/users.ts b/apps/browser/src/autofill/content/components/icons/users.ts
deleted file mode 100644
index eb7840104f0..00000000000
--- a/apps/browser/src/autofill/content/components/icons/users.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { css } from "@emotion/css";
-import { html } from "lit";
-
-import { IconProps } from "../common-types";
-import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
-
-export function Users({ color, disabled, theme }: IconProps) {
- const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
-
- return html`
-
- `;
-}
diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts
index fc5db1c7c2c..c74895e1dea 100644
--- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts
+++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts
@@ -43,26 +43,23 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj => {
render: (args) => Template(args, Icons[iconName]),
} as StoryObj;
- if (iconName !== "BrandIconContainer") {
- story.argTypes = {
- iconLink: { table: { disable: true } },
- };
- }
+ story.argTypes = {
+ iconLink: { table: { disable: true } },
+ };
return story;
};
export const AngleDownIcon = createIconStory("AngleDown");
export const AngleUpIcon = createIconStory("AngleUp");
-export const BrandIcon = createIconStory("BrandIconContainer");
export const BusinessIcon = createIconStory("Business");
export const CloseIcon = createIconStory("Close");
-export const CollectionIcon = createIconStory("Collection");
+export const CollectionSharedIcon = createIconStory("CollectionShared");
export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle");
export const ExternalLinkIcon = createIconStory("ExternalLink");
+export const FamilyIcon = createIconStory("Family");
export const FolderIcon = createIconStory("Folder");
export const GlobeIcon = createIconStory("Globe");
export const PencilSquareIcon = createIconStory("PencilSquare");
export const ShieldIcon = createIconStory("Shield");
export const UserIcon = createIconStory("User");
-export const UsersIcon = createIconStory("Users");
diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts
index 6fa32f11aa2..8661f5957e1 100644
--- a/apps/browser/src/autofill/content/components/notification/button-row.ts
+++ b/apps/browser/src/autofill/content/components/notification/button-row.ts
@@ -4,14 +4,14 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
import { Theme } from "@bitwarden/common/platform/enums";
import { Option, OrgView, FolderView } from "../common-types";
-import { Business, Users, Folder, User } from "../icons";
+import { Business, Family, Folder, User } from "../icons";
import { ButtonRow } from "../rows/button-row";
function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] {
switch (productTierType) {
case ProductTierType.Free:
case ProductTierType.Families:
- return Users;
+ return Family;
case ProductTierType.Teams:
case ProductTierType.Enterprise:
case ProductTierType.TeamsStarter:
diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts
index 50c2c629942..d6cedf6a85a 100644
--- a/apps/browser/src/autofill/content/components/notification/header.ts
+++ b/apps/browser/src/autofill/content/components/notification/header.ts
@@ -49,7 +49,7 @@ const notificationHeaderStyles = ({
display: flex;
align-items: center;
justify-content: flex-start;
- background-color: ${themes[theme].background};
+ background-color: ${themes[theme].background.DEFAULT};
padding: 12px 16px 8px 16px;
white-space: nowrap;
diff --git a/apps/browser/src/autofill/content/components/option-selection/option-item.ts b/apps/browser/src/autofill/content/components/option-selection/option-item.ts
index 619d77e63d3..e8a293e2c3f 100644
--- a/apps/browser/src/autofill/content/components/option-selection/option-item.ts
+++ b/apps/browser/src/autofill/content/components/option-selection/option-item.ts
@@ -62,14 +62,15 @@ const optionItemStyles = css`
`;
const optionItemIconContainerStyles = css`
+ display: flex;
flex-grow: 1;
flex-shrink: 1;
- width: ${optionItemIconWidth}px;
- height: ${optionItemIconWidth}px;
+ max-width: ${optionItemIconWidth}px;
+ max-height: ${optionItemIconWidth}px;
> svg {
width: 100%;
- height: fit-content;
+ height: auto;
}
`;
diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts
deleted file mode 100644
index 88b78dc2495..00000000000
--- a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { CipherType } from "@bitwarden/common/vault/enums";
-import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
-
-import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background";
-import AutofillPageDetails from "../../../models/autofill-page-details";
-
-type WebsiteIconData = {
- imageEnabled: boolean;
- image: string;
- fallbackImage: string;
- icon: string;
-};
-
-type OverlayAddNewItemMessage = {
- login?: {
- uri?: string;
- hostname: string;
- username: string;
- password: string;
- };
-};
-
-type OverlayBackgroundExtensionMessage = {
- [key: string]: any;
- command: string;
- tab?: chrome.tabs.Tab;
- sender?: string;
- details?: AutofillPageDetails;
- overlayElement?: string;
- display?: string;
- data?: LockedVaultPendingNotificationsData;
-} & OverlayAddNewItemMessage;
-
-type OverlayPortMessage = {
- [key: string]: any;
- command: string;
- direction?: string;
- overlayCipherId?: string;
-};
-
-type FocusedFieldData = {
- focusedFieldStyles: Partial;
- focusedFieldRects: Partial;
- tabId?: number;
-};
-
-type OverlayCipherData = {
- id: string;
- name: string;
- type: CipherType;
- reprompt: CipherRepromptType;
- favorite: boolean;
- icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string };
- login?: { username: string };
- card?: string;
-};
-
-type BackgroundMessageParam = {
- message: OverlayBackgroundExtensionMessage;
-};
-type BackgroundSenderParam = {
- sender: chrome.runtime.MessageSender;
-};
-type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
-
-type OverlayBackgroundExtensionMessageHandlers = {
- [key: string]: CallableFunction;
- openAutofillOverlay: () => void;
- autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
- autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
- getAutofillOverlayVisibility: () => void;
- checkAutofillOverlayFocused: () => void;
- focusAutofillOverlayList: () => void;
- updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
- updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void;
- updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
- collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
- unlockCompleted: ({ message }: BackgroundMessageParam) => void;
- addedCipher: () => void;
- addEditCipherSubmitted: () => void;
- editedCipher: () => void;
- deletedCipher: () => void;
-};
-
-type PortMessageParam = {
- message: OverlayPortMessage;
-};
-type PortConnectionParam = {
- port: chrome.runtime.Port;
-};
-type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
-
-type OverlayButtonPortMessageHandlers = {
- [key: string]: CallableFunction;
- overlayButtonClicked: ({ port }: PortConnectionParam) => void;
- closeAutofillOverlay: ({ port }: PortConnectionParam) => void;
- forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void;
- overlayPageBlurred: () => void;
- redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
-};
-
-type OverlayListPortMessageHandlers = {
- [key: string]: CallableFunction;
- checkAutofillOverlayButtonFocused: () => void;
- forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void;
- overlayPageBlurred: () => void;
- unlockVault: ({ port }: PortConnectionParam) => void;
- fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void;
- addNewVaultItem: ({ port }: PortConnectionParam) => void;
- viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
- redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
-};
-
-export {
- WebsiteIconData,
- OverlayBackgroundExtensionMessage,
- OverlayPortMessage,
- FocusedFieldData,
- OverlayCipherData,
- OverlayAddNewItemMessage,
- OverlayBackgroundExtensionMessageHandlers,
- OverlayButtonPortMessageHandlers,
- OverlayListPortMessageHandlers,
-};
diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts
deleted file mode 100644
index 68f8032350e..00000000000
--- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts
+++ /dev/null
@@ -1,1464 +0,0 @@
-import { mock, MockProxy, mockReset } from "jest-mock-extended";
-import { BehaviorSubject, of } from "rxjs";
-
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { AuthService } from "@bitwarden/common/auth/services/auth.service";
-import {
- SHOW_AUTOFILL_BUTTON,
- AutofillOverlayVisibility,
-} from "@bitwarden/common/autofill/constants";
-import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
-import {
- DefaultDomainSettingsService,
- DomainSettingsService,
-} from "@bitwarden/common/autofill/services/domain-settings.service";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-import {
- EnvironmentService,
- Region,
-} from "@bitwarden/common/platform/abstractions/environment.service";
-import { ThemeType } from "@bitwarden/common/platform/enums";
-import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
-import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
-import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
-import {
- FakeStateProvider,
- FakeAccountService,
- mockAccountServiceWith,
-} from "@bitwarden/common/spec";
-import { UserId } from "@bitwarden/common/types/guid";
-import { CipherType } from "@bitwarden/common/vault/enums";
-import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
-import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
-import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
-
-import { BrowserApi } from "../../../platform/browser/browser-api";
-import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service";
-import {
- AutofillOverlayElement,
- AutofillOverlayPort,
- RedirectFocusDirection,
-} from "../../enums/autofill-overlay.enum";
-import { AutofillService } from "../../services/abstractions/autofill.service";
-import {
- createAutofillPageDetailsMock,
- createChromeTabMock,
- createFocusedFieldDataMock,
- createPageDetailMock,
- createPortSpyMock,
-} from "../../spec/autofill-mocks";
-import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils";
-
-import LegacyOverlayBackground from "./overlay.background.deprecated";
-
-describe("OverlayBackground", () => {
- const mockUserId = Utils.newGuid() as UserId;
- const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
- const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
- let domainSettingsService: DomainSettingsService;
- let buttonPortSpy: chrome.runtime.Port;
- let listPortSpy: chrome.runtime.Port;
- let overlayBackground: LegacyOverlayBackground;
- const cipherService = mock();
- const autofillService = mock();
- let configService: MockProxy;
- let activeAccountStatusMock$: BehaviorSubject;
- let authService: MockProxy;
-
- const environmentService = mock();
- environmentService.environment$ = new BehaviorSubject(
- new CloudEnvironment({
- key: Region.US,
- domain: "bitwarden.com",
- urls: { icons: "https://icons.bitwarden.com/" },
- }),
- );
- const autofillSettingsService = mock();
- const i18nService = mock();
- const platformUtilsService = mock();
- const themeStateService = mock();
- const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => {
- const { initList, initButton } = options;
- if (initButton) {
- await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button));
- buttonPortSpy = overlayBackground["overlayButtonPort"];
- }
-
- if (initList) {
- await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List));
- listPortSpy = overlayBackground["overlayListPort"];
- }
-
- return { buttonPortSpy, listPortSpy };
- };
-
- beforeEach(() => {
- configService = mock();
- configService.getFeatureFlag$.mockImplementation(() => of(true));
- domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
- activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
- authService = mock();
- authService.activeAccountStatus$ = activeAccountStatusMock$;
- overlayBackground = new LegacyOverlayBackground(
- cipherService,
- autofillService,
- authService,
- environmentService,
- domainSettingsService,
- autofillSettingsService,
- i18nService,
- platformUtilsService,
- themeStateService,
- accountService,
- );
-
- jest
- .spyOn(overlayBackground as any, "getOverlayVisibility")
- .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
-
- themeStateService.selectedTheme$ = of(ThemeType.Light);
- domainSettingsService.showFavicons$ = of(true);
-
- void overlayBackground.init();
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- mockReset(cipherService);
- });
-
- describe("removePageDetails", () => {
- it("removes the page details for a specific tab from the pageDetailsForTab object", () => {
- const tabId = 1;
- const frameId = 2;
- overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]);
- overlayBackground.removePageDetails(tabId);
-
- expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined();
- });
- });
-
- describe("init", () => {
- it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => {
- overlayBackground["setupExtensionMessageListeners"] = jest.fn();
- overlayBackground["getOverlayVisibility"] = jest.fn();
- overlayBackground["getAuthStatus"] = jest.fn();
-
- await overlayBackground.init();
-
- expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled();
- expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled();
- expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled();
- });
- });
-
- describe("updateOverlayCiphers", () => {
- const url = "https://jest-testing-website.com";
- const tab = createChromeTabMock({ url });
- const cipher1 = mock({
- id: "id-1",
- localData: { lastUsedDate: 222 },
- name: "name-1",
- type: CipherType.Login,
- login: { username: "username-1", uri: url },
- });
- const cipher2 = mock({
- id: "id-2",
- localData: { lastUsedDate: 111 },
- name: "name-2",
- type: CipherType.Login,
- login: { username: "username-2", uri: url },
- });
-
- beforeEach(() => {
- activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
- });
-
- it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => {
- activeAccountStatusMock$.next(AuthenticationStatus.Locked);
- jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
- jest.spyOn(cipherService, "getAllDecryptedForUrl");
-
- await overlayBackground.updateOverlayCiphers();
-
- expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled();
- });
-
- it("ignores updating the overlay ciphers if the tab is undefined", async () => {
- jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined);
- jest.spyOn(cipherService, "getAllDecryptedForUrl");
-
- await overlayBackground.updateOverlayCiphers();
-
- expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled();
- });
-
- it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => {
- jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab);
- cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
- cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
- jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
- jest.spyOn(overlayBackground as any, "getOverlayCipherData");
-
- await overlayBackground.updateOverlayCiphers();
-
- expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId);
- expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled();
- expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual(
- new Map([
- ["overlay-cipher-0", cipher2],
- ["overlay-cipher-1", cipher1],
- ]),
- );
- expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled();
- });
-
- it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => {
- overlayBackground["overlayListPort"] = mock();
- cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
- cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
- jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab);
- jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
-
- await overlayBackground.updateOverlayCiphers();
-
- expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({
- command: "updateOverlayListCiphers",
- ciphers: [
- {
- card: null,
- favorite: cipher2.favorite,
- icon: {
- fallbackImage: "images/bwi-globe.png",
- icon: "bwi-globe",
- image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
- imageEnabled: true,
- },
- id: "overlay-cipher-0",
- login: {
- username: "username-2",
- },
- name: "name-2",
- reprompt: cipher2.reprompt,
- type: 1,
- },
- {
- card: null,
- favorite: cipher1.favorite,
- icon: {
- fallbackImage: "images/bwi-globe.png",
- icon: "bwi-globe",
- image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
- imageEnabled: true,
- },
- id: "overlay-cipher-1",
- login: {
- username: "username-1",
- },
- name: "name-1",
- reprompt: cipher1.reprompt,
- type: 1,
- },
- ],
- });
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- tab,
- "updateIsOverlayCiphersPopulated",
- { isOverlayCiphersPopulated: true },
- );
- });
- });
-
- describe("getOverlayCipherData", () => {
- const url = "https://jest-testing-website.com";
- const cipher1 = mock({
- id: "id-1",
- localData: { lastUsedDate: 222 },
- name: "name-1",
- type: CipherType.Login,
- login: { username: "username-1", uri: url },
- });
- const cipher2 = mock({
- id: "id-2",
- localData: { lastUsedDate: 111 },
- name: "name-2",
- type: CipherType.Login,
- login: { username: "username-2", uri: url },
- });
- const cipher3 = mock({
- id: "id-3",
- localData: { lastUsedDate: 333 },
- name: "name-3",
- type: CipherType.Card,
- card: { subTitle: "Visa, *6789" },
- });
- const cipher4 = mock({
- id: "id-4",
- localData: { lastUsedDate: 444 },
- name: "name-4",
- type: CipherType.Card,
- card: { subTitle: "Mastercard, *1234" },
- });
-
- it("formats and returns the cipher data", async () => {
- overlayBackground["overlayLoginCiphers"] = new Map([
- ["overlay-cipher-0", cipher2],
- ["overlay-cipher-1", cipher1],
- ["overlay-cipher-2", cipher3],
- ["overlay-cipher-3", cipher4],
- ]);
-
- const overlayCipherData = await overlayBackground["getOverlayCipherData"]();
-
- expect(overlayCipherData).toStrictEqual([
- {
- card: null,
- favorite: cipher2.favorite,
- icon: {
- fallbackImage: "images/bwi-globe.png",
- icon: "bwi-globe",
- image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
- imageEnabled: true,
- },
- id: "overlay-cipher-0",
- login: {
- username: "username-2",
- },
- name: "name-2",
- reprompt: cipher2.reprompt,
- type: 1,
- },
- {
- card: null,
- favorite: cipher1.favorite,
- icon: {
- fallbackImage: "images/bwi-globe.png",
- icon: "bwi-globe",
- image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
- imageEnabled: true,
- },
- id: "overlay-cipher-1",
- login: {
- username: "username-1",
- },
- name: "name-1",
- reprompt: cipher1.reprompt,
- type: 1,
- },
- {
- card: "Visa, *6789",
- favorite: cipher3.favorite,
- icon: {
- fallbackImage: "",
- icon: "bwi-credit-card",
- image: null,
- imageEnabled: true,
- },
- id: "overlay-cipher-2",
- login: null,
- name: "name-3",
- reprompt: cipher3.reprompt,
- type: 3,
- },
- {
- card: "Mastercard, *1234",
- favorite: cipher4.favorite,
- icon: {
- fallbackImage: "",
- icon: "bwi-credit-card",
- image: null,
- imageEnabled: true,
- },
- id: "overlay-cipher-3",
- login: null,
- name: "name-4",
- reprompt: cipher4.reprompt,
- type: 3,
- },
- ]);
- });
- });
-
- describe("getAuthStatus", () => {
- it("will update the user's auth status but will not update the overlay ciphers", async () => {
- const authStatus = AuthenticationStatus.Unlocked;
- overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked;
- jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus);
- jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation();
- jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation();
-
- const status = await overlayBackground["getAuthStatus"]();
-
- expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled();
- expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled();
- expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled();
- expect(overlayBackground["userAuthStatus"]).toBe(authStatus);
- expect(status).toBe(authStatus);
- });
-
- it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => {
- const authStatus = AuthenticationStatus.Unlocked;
- overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut;
- jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus);
- jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation();
- jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation();
-
- await overlayBackground["getAuthStatus"]();
-
- expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled();
- expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled();
- expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled();
- expect(overlayBackground["userAuthStatus"]).toBe(authStatus);
- });
- });
-
- describe("updateOverlayButtonAuthStatus", () => {
- it("will send a message to the button port with the user's auth status", () => {
- overlayBackground["overlayButtonPort"] = mock();
- jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage");
-
- overlayBackground["updateOverlayButtonAuthStatus"]();
-
- expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({
- command: "updateOverlayButtonAuthStatus",
- authStatus: overlayBackground["userAuthStatus"],
- });
- });
- });
-
- describe("getTranslations", () => {
- it("will query the overlay page translations if they have not been queried", () => {
- overlayBackground["overlayPageTranslations"] = undefined;
- jest.spyOn(overlayBackground as any, "getTranslations");
- jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key);
- jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en");
-
- const translations = overlayBackground["getTranslations"]();
-
- expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
- const translationKeys = [
- "opensInANewWindow",
- "bitwardenOverlayButton",
- "toggleBitwardenVaultOverlay",
- "bitwardenVault",
- "unlockYourAccountToViewMatchingLogins",
- "unlockAccount",
- "fillCredentialsFor",
- "partialUsername",
- "view",
- "noItemsToShow",
- "newItem",
- "addNewVaultItem",
- ];
- translationKeys.forEach((key) => {
- expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key);
- });
- expect(translations).toStrictEqual({
- locale: "en",
- opensInANewWindow: "opensInANewWindow",
- buttonPageTitle: "bitwardenOverlayButton",
- toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay",
- listPageTitle: "bitwardenVault",
- unlockYourAccount: "unlockYourAccountToViewMatchingLogins",
- unlockAccount: "unlockAccount",
- fillCredentialsFor: "fillCredentialsFor",
- partialUsername: "partialUsername",
- view: "view",
- noItemsToShow: "noItemsToShow",
- newItem: "newItem",
- addNewVaultItem: "addNewVaultItem",
- });
- });
- });
-
- describe("setupExtensionMessageListeners", () => {
- it("will set up onMessage and onConnect listeners", () => {
- overlayBackground["setupExtensionMessageListeners"]();
-
- expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled();
- expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled();
- });
- });
-
- describe("handleExtensionMessage", () => {
- it("will return early if the message command is not present within the extensionMessageHandlers", () => {
- const message = {
- command: "not-a-command",
- };
- const sender = mock({ tab: { id: 1 } });
- const sendResponse = jest.fn();
-
- const returnValue = overlayBackground["handleExtensionMessage"](
- message,
- sender,
- sendResponse,
- );
-
- expect(returnValue).toBe(null);
- expect(sendResponse).not.toHaveBeenCalled();
- });
-
- it("will trigger the message handler and return undefined if the message does not have a response", () => {
- const message = {
- command: "autofillOverlayElementClosed",
- };
- const sender = mock({ tab: { id: 1 } });
- const sendResponse = jest.fn();
- jest.spyOn(overlayBackground as any, "overlayElementClosed");
-
- const returnValue = overlayBackground["handleExtensionMessage"](
- message,
- sender,
- sendResponse,
- );
-
- expect(returnValue).toBe(null);
- expect(sendResponse).not.toHaveBeenCalled();
- expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender);
- });
-
- it("will return a response if the message handler returns a response", async () => {
- const message = {
- command: "openAutofillOverlay",
- };
- const sender = mock({ tab: { id: 1 } });
- const sendResponse = jest.fn();
- jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations");
-
- const returnValue = overlayBackground["handleExtensionMessage"](
- message,
- sender,
- sendResponse,
- );
-
- expect(returnValue).toBe(true);
- });
-
- describe("extension message handlers", () => {
- beforeEach(() => {
- jest
- .spyOn(overlayBackground as any, "getAuthStatus")
- .mockResolvedValue(AuthenticationStatus.Unlocked);
- });
-
- describe("openAutofillOverlay message handler", () => {
- it("opens the autofill overlay by sending a message to the current tab", async () => {
- const sender = mock({ tab: { id: 1 } });
- jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab);
- jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
-
- sendMockExtensionMessage({ command: "openAutofillOverlay" });
- await flushPromises();
-
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- sender.tab,
- "openAutofillOverlay",
- {
- isFocusingFieldElement: false,
- isOpeningFullOverlay: false,
- authStatus: AuthenticationStatus.Unlocked,
- },
- );
- });
- });
-
- describe("autofillOverlayElementClosed message handler", () => {
- beforeEach(async () => {
- await initOverlayElementPorts();
- });
-
- it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => {
- const port1 = mock();
- const port2 = mock();
- overlayBackground["expiredPorts"] = [port1, port2];
- const sender = mock({ tab: { id: 1 } });
- const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- sendMockExtensionMessage(
- {
- command: "autofillOverlayElementClosed",
- overlayElement: AutofillOverlayElement.Button,
- },
- sender,
- );
-
- expect(port1.disconnect).toHaveBeenCalled();
- expect(port2.disconnect).toHaveBeenCalled();
- });
-
- it("disconnects the button element port", () => {
- sendMockExtensionMessage({
- command: "autofillOverlayElementClosed",
- overlayElement: AutofillOverlayElement.Button,
- });
-
- expect(buttonPortSpy.disconnect).toHaveBeenCalled();
- expect(overlayBackground["overlayButtonPort"]).toBeNull();
- });
-
- it("disconnects the list element port", () => {
- sendMockExtensionMessage({
- command: "autofillOverlayElementClosed",
- overlayElement: AutofillOverlayElement.List,
- });
-
- expect(listPortSpy.disconnect).toHaveBeenCalled();
- expect(overlayBackground["overlayListPort"]).toBeNull();
- });
- });
-
- describe("autofillOverlayAddNewVaultItem message handler", () => {
- let sender: chrome.runtime.MessageSender;
- beforeEach(() => {
- sender = mock({ tab: { id: 1 } });
- jest
- .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo")
- .mockImplementation();
- jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation();
- });
-
- it("will not open the add edit popout window if the message does not have a login cipher provided", () => {
- sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender);
-
- expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled();
- expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled();
- });
-
- it("will open the add edit popout window after creating a new cipher", async () => {
- jest.spyOn(BrowserApi, "sendMessage");
-
- sendMockExtensionMessage(
- {
- command: "autofillOverlayAddNewVaultItem",
- login: {
- uri: "https://tacos.com",
- hostname: "",
- username: "username",
- password: "password",
- },
- },
- sender,
- );
- await flushPromises();
-
- expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled();
- expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled();
- });
- });
-
- describe("getAutofillOverlayVisibility message handler", () => {
- beforeEach(() => {
- jest
- .spyOn(overlayBackground as any, "getOverlayVisibility")
- .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
- });
-
- it("will set the overlayVisibility property", async () => {
- sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" });
- await flushPromises();
-
- expect(await overlayBackground["getOverlayVisibility"]()).toBe(
- AutofillOverlayVisibility.OnFieldFocus,
- );
- });
-
- it("returns the overlayVisibility property", async () => {
- const sendMessageSpy = jest.fn();
-
- sendMockExtensionMessage(
- { command: "getAutofillOverlayVisibility" },
- undefined,
- sendMessageSpy,
- );
- await flushPromises();
-
- expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus);
- });
- });
-
- describe("checkAutofillOverlayFocused message handler", () => {
- beforeEach(async () => {
- await initOverlayElementPorts();
- });
-
- it("will check if the overlay list is focused if the list port is open", () => {
- sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" });
-
- expect(listPortSpy.postMessage).toHaveBeenCalledWith({
- command: "checkAutofillOverlayListFocused",
- });
- expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({
- command: "checkAutofillOverlayButtonFocused",
- });
- });
-
- it("will check if the overlay button is focused if the list port is not open", () => {
- overlayBackground["overlayListPort"] = undefined;
-
- sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" });
-
- expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
- command: "checkAutofillOverlayButtonFocused",
- });
- expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
- command: "checkAutofillOverlayListFocused",
- });
- });
- });
-
- describe("focusAutofillOverlayList message handler", () => {
- it("will send a `focusOverlayList` message to the overlay list port", async () => {
- await initOverlayElementPorts({ initList: true, initButton: false });
-
- sendMockExtensionMessage({ command: "focusAutofillOverlayList" });
-
- expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" });
- });
- });
-
- describe("updateAutofillOverlayPosition message handler", () => {
- beforeEach(async () => {
- await overlayBackground["handlePortOnConnect"](
- createPortSpyMock(AutofillOverlayPort.List),
- );
- listPortSpy = overlayBackground["overlayListPort"];
-
- await overlayBackground["handlePortOnConnect"](
- createPortSpyMock(AutofillOverlayPort.Button),
- );
- buttonPortSpy = overlayBackground["overlayButtonPort"];
- });
-
- it("ignores updating the position if the overlay element type is not provided", () => {
- sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" });
-
- expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: expect.anything(),
- });
- expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: expect.anything(),
- });
- });
-
- it("skips updating the position if the most recently focused field is different than the message sender", () => {
- const sender = mock({ tab: { id: 1 } });
- const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender);
-
- expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: expect.anything(),
- });
- expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: expect.anything(),
- });
- });
-
- it("updates the overlay button's position", () => {
- const focusedFieldData = createFocusedFieldDataMock();
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- sendMockExtensionMessage({
- command: "updateAutofillOverlayPosition",
- overlayElement: AutofillOverlayElement.Button,
- });
-
- expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: { height: "2px", left: "4px", top: "2px", width: "2px" },
- });
- });
-
- it("modifies the overlay button's height for medium sized input elements", () => {
- const focusedFieldData = createFocusedFieldDataMock({
- focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 },
- });
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- sendMockExtensionMessage({
- command: "updateAutofillOverlayPosition",
- overlayElement: AutofillOverlayElement.Button,
- });
-
- expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: { height: "20px", left: "-22px", top: "8px", width: "20px" },
- });
- });
-
- it("modifies the overlay button's height for large sized input elements", () => {
- const focusedFieldData = createFocusedFieldDataMock({
- focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 },
- });
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- sendMockExtensionMessage({
- command: "updateAutofillOverlayPosition",
- overlayElement: AutofillOverlayElement.Button,
- });
-
- expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: { height: "27px", left: "-32px", top: "13px", width: "27px" },
- });
- });
-
- it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => {
- const focusedFieldData = createFocusedFieldDataMock({
- focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" },
- });
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- sendMockExtensionMessage({
- command: "updateAutofillOverlayPosition",
- overlayElement: AutofillOverlayElement.Button,
- });
-
- expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: { height: "2px", left: "-18px", top: "2px", width: "2px" },
- });
- });
-
- it("will post a message to the overlay list facilitating an update of the list's position", () => {
- const sender = mock({ tab: { id: 1 } });
- const focusedFieldData = createFocusedFieldDataMock();
- sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData });
-
- overlayBackground["updateOverlayPosition"](
- { overlayElement: AutofillOverlayElement.List },
- sender,
- );
- sendMockExtensionMessage({
- command: "updateAutofillOverlayPosition",
- overlayElement: AutofillOverlayElement.List,
- });
-
- expect(listPortSpy.postMessage).toHaveBeenCalledWith({
- command: "updateIframePosition",
- styles: { left: "2px", top: "4px", width: "4px" },
- });
- });
- });
-
- describe("updateOverlayHidden", () => {
- beforeEach(async () => {
- await initOverlayElementPorts();
- });
-
- it("returns early if the display value is not provided", () => {
- const message = {
- command: "updateAutofillOverlayHidden",
- };
-
- sendMockExtensionMessage(message);
-
- expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message);
- expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message);
- });
-
- it("posts a message to the overlay button and list with the display value", () => {
- const message = { command: "updateAutofillOverlayHidden", display: "none" };
-
- sendMockExtensionMessage(message);
-
- expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({
- command: "updateOverlayHidden",
- styles: {
- display: message.display,
- },
- });
- expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({
- command: "updateOverlayHidden",
- styles: {
- display: message.display,
- },
- });
- });
- });
-
- describe("collectPageDetailsResponse message handler", () => {
- let sender: chrome.runtime.MessageSender;
- const pageDetails1 = createAutofillPageDetailsMock({
- login: { username: "username1", password: "password1" },
- });
- const pageDetails2 = createAutofillPageDetailsMock({
- login: { username: "username2", password: "password2" },
- });
-
- beforeEach(() => {
- sender = mock({ tab: { id: 1 } });
- });
-
- it("stores the page details provided by the message by the tab id of the sender", () => {
- sendMockExtensionMessage(
- { command: "collectPageDetailsResponse", details: pageDetails1 },
- sender,
- );
-
- expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual(
- new Map([
- [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }],
- ]),
- );
- });
-
- it("updates the page details for a tab that already has a set of page details stored ", () => {
- const secondFrameSender = mock({
- tab: { id: 1 },
- frameId: 3,
- });
- overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
- [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }],
- ]);
-
- sendMockExtensionMessage(
- { command: "collectPageDetailsResponse", details: pageDetails2 },
- secondFrameSender,
- );
-
- expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual(
- new Map([
- [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }],
- [
- secondFrameSender.frameId,
- {
- frameId: secondFrameSender.frameId,
- tab: secondFrameSender.tab,
- details: pageDetails2,
- },
- ],
- ]),
- );
- });
- });
-
- describe("unlockCompleted message handler", () => {
- let getAuthStatusSpy: jest.SpyInstance;
-
- beforeEach(() => {
- overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut;
- jest.spyOn(BrowserApi, "tabSendMessageData");
- getAuthStatusSpy = jest
- .spyOn(overlayBackground as any, "getAuthStatus")
- .mockImplementation(() => {
- overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked;
- return Promise.resolve(AuthenticationStatus.Unlocked);
- });
- });
-
- it("updates the user's auth status but does not open the overlay", async () => {
- const message = {
- command: "unlockCompleted",
- data: {
- commandToRetry: { message: { command: "" } },
- },
- };
-
- sendMockExtensionMessage(message);
- await flushPromises();
-
- expect(getAuthStatusSpy).toHaveBeenCalled();
- expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled();
- });
-
- it("updates user's auth status and opens the overlay if a follow up command is provided", async () => {
- const sender = mock({ tab: { id: 1 } });
- const message = {
- command: "unlockCompleted",
- data: {
- commandToRetry: { message: { command: "openAutofillOverlay" } },
- },
- };
- jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab);
-
- sendMockExtensionMessage(message);
- await flushPromises();
-
- expect(getAuthStatusSpy).toHaveBeenCalled();
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- sender.tab,
- "openAutofillOverlay",
- {
- isFocusingFieldElement: true,
- isOpeningFullOverlay: false,
- authStatus: AuthenticationStatus.Unlocked,
- },
- );
- });
- });
-
- describe("extension messages that trigger an update of the inline menu ciphers", () => {
- const extensionMessages = [
- "addedCipher",
- "addEditCipherSubmitted",
- "editedCipher",
- "deletedCipher",
- ];
-
- beforeEach(() => {
- jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation();
- });
-
- extensionMessages.forEach((message) => {
- it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => {
- sendMockExtensionMessage({ command: message });
- expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled();
- });
- });
- });
- });
- });
-
- describe("handlePortOnConnect", () => {
- beforeEach(() => {
- jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation();
- jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation();
- jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation();
- jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation();
- });
-
- it("skips setting up the overlay port if the port connection is not for an overlay element", async () => {
- const port = createPortSpyMock("not-an-overlay-element");
-
- await overlayBackground["handlePortOnConnect"](port);
-
- expect(port.onMessage.addListener).not.toHaveBeenCalled();
- expect(port.postMessage).not.toHaveBeenCalled();
- });
-
- it("sets up the overlay list port if the port connection is for the overlay list", async () => {
- await initOverlayElementPorts({ initList: true, initButton: false });
- await flushPromises();
-
- expect(overlayBackground["overlayButtonPort"]).toBeUndefined();
- expect(listPortSpy.onMessage.addListener).toHaveBeenCalled();
- expect(listPortSpy.postMessage).toHaveBeenCalled();
- expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled();
- expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css");
- expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
- expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled();
- expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith(
- { overlayElement: AutofillOverlayElement.List },
- listPortSpy.sender,
- );
- });
-
- it("sets up the overlay button port if the port connection is for the overlay button", async () => {
- await initOverlayElementPorts({ initList: false, initButton: true });
- await flushPromises();
-
- expect(overlayBackground["overlayListPort"]).toBeUndefined();
- expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled();
- expect(buttonPortSpy.postMessage).toHaveBeenCalled();
- expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled();
- expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css");
- expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
- expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith(
- { overlayElement: AutofillOverlayElement.Button },
- buttonPortSpy.sender,
- );
- });
-
- it("stores an existing overlay port so that it can be disconnected at a later time", async () => {
- overlayBackground["overlayButtonPort"] = mock();
-
- await initOverlayElementPorts({ initList: false, initButton: true });
- await flushPromises();
-
- expect(overlayBackground["expiredPorts"].length).toBe(1);
- });
-
- it("gets the system theme", async () => {
- themeStateService.selectedTheme$ = of(ThemeType.System);
-
- await initOverlayElementPorts({ initList: true, initButton: false });
- await flushPromises();
-
- expect(listPortSpy.postMessage).toHaveBeenCalledWith(
- expect.objectContaining({ theme: ThemeType.System }),
- );
- });
- });
-
- describe("handleOverlayElementPortMessage", () => {
- beforeEach(async () => {
- await initOverlayElementPorts();
- overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked;
- });
-
- it("ignores port messages that do not contain a handler", () => {
- jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation();
-
- sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" });
-
- expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled();
- });
-
- describe("overlay button message handlers", () => {
- it("unlocks the vault if the user auth status is not unlocked", () => {
- overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut;
- jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation();
-
- sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" });
-
- expect(overlayBackground["unlockVault"]).toHaveBeenCalled();
- });
-
- it("opens the autofill overlay if the auth status is unlocked", () => {
- jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation();
-
- sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" });
-
- expect(overlayBackground["openOverlay"]).toHaveBeenCalled();
- });
-
- describe("closeAutofillOverlay", () => {
- it("sends a `closeOverlay` message to the sender tab", () => {
- jest.spyOn(BrowserApi, "tabSendMessageData");
-
- sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" });
-
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- buttonPortSpy.sender.tab,
- "closeAutofillOverlay",
- { forceCloseOverlay: false },
- );
- });
- });
-
- describe("forceCloseAutofillOverlay", () => {
- it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => {
- jest.spyOn(BrowserApi, "tabSendMessageData");
-
- sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" });
-
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- buttonPortSpy.sender.tab,
- "closeAutofillOverlay",
- { forceCloseOverlay: true },
- );
- });
- });
-
- describe("overlayPageBlurred", () => {
- it("checks if the overlay list is focused", () => {
- jest.spyOn(overlayBackground as any, "checkOverlayListFocused");
-
- sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" });
-
- expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled();
- });
- });
-
- describe("redirectOverlayFocusOut", () => {
- beforeEach(() => {
- jest.spyOn(BrowserApi, "tabSendMessageData");
- });
-
- it("ignores the redirect message if the direction is not provided", () => {
- sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" });
-
- expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled();
- });
-
- it("sends the redirect message if the direction is provided", () => {
- sendPortMessage(buttonPortSpy, {
- command: "redirectOverlayFocusOut",
- direction: RedirectFocusDirection.Next,
- });
-
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- buttonPortSpy.sender.tab,
- "redirectOverlayFocusOut",
- { direction: RedirectFocusDirection.Next },
- );
- });
- });
- });
-
- describe("overlay list message handlers", () => {
- describe("checkAutofillOverlayButtonFocused", () => {
- it("checks on the focus state of the overlay button", () => {
- jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation();
-
- sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" });
-
- expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled();
- });
- });
-
- describe("forceCloseAutofillOverlay", () => {
- it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => {
- jest.spyOn(BrowserApi, "tabSendMessageData");
-
- sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" });
-
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- listPortSpy.sender.tab,
- "closeAutofillOverlay",
- { forceCloseOverlay: true },
- );
- });
- });
-
- describe("overlayPageBlurred", () => {
- it("checks on the focus state of the overlay button", () => {
- jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation();
-
- sendPortMessage(listPortSpy, { command: "overlayPageBlurred" });
-
- expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled();
- });
- });
-
- describe("unlockVault", () => {
- it("closes the autofill overlay and opens the unlock popout", async () => {
- jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation();
- jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation();
- jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
-
- sendPortMessage(listPortSpy, { command: "unlockVault" });
- await flushPromises();
-
- expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy);
- expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
- listPortSpy.sender.tab,
- "addToLockedVaultPendingNotifications",
- {
- commandToRetry: {
- message: { command: "openAutofillOverlay" },
- sender: listPortSpy.sender,
- },
- target: "overlay.background",
- },
- );
- expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith(
- listPortSpy.sender.tab,
- true,
- );
- });
- });
-
- describe("fillSelectedListItem", () => {
- let getLoginCiphersSpy: jest.SpyInstance;
- let isPasswordRepromptRequiredSpy: jest.SpyInstance;
- let doAutoFillSpy: jest.SpyInstance;
- let sender: chrome.runtime.MessageSender;
- const pageDetails = createAutofillPageDetailsMock({
- login: { username: "username1", password: "password1" },
- });
-
- beforeEach(() => {
- getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get");
- isPasswordRepromptRequiredSpy = jest.spyOn(
- overlayBackground["autofillService"],
- "isPasswordRepromptRequired",
- );
- doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill");
- sender = mock({ tab: { id: 1 } });
- });
-
- it("ignores the fill request if the overlay cipher id is not provided", async () => {
- sendPortMessage(listPortSpy, { command: "fillSelectedListItem" });
- await flushPromises();
-
- expect(getLoginCiphersSpy).not.toHaveBeenCalled();
- expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled();
- expect(doAutoFillSpy).not.toHaveBeenCalled();
- });
-
- it("ignores the fill request if the tab does not contain any identified page details", async () => {
- sendPortMessage(listPortSpy, {
- command: "fillSelectedListItem",
- overlayCipherId: "overlay-cipher-1",
- });
- await flushPromises();
-
- expect(getLoginCiphersSpy).not.toHaveBeenCalled();
- expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled();
- expect(doAutoFillSpy).not.toHaveBeenCalled();
- });
-
- it("ignores the fill request if a master password reprompt is required", async () => {
- const cipher = mock({
- reprompt: CipherRepromptType.Password,
- type: CipherType.Login,
- });
- overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]);
- overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
- [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }],
- ]);
- getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get");
- isPasswordRepromptRequiredSpy.mockResolvedValue(true);
-
- sendPortMessage(listPortSpy, {
- command: "fillSelectedListItem",
- overlayCipherId: "overlay-cipher-1",
- });
- await flushPromises();
-
- expect(getLoginCiphersSpy).toHaveBeenCalled();
- expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith(
- cipher,
- listPortSpy.sender.tab,
- );
- expect(doAutoFillSpy).not.toHaveBeenCalled();
- });
-
- it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => {
- const cipher1 = mock({ id: "overlay-cipher-1" });
- const cipher2 = mock({ id: "overlay-cipher-2" });
- const cipher3 = mock({ id: "overlay-cipher-3" });
- overlayBackground["overlayLoginCiphers"] = new Map([
- ["overlay-cipher-1", cipher1],
- ["overlay-cipher-2", cipher2],
- ["overlay-cipher-3", cipher3],
- ]);
- const pageDetailsForTab = {
- frameId: sender.frameId,
- tab: sender.tab,
- details: pageDetails,
- };
- overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
- [sender.frameId, pageDetailsForTab],
- ]);
- isPasswordRepromptRequiredSpy.mockResolvedValue(false);
-
- sendPortMessage(listPortSpy, {
- command: "fillSelectedListItem",
- overlayCipherId: "overlay-cipher-2",
- });
- await flushPromises();
-
- expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith(
- cipher2,
- listPortSpy.sender.tab,
- );
- expect(doAutoFillSpy).toHaveBeenCalledWith({
- tab: listPortSpy.sender.tab,
- cipher: cipher2,
- pageDetails: [pageDetailsForTab],
- fillNewPassword: true,
- allowTotpAutofill: true,
- });
- expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual(
- new Map([
- ["overlay-cipher-2", cipher2],
- ["overlay-cipher-1", cipher1],
- ["overlay-cipher-3", cipher3],
- ]).entries(),
- );
- });
-
- it("copies the cipher's totp code to the clipboard after filling", async () => {
- const cipher1 = mock({ id: "overlay-cipher-1" });
- overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]);
- overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
- [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }],
- ]);
- isPasswordRepromptRequiredSpy.mockResolvedValue(false);
- const copyToClipboardSpy = jest
- .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard")
- .mockImplementation();
- doAutoFillSpy.mockReturnValueOnce("totp-code");
-
- sendPortMessage(listPortSpy, {
- command: "fillSelectedListItem",
- overlayCipherId: "overlay-cipher-2",
- });
- await flushPromises();
-
- expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code");
- });
- });
-
- describe("getNewVaultItemDetails", () => {
- it("will send an addNewVaultItemFromOverlay message", async () => {
- jest.spyOn(BrowserApi, "tabSendMessage");
-
- sendPortMessage(listPortSpy, { command: "addNewVaultItem" });
- await flushPromises();
-
- expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, {
- command: "addNewVaultItemFromOverlay",
- });
- });
- });
-
- describe("viewSelectedCipher", () => {
- let openViewVaultItemPopoutSpy: jest.SpyInstance;
-
- beforeEach(() => {
- openViewVaultItemPopoutSpy = jest
- .spyOn(overlayBackground as any, "openViewVaultItemPopout")
- .mockImplementation();
- });
-
- it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => {
- overlayBackground["overlayLoginCiphers"] = new Map([
- ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })],
- ]);
-
- sendPortMessage(listPortSpy, {
- command: "viewSelectedCipher",
- overlayCipherId: "overlay-cipher-1",
- });
- await flushPromises();
-
- expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled();
- });
-
- it("will open the view vault item popout with the selected cipher", async () => {
- const cipher = mock({ id: "overlay-cipher-1" });
- overlayBackground["overlayLoginCiphers"] = new Map([
- ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })],
- ["overlay-cipher-1", cipher],
- ]);
-
- sendPortMessage(listPortSpy, {
- command: "viewSelectedCipher",
- overlayCipherId: "overlay-cipher-1",
- });
- await flushPromises();
-
- expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith(
- listPortSpy.sender.tab,
- {
- cipherId: cipher.id,
- action: SHOW_AUTOFILL_BUTTON,
- },
- );
- });
- });
-
- describe("redirectOverlayFocusOut", () => {
- it("redirects focus out of the overlay list", async () => {
- const message = {
- command: "redirectOverlayFocusOut",
- direction: RedirectFocusDirection.Next,
- };
- const redirectOverlayFocusOutSpy = jest.spyOn(
- overlayBackground as any,
- "redirectOverlayFocusOut",
- );
-
- sendPortMessage(listPortSpy, message);
- await flushPromises();
-
- expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy);
- });
- });
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts
deleted file mode 100644
index c9eb442d75d..00000000000
--- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts
+++ /dev/null
@@ -1,811 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
-
-import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
-import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
-import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
-import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
-import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
-import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
-import { CipherType } from "@bitwarden/common/vault/enums";
-import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
-import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
-import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
-import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
-
-import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window";
-import { BrowserApi } from "../../../platform/browser/browser-api";
-import {
- openViewVaultItemPopout,
- openAddEditVaultItemPopout,
-} from "../../../vault/popup/utils/vault-popout-window";
-import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background";
-import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background";
-import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum";
-import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service";
-
-import {
- FocusedFieldData,
- OverlayBackgroundExtensionMessageHandlers,
- OverlayButtonPortMessageHandlers,
- OverlayCipherData,
- OverlayListPortMessageHandlers,
- OverlayBackgroundExtensionMessage,
- OverlayAddNewItemMessage,
- OverlayPortMessage,
- WebsiteIconData,
-} from "./abstractions/overlay.background.deprecated";
-
-class LegacyOverlayBackground implements OverlayBackgroundInterface {
- private readonly openUnlockPopout = openUnlockPopout;
- private readonly openViewVaultItemPopout = openViewVaultItemPopout;
- private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout;
- private overlayLoginCiphers: Map = new Map();
- private pageDetailsForTab: Record<
- chrome.runtime.MessageSender["tab"]["id"],
- Map
- > = {};
- private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
- private overlayButtonPort: chrome.runtime.Port;
- private overlayListPort: chrome.runtime.Port;
- private expiredPorts: chrome.runtime.Port[] = [];
- private focusedFieldData: FocusedFieldData;
- private overlayPageTranslations: Record;
- private iconsServerUrl: string;
- private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
- openAutofillOverlay: () => this.openOverlay(false),
- autofillOverlayElementClosed: ({ message, sender }) =>
- this.overlayElementClosed(message, sender),
- autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
- getAutofillOverlayVisibility: () => this.getOverlayVisibility(),
- checkAutofillOverlayFocused: () => this.checkOverlayFocused(),
- focusAutofillOverlayList: () => this.focusOverlayList(),
- updateAutofillOverlayPosition: ({ message, sender }) =>
- this.updateOverlayPosition(message, sender),
- updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message),
- updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
- collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
- unlockCompleted: ({ message }) => this.unlockCompleted(message),
- addedCipher: () => this.updateOverlayCiphers(),
- addEditCipherSubmitted: () => this.updateOverlayCiphers(),
- editedCipher: () => this.updateOverlayCiphers(),
- deletedCipher: () => this.updateOverlayCiphers(),
- };
- private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = {
- overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port),
- closeAutofillOverlay: ({ port }) => this.closeOverlay(port),
- forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true),
- overlayPageBlurred: () => this.checkOverlayListFocused(),
- redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
- };
- private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = {
- checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(),
- forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true),
- overlayPageBlurred: () => this.checkOverlayButtonFocused(),
- unlockVault: ({ port }) => this.unlockVault(port),
- fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port),
- addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port),
- viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
- redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
- };
-
- constructor(
- private cipherService: CipherService,
- private autofillService: AutofillService,
- private authService: AuthService,
- private environmentService: EnvironmentService,
- private domainSettingsService: DomainSettingsService,
- private autofillSettingsService: AutofillSettingsServiceAbstraction,
- private i18nService: I18nService,
- private platformUtilsService: PlatformUtilsService,
- private themeStateService: ThemeStateService,
- private accountService: AccountService,
- ) {}
-
- /**
- * Removes cached page details for a tab
- * based on the passed tabId.
- *
- * @param tabId - Used to reference the page details of a specific tab
- */
- removePageDetails(tabId: number) {
- if (!this.pageDetailsForTab[tabId]) {
- return;
- }
-
- this.pageDetailsForTab[tabId].clear();
- delete this.pageDetailsForTab[tabId];
- }
-
- /**
- * Sets up the extension message listeners and gets the settings for the
- * overlay's visibility and the user's authentication status.
- */
- async init() {
- this.setupExtensionMessageListeners();
- const env = await firstValueFrom(this.environmentService.environment$);
- this.iconsServerUrl = env.getIconsUrl();
- await this.getOverlayVisibility();
- await this.getAuthStatus();
- }
-
- /**
- * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe.
- * Queries all ciphers for the given url, and sorts them by last used. Will not update the
- * list of ciphers if the extension is not unlocked.
- */
- async updateOverlayCiphers() {
- const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
- if (authStatus !== AuthenticationStatus.Unlocked) {
- return;
- }
-
- const currentTab = await BrowserApi.getTabFromCurrentWindowId();
- if (!currentTab?.url) {
- return;
- }
-
- this.overlayLoginCiphers = new Map();
-
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
- const ciphersViews = (
- await this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId)
- ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
- for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
- this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
- }
-
- const ciphers = await this.getOverlayCipherData();
- this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers });
- await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", {
- isOverlayCiphersPopulated: Boolean(ciphers.length),
- });
- }
-
- /**
- * Strips out unnecessary data from the ciphers and returns an array of
- * objects that contain the cipher data needed for the overlay list.
- */
- private async getOverlayCipherData(): Promise {
- const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
- const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
- const overlayCipherData: OverlayCipherData[] = [];
- let loginCipherIcon: WebsiteIconData;
-
- for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
- const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex];
- if (!loginCipherIcon && cipher.type === CipherType.Login) {
- loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons);
- }
-
- overlayCipherData.push({
- id: overlayCipherId,
- name: cipher.name,
- type: cipher.type,
- reprompt: cipher.reprompt,
- favorite: cipher.favorite,
- icon:
- cipher.type === CipherType.Login
- ? loginCipherIcon
- : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons),
- login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null,
- card: cipher.type === CipherType.Card ? cipher.card.subTitle : null,
- });
- }
-
- return overlayCipherData;
- }
-
- /**
- * Handles aggregation of page details for a tab. Stores the page details
- * in association with the tabId of the tab that sent the message.
- *
- * @param message - Message received from the `collectPageDetailsResponse` command
- * @param sender - The sender of the message
- */
- private storePageDetails(
- message: OverlayBackgroundExtensionMessage,
- sender: chrome.runtime.MessageSender,
- ) {
- const pageDetails = {
- frameId: sender.frameId,
- tab: sender.tab,
- details: message.details,
- };
-
- const pageDetailsMap = this.pageDetailsForTab[sender.tab.id];
- if (!pageDetailsMap) {
- this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]);
- return;
- }
-
- pageDetailsMap.set(sender.frameId, pageDetails);
- }
-
- /**
- * Triggers autofill for the selected cipher in the overlay list. Also places
- * the selected cipher at the top of the list of ciphers.
- *
- * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID.
- * @param sender - The sender of the port message
- */
- private async fillSelectedOverlayListItem(
- { overlayCipherId }: OverlayPortMessage,
- { sender }: chrome.runtime.Port,
- ) {
- const pageDetails = this.pageDetailsForTab[sender.tab.id];
- if (!overlayCipherId || !pageDetails?.size) {
- return;
- }
-
- const cipher = this.overlayLoginCiphers.get(overlayCipherId);
-
- if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) {
- return;
- }
- const totpCode = await this.autofillService.doAutoFill({
- tab: sender.tab,
- cipher: cipher,
- pageDetails: Array.from(pageDetails.values()),
- fillNewPassword: true,
- allowTotpAutofill: true,
- });
-
- if (totpCode) {
- this.platformUtilsService.copyToClipboard(totpCode);
- }
-
- this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]);
- }
-
- /**
- * Checks if the overlay is focused. Will check the overlay list
- * if it is open, otherwise it will check the overlay button.
- */
- private checkOverlayFocused() {
- if (this.overlayListPort) {
- this.checkOverlayListFocused();
-
- return;
- }
-
- this.checkOverlayButtonFocused();
- }
-
- /**
- * Posts a message to the overlay button iframe to check if it is focused.
- */
- private checkOverlayButtonFocused() {
- this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" });
- }
-
- /**
- * Posts a message to the overlay list iframe to check if it is focused.
- */
- private checkOverlayListFocused() {
- this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" });
- }
-
- /**
- * Sends a message to the sender tab to close the autofill overlay.
- *
- * @param sender - The sender of the port message
- * @param forceCloseOverlay - Identifies whether the overlay should be force closed
- */
- private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay });
- }
-
- /**
- * Handles cleanup when an overlay element is closed. Disconnects
- * the list and button ports and sets them to null.
- *
- * @param overlayElement - The overlay element that was closed, either the list or button
- * @param sender - The sender of the port message
- */
- private overlayElementClosed(
- { overlayElement }: OverlayBackgroundExtensionMessage,
- sender: chrome.runtime.MessageSender,
- ) {
- if (sender.tab.id !== this.focusedFieldData?.tabId) {
- this.expiredPorts.forEach((port) => port.disconnect());
- this.expiredPorts = [];
- return;
- }
-
- if (overlayElement === AutofillOverlayElement.Button) {
- this.overlayButtonPort?.disconnect();
- this.overlayButtonPort = null;
-
- return;
- }
-
- this.overlayListPort?.disconnect();
- this.overlayListPort = null;
- }
-
- /**
- * Updates the position of either the overlay list or button. The position
- * is based on the focused field's position and dimensions.
- *
- * @param overlayElement - The overlay element to update, either the list or button
- * @param sender - The sender of the port message
- */
- private updateOverlayPosition(
- { overlayElement }: { overlayElement?: string },
- sender: chrome.runtime.MessageSender,
- ) {
- if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) {
- return;
- }
-
- if (overlayElement === AutofillOverlayElement.Button) {
- this.overlayButtonPort?.postMessage({
- command: "updateIframePosition",
- styles: this.getOverlayButtonPosition(),
- });
-
- return;
- }
-
- this.overlayListPort?.postMessage({
- command: "updateIframePosition",
- styles: this.getOverlayListPosition(),
- });
- }
-
- /**
- * Gets the position of the focused field and calculates the position
- * of the overlay button based on the focused field's position and dimensions.
- */
- private getOverlayButtonPosition() {
- if (!this.focusedFieldData) {
- return;
- }
-
- const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
- const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles;
- let elementOffset = height * 0.37;
- if (height >= 35) {
- elementOffset = height >= 50 ? height * 0.47 : height * 0.42;
- }
-
- const elementHeight = height - elementOffset;
- const elementTopPosition = top + elementOffset / 2;
- let elementLeftPosition = left + width - height + elementOffset / 2;
-
- const fieldPaddingRight = parseInt(paddingRight, 10);
- const fieldPaddingLeft = parseInt(paddingLeft, 10);
- if (fieldPaddingRight > fieldPaddingLeft) {
- elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2);
- }
-
- return {
- top: `${Math.round(elementTopPosition)}px`,
- left: `${Math.round(elementLeftPosition)}px`,
- height: `${Math.round(elementHeight)}px`,
- width: `${Math.round(elementHeight)}px`,
- };
- }
-
- /**
- * Gets the position of the focused field and calculates the position
- * of the overlay list based on the focused field's position and dimensions.
- */
- private getOverlayListPosition() {
- if (!this.focusedFieldData) {
- return;
- }
-
- const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
- return {
- width: `${Math.round(width)}px`,
- top: `${Math.round(top + height)}px`,
- left: `${Math.round(left)}px`,
- };
- }
-
- /**
- * Sets the focused field data to the data passed in the extension message.
- *
- * @param focusedFieldData - Contains the rects and styles of the focused field.
- * @param sender - The sender of the extension message
- */
- private setFocusedFieldData(
- { focusedFieldData }: OverlayBackgroundExtensionMessage,
- sender: chrome.runtime.MessageSender,
- ) {
- this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id };
- }
-
- /**
- * Updates the overlay's visibility based on the display property passed in the extension message.
- *
- * @param display - The display property of the overlay, either "block" or "none"
- */
- private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) {
- if (!display) {
- return;
- }
-
- const portMessage = { command: "updateOverlayHidden", styles: { display } };
-
- this.overlayButtonPort?.postMessage(portMessage);
- this.overlayListPort?.postMessage(portMessage);
- }
-
- /**
- * Sends a message to the currently active tab to open the autofill overlay.
- *
- * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened
- * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states
- */
- private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) {
- const currentTab = await BrowserApi.getTabFromCurrentWindowId();
-
- await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", {
- isFocusingFieldElement,
- isOpeningFullOverlay,
- authStatus: await this.getAuthStatus(),
- });
- }
-
- /**
- * Gets the overlay's visibility setting from the settings service.
- */
- private async getOverlayVisibility(): Promise {
- return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$);
- }
-
- /**
- * Gets the user's authentication status from the auth service. If the user's
- * authentication status has changed, the overlay button's authentication status
- * will be updated and the overlay list's ciphers will be updated.
- */
- private async getAuthStatus() {
- const formerAuthStatus = this.userAuthStatus;
- this.userAuthStatus = await this.authService.getAuthStatus();
-
- if (
- this.userAuthStatus !== formerAuthStatus &&
- this.userAuthStatus === AuthenticationStatus.Unlocked
- ) {
- this.updateOverlayButtonAuthStatus();
- await this.updateOverlayCiphers();
- }
-
- return this.userAuthStatus;
- }
-
- /**
- * Sends a message to the overlay button to update its authentication status.
- */
- private updateOverlayButtonAuthStatus() {
- this.overlayButtonPort?.postMessage({
- command: "updateOverlayButtonAuthStatus",
- authStatus: this.userAuthStatus,
- });
- }
-
- /**
- * Handles the overlay button being clicked. If the user is not authenticated,
- * the vault will be unlocked. If the user is authenticated, the overlay will
- * be opened.
- *
- * @param port - The port of the overlay button
- */
- private handleOverlayButtonClicked(port: chrome.runtime.Port) {
- if (this.userAuthStatus !== AuthenticationStatus.Unlocked) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.unlockVault(port);
- return;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.openOverlay(false, true);
- }
-
- /**
- * Facilitates opening the unlock popout window.
- *
- * @param port - The port of the overlay list
- */
- private async unlockVault(port: chrome.runtime.Port) {
- const { sender } = port;
-
- this.closeOverlay(port);
- const retryMessage: LockedVaultPendingNotificationsData = {
- commandToRetry: { message: { command: "openAutofillOverlay" }, sender },
- target: "overlay.background",
- };
- await BrowserApi.tabSendMessageData(
- sender.tab,
- "addToLockedVaultPendingNotifications",
- retryMessage,
- );
- await this.openUnlockPopout(sender.tab, true);
- }
-
- /**
- * Triggers the opening of a vault item popout window associated
- * with the passed cipher ID.
- * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID.
- * @param sender - The sender of the port message
- */
- private async viewSelectedCipher(
- { overlayCipherId }: OverlayPortMessage,
- { sender }: chrome.runtime.Port,
- ) {
- const cipher = this.overlayLoginCiphers.get(overlayCipherId);
- if (!cipher) {
- return;
- }
-
- await this.openViewVaultItemPopout(sender.tab, {
- cipherId: cipher.id,
- action: SHOW_AUTOFILL_BUTTON,
- });
- }
-
- /**
- * Facilitates redirecting focus to the overlay list.
- */
- private focusOverlayList() {
- this.overlayListPort?.postMessage({ command: "focusOverlayList" });
- }
-
- /**
- * Updates the authentication status for the user and opens the overlay if
- * a followup command is present in the message.
- *
- * @param message - Extension message received from the `unlockCompleted` command
- */
- private async unlockCompleted(message: OverlayBackgroundExtensionMessage) {
- await this.getAuthStatus();
-
- if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") {
- await this.openOverlay(true);
- }
- }
-
- /**
- * Gets the translations for the overlay page.
- */
- private getTranslations() {
- if (!this.overlayPageTranslations) {
- this.overlayPageTranslations = {
- locale: BrowserApi.getUILanguage(),
- opensInANewWindow: this.i18nService.translate("opensInANewWindow"),
- buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"),
- toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"),
- listPageTitle: this.i18nService.translate("bitwardenVault"),
- unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"),
- unlockAccount: this.i18nService.translate("unlockAccount"),
- fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"),
- partialUsername: this.i18nService.translate("partialUsername"),
- view: this.i18nService.translate("view"),
- noItemsToShow: this.i18nService.translate("noItemsToShow"),
- newItem: this.i18nService.translate("newItem"),
- addNewVaultItem: this.i18nService.translate("addNewVaultItem"),
- };
- }
-
- return this.overlayPageTranslations;
- }
-
- /**
- * Facilitates redirecting focus out of one of the
- * overlay elements to elements on the page.
- *
- * @param direction - The direction to redirect focus to (either "next", "previous" or "current)
- * @param sender - The sender of the port message
- */
- private redirectOverlayFocusOut(
- { direction }: OverlayPortMessage,
- { sender }: chrome.runtime.Port,
- ) {
- if (!direction) {
- return;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction });
- }
-
- /**
- * Triggers adding a new vault item from the overlay. Gathers data
- * input by the user before calling to open the add/edit window.
- *
- * @param sender - The sender of the port message
- */
- private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
- void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" });
- }
-
- /**
- * Handles adding a new vault item from the overlay. Gathers data login
- * data captured in the extension message.
- *
- * @param login - The login data captured from the extension message
- * @param sender - The sender of the extension message
- */
- private async addNewVaultItem(
- { login }: OverlayAddNewItemMessage,
- sender: chrome.runtime.MessageSender,
- ) {
- if (!login) {
- return;
- }
-
- const uriView = new LoginUriView();
- uriView.uri = login.uri;
-
- const loginView = new LoginView();
- loginView.uris = [uriView];
- loginView.username = login.username || "";
- loginView.password = login.password || "";
-
- const cipherView = new CipherView();
- cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, "");
- cipherView.folderId = null;
- cipherView.type = CipherType.Login;
- cipherView.login = loginView;
-
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
- await this.cipherService.setAddEditCipherInfo(
- {
- cipher: cipherView,
- collectionIds: cipherView.collectionIds,
- },
- activeUserId,
- );
-
- await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
- }
-
- /**
- * Sets up the extension message listeners for the overlay.
- */
- private setupExtensionMessageListeners() {
- BrowserApi.messageListener("overlay.background", this.handleExtensionMessage);
- BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect);
- }
-
- /**
- * Handles extension messages sent to the extension background.
- *
- * @param message - The message received from the extension
- * @param sender - The sender of the message
- * @param sendResponse - The response to send back to the sender
- */
- private handleExtensionMessage = (
- message: OverlayBackgroundExtensionMessage,
- sender: chrome.runtime.MessageSender,
- sendResponse: (response?: any) => void,
- ) => {
- const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
- if (!handler) {
- return null;
- }
-
- const messageResponse = handler({ message, sender });
- if (typeof messageResponse === "undefined") {
- return null;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- Promise.resolve(messageResponse).then((response) => sendResponse(response));
- return true;
- };
-
- /**
- * Handles the connection of a port to the extension background.
- *
- * @param port - The port that connected to the extension background
- */
- private handlePortOnConnect = async (port: chrome.runtime.Port) => {
- const isOverlayListPort = port.name === AutofillOverlayPort.List;
- const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
- if (!isOverlayListPort && !isOverlayButtonPort) {
- return;
- }
-
- this.storeOverlayPort(port);
- port.onMessage.addListener(this.handleOverlayElementPortMessage);
- port.postMessage({
- command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
- authStatus: await this.getAuthStatus(),
- styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`),
- theme: await firstValueFrom(this.themeStateService.selectedTheme$),
- translations: this.getTranslations(),
- ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
- });
- this.updateOverlayPosition(
- {
- overlayElement: isOverlayListPort
- ? AutofillOverlayElement.List
- : AutofillOverlayElement.Button,
- },
- port.sender,
- );
- };
-
- /**
- * Stores the connected overlay port and sets up any existing ports to be disconnected.
- *
- * @param port - The port to store
-| */
- private storeOverlayPort(port: chrome.runtime.Port) {
- if (port.name === AutofillOverlayPort.List) {
- this.storeExpiredOverlayPort(this.overlayListPort);
- this.overlayListPort = port;
- return;
- }
-
- if (port.name === AutofillOverlayPort.Button) {
- this.storeExpiredOverlayPort(this.overlayButtonPort);
- this.overlayButtonPort = port;
- }
- }
-
- /**
- * When registering a new connection, we want to ensure that the port is disconnected.
- * This method places an existing port in the expiredPorts array to be disconnected
- * at a later time.
- *
- * @param port - The port to store in the expiredPorts array
- */
- private storeExpiredOverlayPort(port: chrome.runtime.Port | null) {
- if (port) {
- this.expiredPorts.push(port);
- }
- }
-
- /**
- * Handles messages sent to the overlay list or button ports.
- *
- * @param message - The message received from the port
- * @param port - The port that sent the message
- */
- private handleOverlayElementPortMessage = (
- message: OverlayBackgroundExtensionMessage,
- port: chrome.runtime.Port,
- ) => {
- const command = message?.command;
- let handler: CallableFunction | undefined;
-
- if (port.name === AutofillOverlayPort.Button) {
- handler = this.overlayButtonPortMessageHandlers[command];
- }
-
- if (port.name === AutofillOverlayPort.List) {
- handler = this.overlayListPortMessageHandlers[command];
- }
-
- if (!handler) {
- return;
- }
-
- handler({ message, port });
- };
-}
-
-export default LegacyOverlayBackground;
diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts
deleted file mode 100644
index ed422822b36..00000000000
--- a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-
-import AutofillScript from "../../../models/autofill-script";
-
-type AutofillExtensionMessage = {
- command: string;
- tab?: chrome.tabs.Tab;
- sender?: string;
- fillScript?: AutofillScript;
- url?: string;
- pageDetailsUrl?: string;
- ciphers?: any;
- data?: {
- authStatus?: AuthenticationStatus;
- isFocusingFieldElement?: boolean;
- isOverlayCiphersPopulated?: boolean;
- direction?: "previous" | "next";
- isOpeningFullOverlay?: boolean;
- forceCloseOverlay?: boolean;
- autofillOverlayVisibility?: number;
- };
-};
-
-type AutofillExtensionMessageParam = { message: AutofillExtensionMessage };
-
-type AutofillExtensionMessageHandlers = {
- [key: string]: CallableFunction;
- collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void;
- collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void;
- fillForm: ({ message }: AutofillExtensionMessageParam) => void;
- openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
- closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
- addNewVaultItemFromOverlay: () => void;
- redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
- updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
- bgUnlockPopoutOpened: () => void;
- bgVaultItemRepromptPopoutOpened: () => void;
- updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void;
-};
-
-export { AutofillExtensionMessage, AutofillExtensionMessageHandlers };
diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts
deleted file mode 100644
index 96d5e85ca34..00000000000
--- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts
+++ /dev/null
@@ -1,604 +0,0 @@
-import { mock } from "jest-mock-extended";
-
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
-
-import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum";
-import AutofillPageDetails from "../../models/autofill-page-details";
-import AutofillScript from "../../models/autofill-script";
-import {
- flushPromises,
- mockQuerySelectorAllDefinedCall,
- sendMockExtensionMessage,
-} from "../../spec/testing-utils";
-import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated";
-
-import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated";
-import AutofillInitDeprecated from "./autofill-init.deprecated";
-
-describe("AutofillInit", () => {
- let autofillInit: AutofillInitDeprecated;
- const autofillOverlayContentService = mock();
- const originalDocumentReadyState = document.readyState;
- const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
-
- beforeEach(() => {
- chrome.runtime.connect = jest.fn().mockReturnValue({
- onDisconnect: {
- addListener: jest.fn(),
- },
- });
- autofillInit = new AutofillInitDeprecated(autofillOverlayContentService);
- window.IntersectionObserver = jest.fn(() => mock());
- });
-
- afterEach(() => {
- jest.resetModules();
- jest.clearAllMocks();
- Object.defineProperty(document, "readyState", {
- value: originalDocumentReadyState,
- writable: true,
- });
- });
-
- afterAll(() => {
- mockQuerySelectorAll.mockRestore();
- });
-
- describe("init", () => {
- it("sets up the extension message listeners", () => {
- jest.spyOn(autofillInit as any, "setupExtensionMessageListeners");
-
- autofillInit.init();
-
- expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled();
- });
-
- it("triggers a collection of page details if the document is in a `complete` ready state", () => {
- jest.useFakeTimers();
- Object.defineProperty(document, "readyState", { value: "complete", writable: true });
-
- autofillInit.init();
- jest.advanceTimersByTime(250);
-
- expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
- {
- command: "bgCollectPageDetails",
- sender: "autofillInit",
- },
- expect.any(Function),
- );
- });
-
- it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => {
- jest.spyOn(window, "addEventListener");
- Object.defineProperty(document, "readyState", { value: "loading", writable: true });
-
- autofillInit.init();
-
- expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function));
- });
- });
-
- describe("setupExtensionMessageListeners", () => {
- it("sets up a chrome runtime on message listener", () => {
- jest.spyOn(chrome.runtime.onMessage, "addListener");
-
- autofillInit["setupExtensionMessageListeners"]();
-
- expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith(
- autofillInit["handleExtensionMessage"],
- );
- });
- });
-
- describe("handleExtensionMessage", () => {
- let message: AutofillExtensionMessage;
- let sender: chrome.runtime.MessageSender;
- const sendResponse = jest.fn();
-
- beforeEach(() => {
- message = {
- command: "collectPageDetails",
- tab: mock(),
- sender: "sender",
- };
- sender = mock();
- });
-
- it("returns a undefined value if a extension message handler is not found with the given message command", () => {
- message.command = "unknownCommand";
-
- const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse);
-
- expect(response).toBe(null);
- });
-
- it("returns a undefined value if the message handler does not return a response", async () => {
- const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse);
- await flushPromises();
-
- expect(response1).not.toBe(false);
-
- message.command = "removeAutofillOverlay";
- message.fillScript = mock();
-
- const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse);
- await flushPromises();
-
- expect(response2).toBe(null);
- });
-
- it("returns a true value and calls sendResponse if the message handler returns a response", async () => {
- message.command = "collectPageDetailsImmediately";
- const pageDetails: AutofillPageDetails = {
- title: "title",
- url: "http://example.com",
- documentUrl: "documentUrl",
- forms: {},
- fields: [],
- collectedTimestamp: 0,
- };
- jest
- .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails")
- .mockResolvedValue(pageDetails);
-
- const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse);
- await flushPromises();
-
- expect(response).toBe(true);
- expect(sendResponse).toHaveBeenCalledWith(pageDetails);
- });
-
- describe("extension message handlers", () => {
- beforeEach(() => {
- autofillInit.init();
- });
-
- describe("collectPageDetails", () => {
- it("sends the collected page details for autofill using a background script message", async () => {
- const pageDetails: AutofillPageDetails = {
- title: "title",
- url: "http://example.com",
- documentUrl: "documentUrl",
- forms: {},
- fields: [],
- collectedTimestamp: 0,
- };
- const message = {
- command: "collectPageDetails",
- sender: "sender",
- tab: mock(),
- };
- jest
- .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails")
- .mockResolvedValue(pageDetails);
-
- sendMockExtensionMessage(message, sender, sendResponse);
- await flushPromises();
-
- expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
- command: "collectPageDetailsResponse",
- tab: message.tab,
- details: pageDetails,
- sender: message.sender,
- });
- });
- });
-
- describe("collectPageDetailsImmediately", () => {
- it("returns collected page details for autofill if set to send the details in the response", async () => {
- const pageDetails: AutofillPageDetails = {
- title: "title",
- url: "http://example.com",
- documentUrl: "documentUrl",
- forms: {},
- fields: [],
- collectedTimestamp: 0,
- };
- jest
- .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails")
- .mockResolvedValue(pageDetails);
-
- sendMockExtensionMessage(
- { command: "collectPageDetailsImmediately" },
- sender,
- sendResponse,
- );
- await flushPromises();
-
- expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled();
- expect(sendResponse).toBeCalledWith(pageDetails);
- expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({
- command: "collectPageDetailsResponse",
- tab: message.tab,
- details: pageDetails,
- sender: message.sender,
- });
- });
- });
-
- describe("fillForm", () => {
- let fillScript: AutofillScript;
- beforeEach(() => {
- fillScript = mock();
- jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation();
- });
-
- it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => {
- const fillScript = mock();
- const message = {
- command: "fillForm",
- fillScript,
- pageDetailsUrl: "https://a-different-url.com",
- };
-
- sendMockExtensionMessage(message);
- await flushPromises();
-
- expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith(
- fillScript,
- );
- });
-
- it("calls the InsertAutofillContentService to fill the form", async () => {
- sendMockExtensionMessage({
- command: "fillForm",
- fillScript,
- pageDetailsUrl: window.location.href,
- });
- await flushPromises();
-
- expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith(
- fillScript,
- );
- });
-
- it("removes the overlay when filling the form", async () => {
- const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay");
- sendMockExtensionMessage({
- command: "fillForm",
- fillScript,
- pageDetailsUrl: window.location.href,
- });
- await flushPromises();
-
- expect(blurAndRemoveOverlaySpy).toHaveBeenCalled();
- });
-
- it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => {
- jest.useFakeTimers();
- jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling");
- jest
- .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField")
- .mockImplementation();
-
- sendMockExtensionMessage({
- command: "fillForm",
- fillScript,
- pageDetailsUrl: window.location.href,
- });
- await flushPromises();
- jest.advanceTimersByTime(300);
-
- expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true);
- expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith(
- fillScript,
- );
- expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false);
- });
-
- it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => {
- jest.useFakeTimers();
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
- jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling");
- jest
- .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm")
- .mockImplementation();
-
- sendMockExtensionMessage({
- command: "fillForm",
- fillScript,
- pageDetailsUrl: window.location.href,
- });
- await flushPromises();
- jest.advanceTimersByTime(300);
-
- expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(
- 1,
- true,
- );
- expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith(
- fillScript,
- );
- expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith(
- 2,
- false,
- );
- });
- });
-
- describe("openAutofillOverlay", () => {
- const message = {
- command: "openAutofillOverlay",
- data: {
- isFocusingFieldElement: true,
- isOpeningFullOverlay: true,
- authStatus: AuthenticationStatus.Unlocked,
- },
- };
-
- it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
-
- sendMockExtensionMessage(message);
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- });
-
- it("opens the autofill overlay", () => {
- sendMockExtensionMessage(message);
-
- expect(
- autofillInit["autofillOverlayContentService"].openAutofillOverlay,
- ).toHaveBeenCalledWith({
- isFocusingFieldElement: message.data.isFocusingFieldElement,
- isOpeningFullOverlay: message.data.isOpeningFullOverlay,
- authStatus: message.data.authStatus,
- });
- });
- });
-
- describe("closeAutofillOverlay", () => {
- beforeEach(() => {
- autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false;
- autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false;
- });
-
- it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
- jest.spyOn(newAutofillInit as any, "removeAutofillOverlay");
-
- sendMockExtensionMessage({
- command: "closeAutofillOverlay",
- data: { forceCloseOverlay: false },
- });
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- });
-
- it("removes the autofill overlay if the message flags a forced closure", () => {
- sendMockExtensionMessage({
- command: "closeAutofillOverlay",
- data: { forceCloseOverlay: true },
- });
-
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
- ).toHaveBeenCalled();
- });
-
- it("ignores the message if a field is currently focused", () => {
- autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true;
-
- sendMockExtensionMessage({ command: "closeAutofillOverlay" });
-
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlayList,
- ).not.toHaveBeenCalled();
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
- ).not.toHaveBeenCalled();
- });
-
- it("removes the autofill overlay list if the overlay is currently filling", () => {
- autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true;
-
- sendMockExtensionMessage({ command: "closeAutofillOverlay" });
-
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlayList,
- ).toHaveBeenCalled();
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
- ).not.toHaveBeenCalled();
- });
-
- it("removes the entire overlay if the overlay is not currently filling", () => {
- sendMockExtensionMessage({ command: "closeAutofillOverlay" });
-
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlayList,
- ).not.toHaveBeenCalled();
- expect(
- autofillInit["autofillOverlayContentService"].removeAutofillOverlay,
- ).toHaveBeenCalled();
- });
- });
-
- describe("addNewVaultItemFromOverlay", () => {
- it("will not add a new vault item if the autofillOverlayContentService is not present", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
-
- sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- });
-
- it("will add a new vault item", () => {
- sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
-
- expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled();
- });
- });
-
- describe("redirectOverlayFocusOut", () => {
- const message = {
- command: "redirectOverlayFocusOut",
- data: {
- direction: RedirectFocusDirection.Next,
- },
- };
-
- it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
-
- sendMockExtensionMessage(message);
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- });
-
- it("redirects the overlay focus", () => {
- sendMockExtensionMessage(message);
-
- expect(
- autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut,
- ).toHaveBeenCalledWith(message.data.direction);
- });
- });
-
- describe("updateIsOverlayCiphersPopulated", () => {
- const message = {
- command: "updateIsOverlayCiphersPopulated",
- data: {
- isOverlayCiphersPopulated: true,
- },
- };
-
- it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
-
- sendMockExtensionMessage(message);
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- });
-
- it("updates whether the overlay ciphers are populated", () => {
- sendMockExtensionMessage(message);
-
- expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual(
- message.data.isOverlayCiphersPopulated,
- );
- });
- });
-
- describe("bgUnlockPopoutOpened", () => {
- it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
- jest.spyOn(newAutofillInit as any, "removeAutofillOverlay");
-
- sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" });
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled();
- });
-
- it("blurs the most recently focused feel and remove the autofill overlay", () => {
- jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField");
- jest.spyOn(autofillInit as any, "removeAutofillOverlay");
-
- sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" });
-
- expect(
- autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField,
- ).toHaveBeenCalled();
- expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
- });
- });
-
- describe("bgVaultItemRepromptPopoutOpened", () => {
- it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => {
- const newAutofillInit = new AutofillInitDeprecated(undefined);
- newAutofillInit.init();
- jest.spyOn(newAutofillInit as any, "removeAutofillOverlay");
-
- sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" });
-
- expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
- expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled();
- });
-
- it("blurs the most recently focused feel and remove the autofill overlay", () => {
- jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField");
- jest.spyOn(autofillInit as any, "removeAutofillOverlay");
-
- sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" });
-
- expect(
- autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField,
- ).toHaveBeenCalled();
- expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
- });
- });
-
- describe("updateAutofillOverlayVisibility", () => {
- beforeEach(() => {
- autofillInit["autofillOverlayContentService"].autofillOverlayVisibility =
- AutofillOverlayVisibility.OnButtonClick;
- });
-
- it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => {
- sendMockExtensionMessage({
- command: "updateAutofillOverlayVisibility",
- data: {},
- });
-
- expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
- AutofillOverlayVisibility.OnButtonClick,
- );
- });
-
- it("updates the overlay visibility value", () => {
- const message = {
- command: "updateAutofillOverlayVisibility",
- data: {
- autofillOverlayVisibility: AutofillOverlayVisibility.Off,
- },
- };
-
- sendMockExtensionMessage(message);
-
- expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
- message.data.autofillOverlayVisibility,
- );
- });
- });
- });
- });
-
- describe("destroy", () => {
- it("clears the timeout used to collect page details on load", () => {
- jest.spyOn(window, "clearTimeout");
-
- autofillInit.init();
- autofillInit.destroy();
-
- expect(window.clearTimeout).toHaveBeenCalledWith(
- autofillInit["collectPageDetailsOnLoadTimeout"],
- );
- });
-
- it("removes the extension message listeners", () => {
- autofillInit.destroy();
-
- expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith(
- autofillInit["handleExtensionMessage"],
- );
- });
-
- it("destroys the collectAutofillContentService", () => {
- jest.spyOn(autofillInit["collectAutofillContentService"], "destroy");
-
- autofillInit.destroy();
-
- expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled();
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts
deleted file mode 100644
index fac9c0852f5..00000000000
--- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts
+++ /dev/null
@@ -1,315 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { AutofillInit } from "../../content/abstractions/autofill-init";
-import AutofillPageDetails from "../../models/autofill-page-details";
-import { CollectAutofillContentService } from "../../services/collect-autofill-content.service";
-import DomElementVisibilityService from "../../services/dom-element-visibility.service";
-import { DomQueryService } from "../../services/dom-query.service";
-import InsertAutofillContentService from "../../services/insert-autofill-content.service";
-import { sendExtensionMessage } from "../../utils";
-import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
-
-import {
- AutofillExtensionMessage,
- AutofillExtensionMessageHandlers,
-} from "./abstractions/autofill-init.deprecated";
-
-class LegacyAutofillInit implements AutofillInit {
- private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined;
- private readonly domElementVisibilityService: DomElementVisibilityService;
- private readonly collectAutofillContentService: CollectAutofillContentService;
- private readonly insertAutofillContentService: InsertAutofillContentService;
- private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined;
- private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = {
- collectPageDetails: ({ message }) => this.collectPageDetails(message),
- collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
- fillForm: ({ message }) => this.fillForm(message),
- openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message),
- closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message),
- addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(),
- redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message),
- updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
- bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(),
- bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(),
- updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message),
- };
-
- /**
- * AutofillInit constructor. Initializes the DomElementVisibilityService,
- * CollectAutofillContentService and InsertAutofillContentService classes.
- *
- * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
- */
- constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) {
- this.autofillOverlayContentService = autofillOverlayContentService;
- this.domElementVisibilityService = new DomElementVisibilityService();
- const domQueryService = new DomQueryService();
- this.collectAutofillContentService = new CollectAutofillContentService(
- this.domElementVisibilityService,
- domQueryService,
- this.autofillOverlayContentService,
- );
- this.insertAutofillContentService = new InsertAutofillContentService(
- this.domElementVisibilityService,
- this.collectAutofillContentService,
- );
- }
-
- /**
- * Initializes the autofill content script, setting up
- * the extension message listeners. This method should
- * be called once when the content script is loaded.
- */
- init() {
- this.setupExtensionMessageListeners();
- this.autofillOverlayContentService?.init();
- this.collectPageDetailsOnLoad();
- }
-
- /**
- * Triggers a collection of the page details from the
- * background script, ensuring that autofill is ready
- * to act on the page.
- */
- private collectPageDetailsOnLoad() {
- const sendCollectDetailsMessage = () => {
- this.clearCollectPageDetailsOnLoadTimeout();
- this.collectPageDetailsOnLoadTimeout = setTimeout(
- () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }),
- 250,
- );
- };
-
- if (globalThis.document.readyState === "complete") {
- sendCollectDetailsMessage();
- }
-
- globalThis.addEventListener("load", sendCollectDetailsMessage);
- }
-
- /**
- * Collects the page details and sends them to the
- * extension background script. If the `sendDetailsInResponse`
- * parameter is set to true, the page details will be
- * returned to facilitate sending the details in the
- * response to the extension message.
- *
- * @param message - The extension message.
- * @param sendDetailsInResponse - Determines whether to send the details in the response.
- */
- private async collectPageDetails(
- message: AutofillExtensionMessage,
- sendDetailsInResponse = false,
- ): Promise {
- const pageDetails: AutofillPageDetails =
- await this.collectAutofillContentService.getPageDetails();
- if (sendDetailsInResponse) {
- return pageDetails;
- }
-
- void chrome.runtime.sendMessage({
- command: "collectPageDetailsResponse",
- tab: message.tab,
- details: pageDetails,
- sender: message.sender,
- });
- }
-
- /**
- * Fills the form with the given fill script.
- *
- * @param {AutofillExtensionMessage} message
- */
- private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) {
- if ((document.defaultView || window).location.href !== pageDetailsUrl) {
- return;
- }
-
- this.blurAndRemoveOverlay();
- this.updateOverlayIsCurrentlyFilling(true);
- await this.insertAutofillContentService.fillForm(fillScript);
-
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250);
- }
-
- /**
- * Handles updating the overlay is currently filling value.
- *
- * @param isCurrentlyFilling - Indicates if the overlay is currently filling
- */
- private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) {
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling;
- }
-
- /**
- * Opens the autofill overlay.
- *
- * @param data - The extension message data.
- */
- private openAutofillOverlay({ data }: AutofillExtensionMessage) {
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- this.autofillOverlayContentService.openAutofillOverlay(data);
- }
-
- /**
- * Blurs the most recent overlay field and removes the overlay. Used
- * in cases where the background unlock or vault item reprompt popout
- * is opened.
- */
- private blurAndRemoveOverlay() {
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- this.autofillOverlayContentService.blurMostRecentOverlayField();
- this.removeAutofillOverlay();
- }
-
- /**
- * Removes the autofill overlay if the field is not currently focused.
- * If the autofill is currently filling, only the overlay list will be
- * removed.
- */
- private removeAutofillOverlay(message?: AutofillExtensionMessage) {
- if (message?.data?.forceCloseOverlay) {
- this.autofillOverlayContentService?.removeAutofillOverlay();
- return;
- }
-
- if (
- !this.autofillOverlayContentService ||
- this.autofillOverlayContentService.isFieldCurrentlyFocused
- ) {
- return;
- }
-
- if (this.autofillOverlayContentService.isCurrentlyFilling) {
- this.autofillOverlayContentService.removeAutofillOverlayList();
- return;
- }
-
- this.autofillOverlayContentService.removeAutofillOverlay();
- }
-
- /**
- * Adds a new vault item from the overlay.
- */
- private addNewVaultItemFromOverlay() {
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- this.autofillOverlayContentService.addNewVaultItem();
- }
-
- /**
- * Redirects the overlay focus out of an overlay iframe.
- *
- * @param data - Contains the direction to redirect the focus.
- */
- private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) {
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction);
- }
-
- /**
- * Updates whether the current tab has ciphers that can populate the overlay list
- *
- * @param data - Contains the isOverlayCiphersPopulated value
- *
- */
- private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) {
- if (!this.autofillOverlayContentService) {
- return;
- }
-
- this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean(
- data?.isOverlayCiphersPopulated,
- );
- }
-
- /**
- * Updates the autofill overlay visibility.
- *
- * @param data - Contains the autoFillOverlayVisibility value
- */
- private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) {
- if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) {
- return;
- }
-
- this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility;
- }
-
- /**
- * Clears the send collect details message timeout.
- */
- private clearCollectPageDetailsOnLoadTimeout() {
- if (this.collectPageDetailsOnLoadTimeout) {
- clearTimeout(this.collectPageDetailsOnLoadTimeout);
- }
- }
-
- /**
- * Sets up the extension message listeners for the content script.
- */
- private setupExtensionMessageListeners() {
- chrome.runtime.onMessage.addListener(this.handleExtensionMessage);
- }
-
- /**
- * Handles the extension messages sent to the content script.
- *
- * @param message - The extension message.
- * @param sender - The message sender.
- * @param sendResponse - The send response callback.
- */
- private handleExtensionMessage = (
- message: AutofillExtensionMessage,
- sender: chrome.runtime.MessageSender,
- sendResponse: (response?: any) => void,
- ): boolean => {
- const command: string = message.command;
- const handler: CallableFunction | undefined = this.extensionMessageHandlers[command];
- if (!handler) {
- return null;
- }
-
- const messageResponse = handler({ message, sender });
- if (typeof messageResponse === "undefined") {
- return null;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- Promise.resolve(messageResponse).then((response) => sendResponse(response));
- return true;
- };
-
- /**
- * Handles destroying the autofill init content script. Removes all
- * listeners, timeouts, and object instances to prevent memory leaks.
- */
- destroy() {
- this.clearCollectPageDetailsOnLoadTimeout();
- chrome.runtime.onMessage.removeListener(this.handleExtensionMessage);
- this.collectAutofillContentService.destroy();
- this.autofillOverlayContentService?.destroy();
- }
-}
-
-export default LegacyAutofillInit;
diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts
deleted file mode 100644
index 66d672172ae..00000000000
--- a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { setupAutofillInitDisconnectAction } from "../../utils";
-import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated";
-
-import LegacyAutofillInit from "./autofill-init.deprecated";
-
-(function (windowContext) {
- if (!windowContext.bitwardenAutofillInit) {
- const autofillOverlayContentService = new LegacyAutofillOverlayContentService();
- windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService);
- setupAutofillInitDisconnectAction(windowContext);
-
- windowContext.bitwardenAutofillInit.init();
- }
-})(window);
diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts
deleted file mode 100644
index b6b22be9439..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-
-type OverlayButtonMessage = { command: string; colorScheme?: string };
-
-type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: AuthenticationStatus };
-
-type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & {
- styleSheetUrl: string;
- translations: Record;
-};
-
-type OverlayButtonWindowMessageHandlers = {
- [key: string]: CallableFunction;
- initAutofillOverlayButton: ({ message }: { message: InitAutofillOverlayButtonMessage }) => void;
- checkAutofillOverlayButtonFocused: () => void;
- updateAutofillOverlayButtonAuthStatus: ({
- message,
- }: {
- message: UpdateAuthStatusMessage;
- }) => void;
- updateOverlayPageColorScheme: ({ message }: { message: OverlayButtonMessage }) => void;
-};
-
-export {
- UpdateAuthStatusMessage,
- OverlayButtonMessage,
- InitAutofillOverlayButtonMessage,
- OverlayButtonWindowMessageHandlers,
-};
diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts
deleted file mode 100644
index 0c4160a0709..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-type AutofillOverlayIframeExtensionMessage = {
- command: string;
- styles?: Partial;
- theme?: string;
-};
-
-type AutofillOverlayIframeWindowMessageHandlers = {
- [key: string]: CallableFunction;
- updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void;
- getPageColorScheme: () => void;
-};
-
-type AutofillOverlayIframeExtensionMessageParam = {
- message: AutofillOverlayIframeExtensionMessage;
-};
-
-type BackgroundPortMessageHandlers = {
- [key: string]: CallableFunction;
- initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
- updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
- updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
-};
-
-interface AutofillOverlayIframeService {
- initOverlayIframe(initStyles: Partial, ariaAlert?: string): void;
-}
-
-export {
- AutofillOverlayIframeExtensionMessage,
- AutofillOverlayIframeWindowMessageHandlers,
- BackgroundPortMessageHandlers,
- AutofillOverlayIframeService,
-};
diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts
deleted file mode 100644
index 83578b13043..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-
-import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated";
-
-type OverlayListMessage = { command: string };
-
-type UpdateOverlayListCiphersMessage = OverlayListMessage & {
- ciphers: OverlayCipherData[];
-};
-
-type InitAutofillOverlayListMessage = OverlayListMessage & {
- authStatus: AuthenticationStatus;
- styleSheetUrl: string;
- theme: string;
- translations: Record;
- ciphers?: OverlayCipherData[];
-};
-
-type OverlayListWindowMessageHandlers = {
- [key: string]: CallableFunction;
- initAutofillOverlayList: ({ message }: { message: InitAutofillOverlayListMessage }) => void;
- checkAutofillOverlayListFocused: () => void;
- updateOverlayListCiphers: ({ message }: { message: UpdateOverlayListCiphersMessage }) => void;
- focusOverlayList: () => void;
-};
-
-export {
- UpdateOverlayListCiphersMessage,
- InitAutofillOverlayListMessage,
- OverlayListWindowMessageHandlers,
-};
diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts
deleted file mode 100644
index 368ae4e7303..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated";
-import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated";
-
-type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers;
-
-type AutofillOverlayPageElementWindowMessage = {
- [key: string]: any;
- command: string;
- overlayCipherId?: string;
- height?: number;
-};
-
-export { WindowMessageHandlers, AutofillOverlayPageElementWindowMessage };
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap
deleted file mode 100644
index 132bd968899..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap
+++ /dev/null
@@ -1,23 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AutofillOverlayIframeService initOverlayIframe creates an aria alert element if the ariaAlert param is passed 1`] = `
-
- aria alert
-
-`;
-
-exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's attributes 1`] = `
-
-`;
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts
deleted file mode 100644
index 4652bb832d1..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import AutofillOverlayButtonIframe from "./autofill-overlay-button-iframe.deprecated";
-
-describe("AutofillOverlayButtonIframe", () => {
- window.customElements.define(
- "autofill-overlay-button-iframe",
- class extends HTMLElement {
- constructor() {
- super();
- new AutofillOverlayButtonIframe(this);
- }
- },
- );
-
- afterAll(() => {
- jest.clearAllMocks();
- });
-
- it("creates a custom element that is an instance of the AutofillIframeElement parent class", () => {
- document.body.innerHTML = "";
-
- const iframe = document.querySelector("autofill-overlay-button-iframe");
-
- expect(iframe).toBeInstanceOf(HTMLElement);
- expect(iframe.shadowRoot).toBeDefined();
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts
deleted file mode 100644
index 71b6235a895..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { AutofillOverlayPort } from "../../../enums/autofill-overlay.enum";
-
-import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element.deprecated";
-
-class AutofillOverlayButtonIframe extends AutofillOverlayIframeElement {
- constructor(element: HTMLElement) {
- super(
- element,
- "overlay/button.html",
- AutofillOverlayPort.Button,
- {
- background: "transparent",
- border: "none",
- },
- chrome.i18n.getMessage("bitwardenOverlayButton"),
- chrome.i18n.getMessage("bitwardenOverlayMenuAvailable"),
- );
- }
-}
-
-export default AutofillOverlayButtonIframe;
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts
deleted file mode 100644
index fd4347e4b06..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element.deprecated";
-import AutofillOverlayIframeService from "./autofill-overlay-iframe.service.deprecated";
-
-jest.mock("./autofill-overlay-iframe.service.deprecated");
-
-describe("AutofillOverlayIframeElement", () => {
- window.customElements.define(
- "autofill-overlay-iframe",
- class extends HTMLElement {
- constructor() {
- super();
- new AutofillOverlayIframeElement(
- this,
- "overlay/button.html",
- "overlay/button",
- { background: "transparent", border: "none" },
- "bitwardenOverlayButton",
- );
- }
- },
- );
-
- afterAll(() => {
- jest.clearAllMocks();
- });
-
- it("creates a custom element that is an instance of the HTMLElement parent class", () => {
- document.body.innerHTML = "";
-
- const iframe = document.querySelector("autofill-overlay-iframe");
-
- expect(iframe).toBeInstanceOf(HTMLElement);
- });
-
- it("attaches a closed shadow DOM", () => {
- document.body.innerHTML = "";
-
- const iframe = document.querySelector("autofill-overlay-iframe");
-
- expect(iframe.shadowRoot).toBeNull();
- });
-
- it("instantiates the autofill overlay iframe service for each attached custom element", () => {
- expect(AutofillOverlayIframeService).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts
deleted file mode 100644
index 90049440cca..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import AutofillOverlayIframeService from "./autofill-overlay-iframe.service.deprecated";
-
-class AutofillOverlayIframeElement {
- constructor(
- element: HTMLElement,
- iframePath: string,
- portName: string,
- initStyles: Partial,
- iframeTitle: string,
- ariaAlert?: string,
- ) {
- const shadow: ShadowRoot = element.attachShadow({ mode: "closed" });
- const autofillOverlayIframeService = new AutofillOverlayIframeService(
- iframePath,
- portName,
- shadow,
- );
- autofillOverlayIframeService.initOverlayIframe(initStyles, iframeTitle, ariaAlert);
- }
-}
-
-export default AutofillOverlayIframeElement;
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts
deleted file mode 100644
index e79cba71763..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts
+++ /dev/null
@@ -1,521 +0,0 @@
-import { mock } from "jest-mock-extended";
-
-import { EVENTS } from "@bitwarden/common/autofill/constants";
-import { ThemeType } from "@bitwarden/common/platform/enums";
-
-import { AutofillOverlayPort } from "../../../enums/autofill-overlay.enum";
-import { createPortSpyMock } from "../../../spec/autofill-mocks";
-import {
- flushPromises,
- sendPortMessage,
- triggerPortOnDisconnectEvent,
-} from "../../../spec/testing-utils";
-
-import AutofillOverlayIframeService from "./autofill-overlay-iframe.service.deprecated";
-
-describe("AutofillOverlayIframeService", () => {
- const iframePath = "overlay/legacy-list.html";
- let autofillOverlayIframeService: AutofillOverlayIframeService;
- let portSpy: chrome.runtime.Port;
- let shadowAppendSpy: jest.SpyInstance;
- let handlePortDisconnectSpy: jest.SpyInstance;
- let handlePortMessageSpy: jest.SpyInstance;
- let handleWindowMessageSpy: jest.SpyInstance;
-
- beforeEach(() => {
- const shadow = document.createElement("div").attachShadow({ mode: "open" });
- autofillOverlayIframeService = new AutofillOverlayIframeService(
- iframePath,
- AutofillOverlayPort.Button,
- shadow,
- );
- shadowAppendSpy = jest.spyOn(shadow, "appendChild");
- handlePortDisconnectSpy = jest.spyOn(
- autofillOverlayIframeService as any,
- "handlePortDisconnect",
- );
- handlePortMessageSpy = jest.spyOn(autofillOverlayIframeService as any, "handlePortMessage");
- handleWindowMessageSpy = jest.spyOn(autofillOverlayIframeService as any, "handleWindowMessage");
- chrome.runtime.connect = jest.fn((connectInfo: chrome.runtime.ConnectInfo) =>
- createPortSpyMock(connectInfo.name),
- ) as unknown as typeof chrome.runtime.connect;
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("initOverlayIframe", () => {
- it("sets up the iframe's attributes", () => {
- autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title");
-
- expect(autofillOverlayIframeService["iframe"]).toMatchSnapshot();
- });
-
- it("appends the iframe to the shadowDom", () => {
- jest.spyOn(autofillOverlayIframeService["shadow"], "appendChild");
-
- autofillOverlayIframeService.initOverlayIframe({}, "title");
-
- expect(autofillOverlayIframeService["shadow"].appendChild).toBeCalledWith(
- autofillOverlayIframeService["iframe"],
- );
- });
-
- it("creates an aria alert element if the ariaAlert param is passed", () => {
- const ariaAlert = "aria alert";
- jest.spyOn(autofillOverlayIframeService as any, "createAriaAlertElement");
-
- autofillOverlayIframeService.initOverlayIframe({}, "title", ariaAlert);
-
- expect(autofillOverlayIframeService["createAriaAlertElement"]).toBeCalledWith(ariaAlert);
- expect(autofillOverlayIframeService["ariaAlertElement"]).toMatchSnapshot();
- });
-
- describe("on load of the iframe source", () => {
- beforeEach(() => {
- autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title", "ariaAlert");
- });
-
- it("sets up and connects the port message listener to the extension background", () => {
- jest.spyOn(globalThis, "addEventListener");
-
- autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
- portSpy = autofillOverlayIframeService["port"];
-
- expect(chrome.runtime.connect).toBeCalledWith({ name: AutofillOverlayPort.Button });
- expect(portSpy.onDisconnect.addListener).toBeCalledWith(handlePortDisconnectSpy);
- expect(portSpy.onMessage.addListener).toBeCalledWith(handlePortMessageSpy);
- expect(globalThis.addEventListener).toBeCalledWith(EVENTS.MESSAGE, handleWindowMessageSpy);
- });
-
- it("skips announcing the aria alert if the aria alert element is not populated", () => {
- jest.spyOn(globalThis, "setTimeout");
- autofillOverlayIframeService["ariaAlertElement"] = undefined;
-
- autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
-
- expect(globalThis.setTimeout).not.toBeCalled();
- });
-
- it("announces the aria alert if the aria alert element is populated", () => {
- jest.useFakeTimers();
- jest.spyOn(globalThis, "setTimeout");
- autofillOverlayIframeService["ariaAlertElement"] = document.createElement("div");
- autofillOverlayIframeService["ariaAlertTimeout"] = setTimeout(jest.fn(), 2000);
-
- autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
-
- expect(globalThis.setTimeout).toBeCalled();
- jest.advanceTimersByTime(2000);
-
- expect(shadowAppendSpy).toBeCalledWith(autofillOverlayIframeService["ariaAlertElement"]);
- });
- });
- });
-
- describe("event listeners", () => {
- beforeEach(() => {
- autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title", "ariaAlert");
- autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
- Object.defineProperty(autofillOverlayIframeService["iframe"], "contentWindow", {
- value: {
- postMessage: jest.fn(),
- },
- writable: true,
- });
- jest.spyOn(autofillOverlayIframeService["iframe"].contentWindow, "postMessage");
- portSpy = autofillOverlayIframeService["port"];
- });
-
- describe("handlePortDisconnect", () => {
- it("ignores ports that do not have the correct port name", () => {
- portSpy.name = "wrong-port-name";
- triggerPortOnDisconnectEvent(portSpy);
-
- expect(autofillOverlayIframeService["port"]).not.toBeNull();
- });
-
- it("resets the iframe element's opacity, height, and display styles", () => {
- triggerPortOnDisconnectEvent(portSpy);
-
- expect(autofillOverlayIframeService["iframe"].style.opacity).toBe("0");
- expect(autofillOverlayIframeService["iframe"].style.height).toBe("0px");
- expect(autofillOverlayIframeService["iframe"].style.display).toBe("block");
- });
-
- it("removes the global message listener", () => {
- jest.spyOn(globalThis, "removeEventListener");
-
- triggerPortOnDisconnectEvent(portSpy);
-
- expect(globalThis.removeEventListener).toBeCalledWith(
- EVENTS.MESSAGE,
- handleWindowMessageSpy,
- );
- });
-
- it("removes the port's onMessage listener", () => {
- triggerPortOnDisconnectEvent(portSpy);
-
- expect(portSpy.onMessage.removeListener).toBeCalledWith(handlePortMessageSpy);
- });
-
- it("removes the port's onDisconnect listener", () => {
- triggerPortOnDisconnectEvent(portSpy);
-
- expect(portSpy.onDisconnect.removeListener).toBeCalledWith(handlePortDisconnectSpy);
- });
-
- it("disconnects the port", () => {
- triggerPortOnDisconnectEvent(portSpy);
-
- expect(portSpy.disconnect).toBeCalled();
- expect(autofillOverlayIframeService["port"]).toBeNull();
- });
- });
-
- describe("handlePortMessage", () => {
- it("ignores port messages that do not correlate to the correct port name", () => {
- portSpy.name = "wrong-port-name";
- sendPortMessage(portSpy, {});
-
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).not.toBeCalled();
- });
-
- it("passes on the message to the iframe if the message is not registered with the message handlers", () => {
- const message = { command: "unregisteredMessage" };
-
- sendPortMessage(portSpy, message);
-
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
- message,
- "*",
- );
- });
-
- it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
- jest.spyOn(autofillOverlayIframeService as any, "updateIframePosition");
-
- sendPortMessage(portSpy, { command: "updateIframePosition" });
-
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).not.toBeCalled();
- });
-
- describe("initializing the overlay list", () => {
- let updateElementStylesSpy: jest.SpyInstance;
-
- beforeEach(() => {
- updateElementStylesSpy = jest.spyOn(
- autofillOverlayIframeService as any,
- "updateElementStyles",
- );
- });
-
- it("passes the message on to the iframe element", () => {
- const message = {
- command: "initAutofillOverlayList",
- theme: ThemeType.Light,
- };
-
- sendPortMessage(portSpy, message);
-
- expect(updateElementStylesSpy).not.toBeCalled();
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
- message,
- "*",
- );
- });
-
- it("sets a light theme based on the user's system preferences", () => {
- window.matchMedia = jest.fn(() => mock({ matches: false }));
- const message = {
- command: "initAutofillOverlayList",
- theme: ThemeType.System,
- };
-
- sendPortMessage(portSpy, message);
-
- expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
- {
- command: "initAutofillOverlayList",
- theme: ThemeType.Light,
- },
- "*",
- );
- });
-
- it("sets a dark theme based on the user's system preferences", () => {
- window.matchMedia = jest.fn(() => mock({ matches: true }));
- const message = {
- command: "initAutofillOverlayList",
- theme: ThemeType.System,
- };
-
- sendPortMessage(portSpy, message);
-
- expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
- {
- command: "initAutofillOverlayList",
- theme: ThemeType.Dark,
- },
- "*",
- );
- });
-
- it("updates the border to match the `dark` theme", () => {
- const message = {
- command: "initAutofillOverlayList",
- theme: ThemeType.Dark,
- };
-
- sendPortMessage(portSpy, message);
-
- expect(updateElementStylesSpy).toBeCalledWith(autofillOverlayIframeService["iframe"], {
- borderColor: "#4c525f",
- });
- });
- });
-
- describe("updating the iframe's position", () => {
- beforeEach(() => {
- jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
- });
-
- it("ignores updating the iframe position if the document does not have focus", () => {
- jest.spyOn(autofillOverlayIframeService as any, "updateElementStyles");
- jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
-
- sendPortMessage(portSpy, {
- command: "updateIframePosition",
- styles: { top: 100, left: 100 },
- });
-
- expect(autofillOverlayIframeService["updateElementStyles"]).not.toBeCalled();
- });
-
- it("updates the iframe position if the document has focus", () => {
- const styles = { top: "100px", left: "100px" };
-
- sendPortMessage(portSpy, {
- command: "updateIframePosition",
- styles,
- });
-
- expect(autofillOverlayIframeService["iframe"].style.top).toBe(styles.top);
- expect(autofillOverlayIframeService["iframe"].style.left).toBe(styles.left);
- });
-
- it("fades the iframe element in after positioning the element", () => {
- jest.useFakeTimers();
- const styles = { top: "100px", left: "100px" };
-
- sendPortMessage(portSpy, {
- command: "updateIframePosition",
- styles,
- });
-
- expect(autofillOverlayIframeService["iframe"].style.opacity).toBe("0");
- jest.advanceTimersByTime(10);
- expect(autofillOverlayIframeService["iframe"].style.opacity).toBe("1");
- });
-
- it("announces the opening of the iframe using an aria alert", () => {
- jest.useFakeTimers();
- const styles = { top: "100px", left: "100px" };
-
- sendPortMessage(portSpy, {
- command: "updateIframePosition",
- styles,
- });
-
- jest.advanceTimersByTime(2000);
- expect(shadowAppendSpy).toBeCalledWith(autofillOverlayIframeService["ariaAlertElement"]);
- });
- });
-
- it("updates the visibility of the iframe", () => {
- sendPortMessage(portSpy, {
- command: "updateOverlayHidden",
- styles: { display: "none" },
- });
-
- expect(autofillOverlayIframeService["iframe"].style.display).toBe("none");
- });
- });
-
- describe("handleWindowMessage", () => {
- it("ignores window messages when the port is not set", () => {
- autofillOverlayIframeService["port"] = null;
-
- globalThis.dispatchEvent(new MessageEvent("message", { data: {} }));
-
- expect(autofillOverlayIframeService["port"]).toBeNull();
- });
-
- it("ignores window messages whose source is not the iframe's content window", () => {
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: {},
- source: window,
- }),
- );
-
- expect(portSpy.postMessage).not.toBeCalled();
- });
-
- it("ignores window messages whose origin is not from the extension origin", () => {
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: {},
- source: autofillOverlayIframeService["iframe"].contentWindow,
- origin: "https://www.google.com",
- }),
- );
-
- expect(portSpy.postMessage).not.toBeCalled();
- });
-
- it("passes the window message from an iframe element to the background port", () => {
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: { command: "not-a-handled-command" },
- source: autofillOverlayIframeService["iframe"].contentWindow,
- origin: "chrome-extension://id",
- }),
- );
-
- expect(portSpy.postMessage).toBeCalledWith({ command: "not-a-handled-command" });
- });
-
- it("updates the overlay list height", () => {
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: { command: "updateAutofillOverlayListHeight", styles: { height: "300px" } },
- source: autofillOverlayIframeService["iframe"].contentWindow,
- origin: "chrome-extension://id",
- }),
- );
-
- expect(autofillOverlayIframeService["iframe"].style.height).toBe("300px");
- });
-
- describe("getPageColorScheme window message", () => {
- afterEach(() => {
- globalThis.document.head.innerHTML = "";
- });
-
- it("gets and updates the overlay page color scheme", () => {
- const colorSchemeMetaTag = globalThis.document.createElement("meta");
- colorSchemeMetaTag.setAttribute("name", "color-scheme");
- colorSchemeMetaTag.setAttribute("content", "dark");
- globalThis.document.head.append(colorSchemeMetaTag);
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: { command: "getPageColorScheme" },
- source: autofillOverlayIframeService["iframe"].contentWindow,
- origin: "chrome-extension://id",
- }),
- );
-
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
- { command: "updateOverlayPageColorScheme", colorScheme: "dark" },
- "*",
- );
- });
-
- it("sends a normal color scheme if the color scheme meta tag is not present", () => {
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: { command: "getPageColorScheme" },
- source: autofillOverlayIframeService["iframe"].contentWindow,
- origin: "chrome-extension://id",
- }),
- );
-
- expect(autofillOverlayIframeService["iframe"].contentWindow.postMessage).toBeCalledWith(
- { command: "updateOverlayPageColorScheme", colorScheme: "normal" },
- "*",
- );
- });
- });
- });
- });
-
- describe("mutation observer", () => {
- beforeEach(() => {
- autofillOverlayIframeService.initOverlayIframe({ height: "0px" }, "title", "ariaAlert");
- autofillOverlayIframeService["iframe"].dispatchEvent(new Event(EVENTS.LOAD));
- portSpy = autofillOverlayIframeService["port"];
- });
-
- it("skips handling found mutations if excessive mutations are triggering", async () => {
- jest.useFakeTimers();
- jest
- .spyOn(
- autofillOverlayIframeService as any,
- "isTriggeringExcessiveMutationObserverIterations",
- )
- .mockReturnValue(true);
- jest.spyOn(autofillOverlayIframeService as any, "updateElementStyles");
-
- autofillOverlayIframeService["iframe"].style.visibility = "hidden";
- await flushPromises();
-
- expect(autofillOverlayIframeService["updateElementStyles"]).not.toBeCalled();
- });
-
- it("reverts any styles changes made directly to the iframe", async () => {
- jest.useFakeTimers();
-
- autofillOverlayIframeService["iframe"].style.visibility = "hidden";
- await flushPromises();
-
- expect(autofillOverlayIframeService["iframe"].style.visibility).toBe("visible");
- });
-
- it("force closes the autofill overlay if more than 9 foreign mutations are triggered", async () => {
- jest.useFakeTimers();
- autofillOverlayIframeService["foreignMutationsCount"] = 10;
-
- autofillOverlayIframeService["iframe"].src = "http://malicious-site.com";
- await flushPromises();
-
- expect(portSpy.postMessage).toBeCalledWith({ command: "forceCloseAutofillOverlay" });
- });
-
- it("force closes the autofill overlay if excessive mutations are being triggered", async () => {
- jest.useFakeTimers();
- autofillOverlayIframeService["mutationObserverIterations"] = 20;
-
- autofillOverlayIframeService["iframe"].src = "http://malicious-site.com";
- await flushPromises();
-
- expect(portSpy.postMessage).toBeCalledWith({ command: "forceCloseAutofillOverlay" });
- });
-
- it("resets the excessive mutations and foreign mutation counters", async () => {
- jest.useFakeTimers();
- autofillOverlayIframeService["foreignMutationsCount"] = 9;
- autofillOverlayIframeService["mutationObserverIterations"] = 19;
-
- autofillOverlayIframeService["iframe"].src = "http://malicious-site.com";
- jest.advanceTimersByTime(2001);
- await flushPromises();
-
- expect(autofillOverlayIframeService["foreignMutationsCount"]).toBe(0);
- expect(autofillOverlayIframeService["mutationObserverIterations"]).toBe(0);
- });
-
- it("resets any mutated default attributes for the iframe", async () => {
- jest.useFakeTimers();
-
- autofillOverlayIframeService["iframe"].title = "some-other-title";
- await flushPromises();
-
- expect(autofillOverlayIframeService["iframe"].title).toBe("title");
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts
deleted file mode 100644
index e0df9eb60b6..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts
+++ /dev/null
@@ -1,429 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { EVENTS } from "@bitwarden/common/autofill/constants";
-import { ThemeTypes } from "@bitwarden/common/platform/enums";
-
-import { setElementStyles } from "../../../utils";
-import {
- BackgroundPortMessageHandlers,
- AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface,
- AutofillOverlayIframeExtensionMessage,
- AutofillOverlayIframeWindowMessageHandlers,
-} from "../abstractions/autofill-overlay-iframe.service.deprecated";
-
-class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterface {
- private port: chrome.runtime.Port | null = null;
- private extensionOriginsSet: Set;
- private iframeMutationObserver: MutationObserver;
- private iframe: HTMLIFrameElement;
- private ariaAlertElement: HTMLDivElement;
- private ariaAlertTimeout: number | NodeJS.Timeout;
- private iframeStyles: Partial = {
- all: "initial",
- position: "fixed",
- display: "block",
- zIndex: "2147483647",
- lineHeight: "0",
- overflow: "hidden",
- transition: "opacity 125ms ease-out 0s",
- visibility: "visible",
- clipPath: "none",
- pointerEvents: "auto",
- margin: "0",
- padding: "0",
- colorScheme: "normal",
- opacity: "0",
- };
- private defaultIframeAttributes: Record = {
- src: "",
- title: "",
- sandbox: "allow-scripts",
- allowtransparency: "true",
- tabIndex: "-1",
- };
- private foreignMutationsCount = 0;
- private mutationObserverIterations = 0;
- private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
- private readonly windowMessageHandlers: AutofillOverlayIframeWindowMessageHandlers = {
- updateAutofillOverlayListHeight: (message) =>
- this.updateElementStyles(this.iframe, message.styles),
- getPageColorScheme: () => this.updateOverlayPageColorScheme(),
- };
- private readonly backgroundPortMessageHandlers: BackgroundPortMessageHandlers = {
- initAutofillOverlayList: ({ message }) => this.initAutofillOverlayList(message),
- updateIframePosition: ({ message }) => this.updateIframePosition(message.styles),
- updateOverlayHidden: ({ message }) => this.updateElementStyles(this.iframe, message.styles),
- };
-
- constructor(
- private iframePath: string,
- private portName: string,
- private shadow: ShadowRoot,
- ) {
- this.extensionOriginsSet = new Set([
- chrome.runtime.getURL("").slice(0, -1).toLowerCase(), // Remove the trailing slash and normalize the extension url to lowercase
- "null",
- ]);
-
- this.iframeMutationObserver = new MutationObserver(this.handleMutations);
- }
-
- /**
- * Handles initialization of the iframe which includes applying initial styles
- * to the iframe, setting the source, and adding listener that connects the
- * iframe to the background script each time it loads. Can conditionally
- * create an aria alert element to announce to screen readers when the iframe
- * is loaded. The end result is append to the shadowDOM of the custom element
- * that is declared.
- *
- *
- * @param initStyles - Initial styles to apply to the iframe
- * @param iframeTitle - Title to apply to the iframe
- * @param ariaAlert - Text to announce to screen readers when the iframe is loaded
- */
- initOverlayIframe(
- initStyles: Partial,
- iframeTitle: string,
- ariaAlert?: string,
- ) {
- this.defaultIframeAttributes.src = chrome.runtime.getURL(this.iframePath);
- this.defaultIframeAttributes.title = iframeTitle;
-
- this.iframe = globalThis.document.createElement("iframe");
- this.updateElementStyles(this.iframe, { ...this.iframeStyles, ...initStyles });
- for (const [attribute, value] of Object.entries(this.defaultIframeAttributes)) {
- this.iframe.setAttribute(attribute, value);
- }
- this.iframe.addEventListener(EVENTS.LOAD, this.setupPortMessageListener);
-
- if (ariaAlert) {
- this.createAriaAlertElement(ariaAlert);
- }
-
- this.shadow.appendChild(this.iframe);
- }
-
- /**
- * Creates an aria alert element that is used to announce to screen readers
- * when the iframe is loaded.
- *
- * @param ariaAlertText - Text to announce to screen readers when the iframe is loaded
- */
- private createAriaAlertElement(ariaAlertText: string) {
- this.ariaAlertElement = globalThis.document.createElement("div");
- this.ariaAlertElement.setAttribute("role", "status");
- this.ariaAlertElement.setAttribute("aria-live", "polite");
- this.ariaAlertElement.setAttribute("aria-atomic", "true");
- this.updateElementStyles(this.ariaAlertElement, {
- position: "absolute",
- top: "-9999px",
- left: "-9999px",
- width: "1px",
- height: "1px",
- overflow: "hidden",
- opacity: "0",
- pointerEvents: "none",
- });
- this.ariaAlertElement.textContent = ariaAlertText;
- }
-
- /**
- * Sets up the port message listener to the extension background script. This
- * listener is used to communicate between the iframe and the background script.
- * This also facilitates announcing to screen readers when the iframe is loaded.
- */
- private setupPortMessageListener = () => {
- this.port = chrome.runtime.connect({ name: this.portName });
- this.port.onDisconnect.addListener(this.handlePortDisconnect);
- this.port.onMessage.addListener(this.handlePortMessage);
- globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessage);
-
- this.announceAriaAlert();
- };
-
- /**
- * Announces the aria alert element to screen readers when the iframe is loaded.
- */
- private announceAriaAlert() {
- if (!this.ariaAlertElement) {
- return;
- }
-
- this.ariaAlertElement.remove();
- if (this.ariaAlertTimeout) {
- clearTimeout(this.ariaAlertTimeout);
- }
-
- this.ariaAlertTimeout = setTimeout(() => this.shadow.appendChild(this.ariaAlertElement), 2000);
- }
-
- /**
- * Handles disconnecting the port message listener from the extension background
- * script. This also removes the listener that facilitates announcing to screen
- * readers when the iframe is loaded.
- *
- * @param port - The port that is disconnected
- */
- private handlePortDisconnect = (port: chrome.runtime.Port) => {
- if (port.name !== this.portName) {
- return;
- }
-
- this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" });
- globalThis.removeEventListener("message", this.handleWindowMessage);
- this.unobserveIframe();
- this.port?.onMessage.removeListener(this.handlePortMessage);
- this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
- this.port?.disconnect();
- this.port = null;
- };
-
- /**
- * Handles messages sent from the extension background script to the iframe.
- * Triggers behavior within the iframe as well as on the custom element that
- * contains the iframe element.
- *
- * @param message
- * @param port
- */
- private handlePortMessage = (
- message: AutofillOverlayIframeExtensionMessage,
- port: chrome.runtime.Port,
- ) => {
- if (port.name !== this.portName) {
- return;
- }
-
- if (this.backgroundPortMessageHandlers[message.command]) {
- this.backgroundPortMessageHandlers[message.command]({ message, port });
- return;
- }
-
- this.iframe.contentWindow?.postMessage(message, "*");
- };
-
- /**
- * Handles messages sent from the iframe to the extension background script.
- * Will adjust the border element to fit the user's set theme.
- *
- * @param message - The message sent from the iframe
- */
- private initAutofillOverlayList(message: AutofillOverlayIframeExtensionMessage) {
- const { theme } = message;
- let borderColor: string;
- let verifiedTheme = theme;
- if (verifiedTheme === ThemeTypes.System) {
- verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches
- ? ThemeTypes.Dark
- : ThemeTypes.Light;
- }
-
- if (verifiedTheme === ThemeTypes.Dark) {
- borderColor = "#4c525f";
- }
- if (borderColor) {
- this.updateElementStyles(this.iframe, { borderColor });
- }
-
- message.theme = verifiedTheme;
- this.iframe.contentWindow?.postMessage(message, "*");
- }
-
- /**
- * Updates the position of the iframe element. Will also announce
- * to screen readers that the iframe is open.
- *
- * @param position - The position styles to apply to the iframe
- */
- private updateIframePosition(position: Partial) {
- if (!globalThis.document.hasFocus()) {
- return;
- }
-
- this.updateElementStyles(this.iframe, position);
- setTimeout(() => this.updateElementStyles(this.iframe, { opacity: "1" }), 0);
- this.announceAriaAlert();
- }
-
- /**
- * Gets the page color scheme meta tag and sends a message to the iframe
- * to update its color scheme. Will default to "normal" if the meta tag
- * does not exist.
- */
- private updateOverlayPageColorScheme() {
- const colorSchemeValue = globalThis.document
- .querySelector("meta[name='color-scheme']")
- ?.getAttribute("content");
-
- this.iframe.contentWindow?.postMessage(
- { command: "updateOverlayPageColorScheme", colorScheme: colorSchemeValue || "normal" },
- "*",
- );
- }
-
- /**
- * Handles messages sent from the iframe. If the message does not have a
- * specified handler set, it passes the message to the background script.
- *
- * @param event - The message event
- */
- private handleWindowMessage = (event: MessageEvent) => {
- if (
- !this.port ||
- event.source !== this.iframe.contentWindow ||
- !this.isFromExtensionOrigin(event.origin.toLowerCase())
- ) {
- return;
- }
-
- const message = event.data;
- if (this.windowMessageHandlers[message.command]) {
- this.windowMessageHandlers[message.command](message);
- return;
- }
-
- this.port.postMessage(event.data);
- };
-
- /**
- * Accepts an element and updates the styles for that element. This method
- * will also unobserve the element if it is the iframe element. This is
- * done to ensure that we do not trigger the mutation observer when we
- * update the styles for the iframe.
- *
- * @param customElement - The element to update the styles for
- * @param styles - The styles to apply to the element
- */
- private updateElementStyles(customElement: HTMLElement, styles: Partial) {
- if (!customElement) {
- return;
- }
-
- this.unobserveIframe();
-
- setElementStyles(customElement, styles, true);
- this.iframeStyles = { ...this.iframeStyles, ...styles };
-
- this.observeIframe();
- }
-
- /**
- * Chrome returns null for any sandboxed iframe sources.
- * Firefox references the extension URI as its origin.
- * Any other origin value is a security risk.
- *
- * @param messageOrigin - The origin of the window message
- */
- private isFromExtensionOrigin(messageOrigin: string): boolean {
- return this.extensionOriginsSet.has(messageOrigin);
- }
-
- /**
- * Handles mutations to the iframe element. The ensures that the iframe
- * element's styles are not modified by a third party source.
- *
- * @param mutations - The mutations to the iframe element
- */
- private handleMutations = (mutations: MutationRecord[]) => {
- if (this.isTriggeringExcessiveMutationObserverIterations()) {
- return;
- }
-
- for (let index = 0; index < mutations.length; index++) {
- const mutation = mutations[index];
- if (mutation.type !== "attributes") {
- continue;
- }
-
- const element = mutation.target as HTMLElement;
- if (mutation.attributeName !== "style") {
- this.handleElementAttributeMutation(element);
-
- continue;
- }
-
- this.iframe.removeAttribute("style");
- this.updateElementStyles(this.iframe, this.iframeStyles);
- }
- };
-
- /**
- * Handles mutations to the iframe element's attributes. This ensures that
- * the iframe element's attributes are not modified by a third party source.
- *
- * @param element - The element to handle attribute mutations for
- */
- private handleElementAttributeMutation(element: HTMLElement) {
- const attributes = Array.from(element.attributes);
- for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
- const attribute = attributes[attributeIndex];
- if (attribute.name === "style") {
- continue;
- }
-
- if (this.foreignMutationsCount >= 10) {
- this.port?.postMessage({ command: "forceCloseAutofillOverlay" });
- break;
- }
-
- const defaultIframeAttribute = this.defaultIframeAttributes[attribute.name];
- if (!defaultIframeAttribute) {
- this.iframe.removeAttribute(attribute.name);
- this.foreignMutationsCount++;
- continue;
- }
-
- if (attribute.value === defaultIframeAttribute) {
- continue;
- }
-
- this.iframe.setAttribute(attribute.name, defaultIframeAttribute);
- this.foreignMutationsCount++;
- }
- }
-
- /**
- * Observes the iframe element for mutations to its style attribute.
- */
- private observeIframe() {
- this.iframeMutationObserver.observe(this.iframe, { attributes: true });
- }
-
- /**
- * Unobserves the iframe element for mutations to its style attribute.
- */
- private unobserveIframe() {
- this.iframeMutationObserver?.disconnect();
- }
-
- /**
- * Identifies if the mutation observer is triggering excessive iterations.
- * Will remove the autofill overlay if any set mutation observer is
- * triggering excessive iterations.
- */
- private isTriggeringExcessiveMutationObserverIterations() {
- const resetCounters = () => {
- this.mutationObserverIterations = 0;
- this.foreignMutationsCount = 0;
- };
-
- if (this.mutationObserverIterationsResetTimeout) {
- clearTimeout(this.mutationObserverIterationsResetTimeout);
- }
-
- this.mutationObserverIterations++;
- this.mutationObserverIterationsResetTimeout = setTimeout(() => resetCounters(), 2000);
-
- if (this.mutationObserverIterations > 20) {
- clearTimeout(this.mutationObserverIterationsResetTimeout);
- resetCounters();
- this.port?.postMessage({ command: "forceCloseAutofillOverlay" });
-
- return true;
- }
-
- return false;
- }
-}
-
-export default AutofillOverlayIframeService;
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts
deleted file mode 100644
index 7daaaaef7d6..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import AutofillOverlayListIframe from "./autofill-overlay-list-iframe.deprecated";
-
-describe("AutofillOverlayListIframe", () => {
- window.customElements.define(
- "autofill-overlay-list-iframe",
- class extends HTMLElement {
- constructor() {
- super();
- new AutofillOverlayListIframe(this);
- }
- },
- );
-
- afterAll(() => {
- jest.clearAllMocks();
- });
-
- it("creates a custom element that is an instance of the AutofillIframeElement parent class", () => {
- document.body.innerHTML = "";
-
- const iframe = document.querySelector("autofill-overlay-list-iframe");
-
- expect(iframe).toBeInstanceOf(HTMLElement);
- expect(iframe.shadowRoot).toBeDefined();
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts
deleted file mode 100644
index fcfa007aec2..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { AutofillOverlayPort } from "../../../enums/autofill-overlay.enum";
-
-import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element.deprecated";
-
-class AutofillOverlayListIframe extends AutofillOverlayIframeElement {
- constructor(element: HTMLElement) {
- super(
- element,
- "overlay/list.html",
- AutofillOverlayPort.List,
- {
- height: "0px",
- minWidth: "250px",
- maxHeight: "180px",
- boxShadow: "rgba(0, 0, 0, 0.1) 2px 4px 6px 0px",
- borderRadius: "4px",
- borderWidth: "1px",
- borderStyle: "solid",
- borderColor: "rgb(206, 212, 220)",
- },
- chrome.i18n.getMessage("bitwardenVault"),
- );
- }
-}
-
-export default AutofillOverlayListIframe;
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap
deleted file mode 100644
index 61a8b49df29..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap
+++ /dev/null
@@ -1,83 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AutofillOverlayButton initAutofillOverlayButton creates the button element with the locked icon when the user's auth status is not Unlocked 1`] = `
-
-`;
-
-exports[`AutofillOverlayButton initAutofillOverlayButton creates the button element with the normal icon when the user's auth status is Unlocked 1`] = `
-
-`;
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts
deleted file mode 100644
index 7623a2ca97e..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-
-import { postWindowMessage } from "../../../../spec/testing-utils";
-import { InitAutofillOverlayButtonMessage } from "../../abstractions/autofill-overlay-button.deprecated";
-
-import AutofillOverlayButton from "./autofill-overlay-button.deprecated";
-
-const overlayPagesTranslations = {
- locale: "en",
- buttonPageTitle: "buttonPageTitle",
- listPageTitle: "listPageTitle",
- opensInANewWindow: "opensInANewWindow",
- toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay",
- unlockYourAccount: "unlockYourAccount",
- unlockAccount: "unlockAccount",
- fillCredentialsFor: "fillCredentialsFor",
- partialUsername: "partialUsername",
- view: "view",
- noItemsToShow: "noItemsToShow",
- newItem: "newItem",
- addNewVaultItem: "addNewVaultItem",
-};
-function createInitAutofillOverlayButtonMessageMock(
- customFields = {},
-): InitAutofillOverlayButtonMessage {
- return {
- command: "initAutofillOverlayButton",
- translations: overlayPagesTranslations,
- styleSheetUrl: "https://jest-testing-website.com",
- authStatus: AuthenticationStatus.Unlocked,
- ...customFields,
- };
-}
-
-describe("AutofillOverlayButton", () => {
- globalThis.customElements.define("autofill-overlay-button", AutofillOverlayButton);
-
- let autofillOverlayButton: AutofillOverlayButton;
-
- beforeEach(() => {
- document.body.innerHTML = ``;
- autofillOverlayButton = document.querySelector("autofill-overlay-button");
- autofillOverlayButton["messageOrigin"] = "https://localhost/";
- jest.spyOn(globalThis.document, "createElement");
- jest.spyOn(globalThis.parent, "postMessage");
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("initAutofillOverlayButton", () => {
- it("creates the button element with the locked icon when the user's auth status is not Unlocked", () => {
- postWindowMessage(
- createInitAutofillOverlayButtonMessageMock({ authStatus: AuthenticationStatus.Locked }),
- );
-
- expect(autofillOverlayButton["buttonElement"]).toMatchSnapshot();
- expect(autofillOverlayButton["buttonElement"].querySelector("svg")).toBe(
- autofillOverlayButton["logoLockedIconElement"],
- );
- });
-
- it("creates the button element with the normal icon when the user's auth status is Unlocked ", () => {
- postWindowMessage(createInitAutofillOverlayButtonMessageMock());
-
- expect(autofillOverlayButton["buttonElement"]).toMatchSnapshot();
- expect(autofillOverlayButton["buttonElement"].querySelector("svg")).toBe(
- autofillOverlayButton["logoIconElement"],
- );
- });
-
- it("posts a message to the background indicating that the icon was clicked", () => {
- postWindowMessage(createInitAutofillOverlayButtonMessageMock());
- autofillOverlayButton["buttonElement"].click();
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "overlayButtonClicked" },
- "https://localhost/",
- );
- });
- });
-
- describe("global event listeners", () => {
- beforeEach(() => {
- postWindowMessage(createInitAutofillOverlayButtonMessageMock());
- });
-
- it("does not post a message to close the autofill overlay if the element is focused during the focus check", () => {
- jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
-
- postWindowMessage({ command: "checkAutofillOverlayButtonFocused" });
-
- expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
- command: "closeAutofillOverlay",
- });
- });
-
- it("posts a message to close the autofill overlay if the element is not focused during the focus check", () => {
- jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
-
- postWindowMessage({ command: "checkAutofillOverlayButtonFocused" });
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "closeAutofillOverlay" },
- "https://localhost/",
- );
- });
-
- it("updates the user's auth status", () => {
- autofillOverlayButton["authStatus"] = AuthenticationStatus.Locked;
-
- postWindowMessage({
- command: "updateAutofillOverlayButtonAuthStatus",
- authStatus: AuthenticationStatus.Unlocked,
- });
-
- expect(autofillOverlayButton["authStatus"]).toBe(AuthenticationStatus.Unlocked);
- });
-
- it("updates the page color scheme meta tag", () => {
- const colorSchemeMetaTag = globalThis.document.createElement("meta");
- colorSchemeMetaTag.setAttribute("name", "color-scheme");
- colorSchemeMetaTag.setAttribute("content", "light");
- globalThis.document.head.append(colorSchemeMetaTag);
-
- postWindowMessage({
- command: "updateOverlayPageColorScheme",
- colorScheme: "dark",
- });
-
- expect(colorSchemeMetaTag.getAttribute("content")).toBe("dark");
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts
deleted file mode 100644
index a39ed99d424..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import "@webcomponents/custom-elements";
-import "lit/polyfill-support.js";
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { EVENTS } from "@bitwarden/common/autofill/constants";
-
-import { buildSvgDomElement } from "../../../../utils";
-import { logoIcon, logoLockedIcon } from "../../../../utils/svg-icons";
-import {
- InitAutofillOverlayButtonMessage,
- OverlayButtonMessage,
- OverlayButtonWindowMessageHandlers,
-} from "../../abstractions/autofill-overlay-button.deprecated";
-import AutofillOverlayPageElement from "../shared/autofill-overlay-page-element.deprecated";
-
-class AutofillOverlayButton extends AutofillOverlayPageElement {
- private authStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
- private readonly buttonElement: HTMLButtonElement;
- private readonly logoIconElement: HTMLElement;
- private readonly logoLockedIconElement: HTMLElement;
- private readonly overlayButtonWindowMessageHandlers: OverlayButtonWindowMessageHandlers = {
- initAutofillOverlayButton: ({ message }) => this.initAutofillOverlayButton(message),
- checkAutofillOverlayButtonFocused: () => this.checkButtonFocused(),
- updateAutofillOverlayButtonAuthStatus: ({ message }) =>
- this.updateAuthStatus(message.authStatus),
- updateOverlayPageColorScheme: ({ message }) => this.updatePageColorScheme(message),
- };
-
- constructor() {
- super();
-
- this.buttonElement = globalThis.document.createElement("button");
-
- this.setupGlobalListeners(this.overlayButtonWindowMessageHandlers);
-
- this.logoIconElement = buildSvgDomElement(logoIcon);
- this.logoIconElement.classList.add("overlay-button-svg-icon", "logo-icon");
-
- this.logoLockedIconElement = buildSvgDomElement(logoLockedIcon);
- this.logoLockedIconElement.classList.add("overlay-button-svg-icon", "logo-locked-icon");
- }
-
- /**
- * Initializes the overlay button. Facilitates ensuring that the page
- * is set up with the expected styles and translations.
- *
- * @param authStatus - The authentication status of the user
- * @param styleSheetUrl - The URL of the stylesheet to apply to the page
- * @param translations - The translations to apply to the page
- * @private
- */
- private async initAutofillOverlayButton({
- authStatus,
- styleSheetUrl,
- translations,
- }: InitAutofillOverlayButtonMessage) {
- const linkElement = this.initOverlayPage("button", styleSheetUrl, translations);
-
- this.buttonElement.tabIndex = -1;
- this.buttonElement.type = "button";
- this.buttonElement.classList.add("overlay-button");
- this.buttonElement.setAttribute(
- "aria-label",
- this.getTranslation("toggleBitwardenVaultOverlay"),
- );
- this.buttonElement.addEventListener(EVENTS.CLICK, this.handleButtonElementClick);
- this.postMessageToParent({ command: "getPageColorScheme" });
-
- this.updateAuthStatus(authStatus);
-
- this.shadowDom.append(linkElement, this.buttonElement);
- }
-
- /**
- * Updates the authentication status of the user. This will update the icon
- * displayed on the button.
- *
- * @param authStatus - The authentication status of the user
- */
- private updateAuthStatus(authStatus: AuthenticationStatus) {
- this.authStatus = authStatus;
-
- this.buttonElement.innerHTML = "";
- const iconElement =
- this.authStatus === AuthenticationStatus.Unlocked
- ? this.logoIconElement
- : this.logoLockedIconElement;
- this.buttonElement.append(iconElement);
- }
-
- /**
- * Handles updating the page color scheme meta tag. Ensures that the button
- * does not present with a non-transparent background on dark mode pages.
- *
- * @param colorScheme - The color scheme of the iframe's parent page
- */
- private updatePageColorScheme({ colorScheme }: OverlayButtonMessage) {
- const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']");
- colorSchemeMetaTag?.setAttribute("content", colorScheme);
- }
-
- /**
- * Handles a click event on the button element. Posts a message to the
- * parent window indicating that the button was clicked.
- */
- private handleButtonElementClick = () => {
- this.postMessageToParent({ command: "overlayButtonClicked" });
- };
-
- /**
- * Checks if the button is focused. If it is not, then it posts a message
- * to the parent window indicating that the overlay should be closed.
- */
- private checkButtonFocused() {
- if (globalThis.document.hasFocus()) {
- return;
- }
-
- this.postMessageToParent({ command: "closeAutofillOverlay" });
- }
-}
-
-export default AutofillOverlayButton;
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts
deleted file mode 100644
index fd6a79733cb..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum";
-
-import AutofillOverlayButton from "./autofill-overlay-button.deprecated";
-
-// FIXME: Remove when updating file. Eslint update
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-require("./legacy-button.scss");
-
-(function () {
- globalThis.customElements.define(AutofillOverlayElement.Button, AutofillOverlayButton);
-})();
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.html b/apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.html
deleted file mode 100644
index 2211023581f..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Bitwarden overlay button
-
-
-
-
-
-
-
-
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss b/apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss
deleted file mode 100644
index 507442c07dc..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-@import "../../../../shared/styles/variables";
-
-* {
- box-sizing: border-box;
-}
-
-body {
- width: 100%;
- min-width: 100vw;
- height: 100%;
- min-height: 100vh;
- padding: 0;
- margin: 0;
- background: transparent;
- overflow: hidden;
-}
-autofill-overlay-button {
- width: 100%;
- height: auto;
-}
-
-.overlay-button {
- display: block;
- width: 100%;
- padding: 0;
- margin: auto;
- border: none;
- background: transparent;
- cursor: pointer;
-
- .overlay-button-svg-icon {
- display: block;
- width: 100%;
- height: auto;
- }
-}
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap
deleted file mode 100644
index d11fbd50792..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap
+++ /dev/null
@@ -1,537 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an unauthenticated user creates the views for the locked overlay 1`] = `
-
-
- unlockYourAccount
-
-
-
-
-
-`;
-
-exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty list of ciphers creates the views for the no results overlay 1`] = `
-
-
- noItemsToShow
-
-
-
-
-
-`;
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts
deleted file mode 100644
index 48c27e436a4..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts
+++ /dev/null
@@ -1,467 +0,0 @@
-import { mock } from "jest-mock-extended";
-
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { ThemeType } from "@bitwarden/common/platform/enums";
-
-import { createAutofillOverlayCipherDataMock } from "../../../../spec/autofill-mocks";
-import { postWindowMessage } from "../../../../spec/testing-utils";
-import { InitAutofillOverlayListMessage } from "../../abstractions/autofill-overlay-list.deprecated";
-
-import AutofillOverlayList from "./autofill-overlay-list.deprecated";
-
-const overlayPagesTranslations = {
- locale: "en",
- buttonPageTitle: "buttonPageTitle",
- listPageTitle: "listPageTitle",
- opensInANewWindow: "opensInANewWindow",
- toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay",
- unlockYourAccount: "unlockYourAccount",
- unlockAccount: "unlockAccount",
- fillCredentialsFor: "fillCredentialsFor",
- partialUsername: "partialUsername",
- view: "view",
- noItemsToShow: "noItemsToShow",
- newItem: "newItem",
- addNewVaultItem: "addNewVaultItem",
-};
-function createInitAutofillOverlayListMessageMock(
- customFields = {},
-): InitAutofillOverlayListMessage {
- return {
- command: "initAutofillOverlayList",
- translations: overlayPagesTranslations,
- styleSheetUrl: "https://jest-testing-website.com",
- theme: ThemeType.Light,
- authStatus: AuthenticationStatus.Unlocked,
- ciphers: [
- createAutofillOverlayCipherDataMock(1, {
- icon: {
- imageEnabled: true,
- image: "https://jest-testing-website.com/image.png",
- fallbackImage: "",
- icon: "bw-icon",
- },
- }),
- createAutofillOverlayCipherDataMock(2, {
- icon: {
- imageEnabled: true,
- image: "",
- fallbackImage: "https://jest-testing-website.com/fallback.png",
- icon: "bw-icon",
- },
- }),
- createAutofillOverlayCipherDataMock(3, {
- name: "",
- login: { username: "" },
- icon: { imageEnabled: true, image: "", fallbackImage: "", icon: "bw-icon" },
- }),
- createAutofillOverlayCipherDataMock(4, {
- icon: { imageEnabled: false, image: "", fallbackImage: "", icon: "" },
- }),
- createAutofillOverlayCipherDataMock(5),
- createAutofillOverlayCipherDataMock(6),
- createAutofillOverlayCipherDataMock(7),
- createAutofillOverlayCipherDataMock(8),
- ],
- ...customFields,
- };
-}
-
-describe("AutofillOverlayList", () => {
- globalThis.customElements.define("autofill-overlay-list", AutofillOverlayList);
- global.ResizeObserver = jest.fn().mockImplementation(() => ({
- observe: jest.fn(),
- unobserve: jest.fn(),
- disconnect: jest.fn(),
- }));
-
- let autofillOverlayList: AutofillOverlayList;
-
- beforeEach(() => {
- document.body.innerHTML = ``;
- autofillOverlayList = document.querySelector("autofill-overlay-list");
- autofillOverlayList["messageOrigin"] = "https://localhost/";
- jest.spyOn(globalThis.document, "createElement");
- jest.spyOn(globalThis.parent, "postMessage");
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("initAutofillOverlayList", () => {
- describe("the locked overlay for an unauthenticated user", () => {
- beforeEach(() => {
- postWindowMessage(
- createInitAutofillOverlayListMessageMock({
- authStatus: AuthenticationStatus.Locked,
- cipherList: [],
- }),
- );
- });
-
- it("creates the views for the locked overlay", () => {
- expect(autofillOverlayList["overlayListContainer"]).toMatchSnapshot();
- });
-
- it("allows the user to unlock the vault", () => {
- const unlockButton =
- autofillOverlayList["overlayListContainer"].querySelector("#unlock-button");
-
- unlockButton.dispatchEvent(new Event("click"));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "unlockVault" },
- "https://localhost/",
- );
- });
- });
-
- describe("the overlay with an empty list of ciphers", () => {
- beforeEach(() => {
- postWindowMessage(
- createInitAutofillOverlayListMessageMock({
- authStatus: AuthenticationStatus.Unlocked,
- ciphers: [],
- }),
- );
- });
-
- it("creates the views for the no results overlay", () => {
- expect(autofillOverlayList["overlayListContainer"]).toMatchSnapshot();
- });
-
- it("allows the user to add a vault item", () => {
- const addVaultItemButton =
- autofillOverlayList["overlayListContainer"].querySelector("#new-item-button");
-
- addVaultItemButton.dispatchEvent(new Event("click"));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "addNewVaultItem" },
- "https://localhost/",
- );
- });
- });
-
- describe("the list of ciphers for an authenticated user", () => {
- beforeEach(() => {
- postWindowMessage(createInitAutofillOverlayListMessageMock());
- });
-
- it("creates the view for a list of ciphers", () => {
- expect(autofillOverlayList["overlayListContainer"]).toMatchSnapshot();
- });
-
- it("loads ciphers on scroll one page at a time", () => {
- jest.useFakeTimers();
- const originalListOfElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container");
-
- autofillOverlayList["handleCiphersListScrollEvent"]();
- jest.runAllTimers();
-
- const updatedListOfElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container");
-
- expect(originalListOfElements.length).toBe(6);
- expect(updatedListOfElements.length).toBe(8);
- });
-
- it("debounces the ciphers scroll handler", () => {
- jest.useFakeTimers();
- autofillOverlayList["cipherListScrollDebounceTimeout"] = setTimeout(jest.fn, 0);
- const handleDebouncedScrollEventSpy = jest.spyOn(
- autofillOverlayList as any,
- "handleDebouncedScrollEvent",
- );
-
- autofillOverlayList["handleCiphersListScrollEvent"]();
- jest.advanceTimersByTime(100);
- autofillOverlayList["handleCiphersListScrollEvent"]();
- jest.advanceTimersByTime(100);
- autofillOverlayList["handleCiphersListScrollEvent"]();
- jest.advanceTimersByTime(400);
-
- expect(handleDebouncedScrollEventSpy).toHaveBeenCalledTimes(1);
- });
-
- describe("fill cipher button event listeners", () => {
- beforeEach(() => {
- postWindowMessage(createInitAutofillOverlayListMessageMock());
- });
-
- it("allows the user to fill a cipher on click", () => {
- const fillCipherButton =
- autofillOverlayList["overlayListContainer"].querySelector(".fill-cipher-button");
-
- fillCipherButton.dispatchEvent(new Event("click"));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "fillSelectedListItem", overlayCipherId: "1" },
- "https://localhost/",
- );
- });
-
- it("allows the user to move keyboard focus to the next cipher element on ArrowDown", () => {
- const fillCipherElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button");
- const firstFillCipherElement = fillCipherElements[0];
- const secondFillCipherElement = fillCipherElements[1];
- jest.spyOn(secondFillCipherElement as HTMLElement, "focus");
-
- firstFillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
-
- expect((secondFillCipherElement as HTMLElement).focus).toBeCalled();
- });
-
- it("directs focus to the first item in the cipher list if no cipher is present after the current one when pressing ArrowDown", () => {
- const fillCipherElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button");
- const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1];
- const firstFillCipherElement = fillCipherElements[0];
- jest.spyOn(firstFillCipherElement as HTMLElement, "focus");
-
- lastFillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
-
- expect((firstFillCipherElement as HTMLElement).focus).toBeCalled();
- });
-
- it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => {
- const fillCipherElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button");
- const firstFillCipherElement = fillCipherElements[0];
- const secondFillCipherElement = fillCipherElements[1];
- jest.spyOn(firstFillCipherElement as HTMLElement, "focus");
-
- secondFillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" }));
-
- expect((firstFillCipherElement as HTMLElement).focus).toBeCalled();
- });
-
- it("directs focus to the last item in the cipher list if no cipher is present before the current one when pressing ArrowUp", () => {
- const fillCipherElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button");
- const firstFillCipherElement = fillCipherElements[0];
- const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1];
- jest.spyOn(lastFillCipherElement as HTMLElement, "focus");
-
- firstFillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" }));
-
- expect((lastFillCipherElement as HTMLElement).focus).toBeCalled();
- });
-
- it("allows the user to move keyboard focus to the view cipher button on ArrowRight", () => {
- const cipherContainerElement =
- autofillOverlayList["overlayListContainer"].querySelector(".cipher-container");
- const fillCipherElement = cipherContainerElement.querySelector(".fill-cipher-button");
- const viewCipherButton = cipherContainerElement.querySelector(".view-cipher-button");
- jest.spyOn(viewCipherButton as HTMLElement, "focus");
-
- fillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowRight" }));
-
- expect((viewCipherButton as HTMLElement).focus).toBeCalled();
- });
-
- it("ignores keyup events that do not include ArrowUp, ArrowDown, or ArrowRight", () => {
- const fillCipherElement =
- autofillOverlayList["overlayListContainer"].querySelector(".fill-cipher-button");
- jest.spyOn(fillCipherElement as HTMLElement, "focus");
-
- fillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowLeft" }));
-
- expect((fillCipherElement as HTMLElement).focus).not.toBeCalled();
- });
- });
-
- describe("view cipher button event listeners", () => {
- beforeEach(() => {
- postWindowMessage(createInitAutofillOverlayListMessageMock());
- });
-
- it("allows the user to view a cipher on click", () => {
- const viewCipherButton =
- autofillOverlayList["overlayListContainer"].querySelector(".view-cipher-button");
-
- viewCipherButton.dispatchEvent(new Event("click"));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "viewSelectedCipher", overlayCipherId: "1" },
- "https://localhost/",
- );
- });
-
- it("allows the user to move keyboard focus to the current cipher element on ArrowLeft", () => {
- const cipherContainerElement =
- autofillOverlayList["overlayListContainer"].querySelector(".cipher-container");
- const fillCipherButton = cipherContainerElement.querySelector(".fill-cipher-button");
- const viewCipherButton = cipherContainerElement.querySelector(".view-cipher-button");
- jest.spyOn(fillCipherButton as HTMLElement, "focus");
-
- viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowLeft" }));
-
- expect((fillCipherButton as HTMLElement).focus).toBeCalled();
- });
-
- it("allows the user to move keyboard to the next cipher element on ArrowDown", () => {
- const cipherContainerElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container");
- const viewCipherButton = cipherContainerElements[0].querySelector(".view-cipher-button");
- const secondFillCipherButton =
- cipherContainerElements[1].querySelector(".fill-cipher-button");
- jest.spyOn(secondFillCipherButton as HTMLElement, "focus");
-
- viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
-
- expect((secondFillCipherButton as HTMLElement).focus).toBeCalled();
- });
-
- it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => {
- const cipherContainerElements =
- autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container");
- const viewCipherButton = cipherContainerElements[1].querySelector(".view-cipher-button");
- const firstFillCipherButton =
- cipherContainerElements[0].querySelector(".fill-cipher-button");
- jest.spyOn(firstFillCipherButton as HTMLElement, "focus");
-
- viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" }));
-
- expect((firstFillCipherButton as HTMLElement).focus).toBeCalled();
- });
-
- it("ignores keyup events that do not include ArrowUp, ArrowDown, or ArrowRight", () => {
- const viewCipherButton =
- autofillOverlayList["overlayListContainer"].querySelector(".view-cipher-button");
- jest.spyOn(viewCipherButton as HTMLElement, "focus");
-
- viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowRight" }));
-
- expect((viewCipherButton as HTMLElement).focus).not.toBeCalled();
- });
- });
- });
- });
-
- describe("global event listener handlers", () => {
- it("does not post a `checkAutofillOverlayButtonFocused` message to the parent if the overlay is currently focused", () => {
- jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
-
- postWindowMessage({ command: "checkAutofillOverlayListFocused" });
-
- expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
- });
-
- it("posts a `checkAutofillOverlayButtonFocused` message to the parent if the overlay is not currently focused", () => {
- jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
-
- postWindowMessage({ command: "checkAutofillOverlayListFocused" });
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "checkAutofillOverlayButtonFocused" },
- "https://localhost/",
- );
- });
-
- it("updates the list of ciphers", () => {
- postWindowMessage(createInitAutofillOverlayListMessageMock());
- const updateCiphersSpy = jest.spyOn(autofillOverlayList as any, "updateListItems");
-
- postWindowMessage({ command: "updateOverlayListCiphers" });
-
- expect(updateCiphersSpy).toHaveBeenCalled();
- });
-
- describe("directing user focus into the overlay list", () => {
- it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => {
- postWindowMessage(
- createInitAutofillOverlayListMessageMock({
- authStatus: AuthenticationStatus.Locked,
- cipherList: [],
- }),
- );
- const overlayContainerSetAttributeSpy = jest.spyOn(
- autofillOverlayList["overlayListContainer"],
- "setAttribute",
- );
-
- postWindowMessage({ command: "focusOverlayList" });
-
- expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
- expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
- });
-
- it("focuses the unlock button element if the user is not authenticated", () => {
- postWindowMessage(
- createInitAutofillOverlayListMessageMock({
- authStatus: AuthenticationStatus.Locked,
- cipherList: [],
- }),
- );
- const unlockButton =
- autofillOverlayList["overlayListContainer"].querySelector("#unlock-button");
- jest.spyOn(unlockButton as HTMLElement, "focus");
-
- postWindowMessage({ command: "focusOverlayList" });
-
- expect((unlockButton as HTMLElement).focus).toBeCalled();
- });
-
- it("focuses the new item button element if the cipher list is empty", () => {
- postWindowMessage(createInitAutofillOverlayListMessageMock({ ciphers: [] }));
- const newItemButton =
- autofillOverlayList["overlayListContainer"].querySelector("#new-item-button");
- jest.spyOn(newItemButton as HTMLElement, "focus");
-
- postWindowMessage({ command: "focusOverlayList" });
-
- expect((newItemButton as HTMLElement).focus).toBeCalled();
- });
-
- it("focuses the first cipher button element if the cipher list is populated", () => {
- postWindowMessage(createInitAutofillOverlayListMessageMock());
- const firstCipherItem =
- autofillOverlayList["overlayListContainer"].querySelector(".fill-cipher-button");
- jest.spyOn(firstCipherItem as HTMLElement, "focus");
-
- postWindowMessage({ command: "focusOverlayList" });
-
- expect((firstCipherItem as HTMLElement).focus).toBeCalled();
- });
- });
- });
-
- describe("handleResizeObserver", () => {
- beforeEach(() => {
- postWindowMessage(createInitAutofillOverlayListMessageMock());
- });
-
- it("ignores resize entries whose target is not the overlay list", () => {
- const entries = [
- {
- target: mock(),
- contentRect: { height: 300 },
- },
- ];
-
- autofillOverlayList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
-
- expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
- });
-
- it("posts a message to update the overlay list height if the list container is resized", () => {
- const entries = [
- {
- target: autofillOverlayList["overlayListContainer"],
- contentRect: { height: 300 },
- },
- ];
-
- autofillOverlayList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "updateAutofillOverlayListHeight", styles: { height: "300px" } },
- "https://localhost/",
- );
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts
deleted file mode 100644
index 2ab38fe5906..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts
+++ /dev/null
@@ -1,621 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import "@webcomponents/custom-elements";
-import "lit/polyfill-support.js";
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { EVENTS } from "@bitwarden/common/autofill/constants";
-
-import { buildSvgDomElement } from "../../../../utils";
-import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../../utils/svg-icons";
-import { OverlayCipherData } from "../../../background/abstractions/overlay.background.deprecated";
-import {
- InitAutofillOverlayListMessage,
- OverlayListWindowMessageHandlers,
-} from "../../abstractions/autofill-overlay-list.deprecated";
-import AutofillOverlayPageElement from "../shared/autofill-overlay-page-element.deprecated";
-
-class AutofillOverlayList extends AutofillOverlayPageElement {
- private overlayListContainer: HTMLDivElement;
- private resizeObserver: ResizeObserver;
- private eventHandlersMemo: { [key: string]: EventListener } = {};
- private ciphers: OverlayCipherData[] = [];
- private ciphersList: HTMLUListElement;
- private cipherListScrollIsDebounced = false;
- private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
- private currentCipherIndex = 0;
- private readonly showCiphersPerPage = 6;
- private readonly overlayListWindowMessageHandlers: OverlayListWindowMessageHandlers = {
- initAutofillOverlayList: ({ message }) => this.initAutofillOverlayList(message),
- checkAutofillOverlayListFocused: () => this.checkOverlayListFocused(),
- updateOverlayListCiphers: ({ message }) => this.updateListItems(message.ciphers),
- focusOverlayList: () => this.focusOverlayList(),
- };
-
- constructor() {
- super();
-
- this.setupOverlayListGlobalListeners();
- }
-
- /**
- * Initializes the overlay list and updates the list items with the passed ciphers.
- * If the auth status is not `Unlocked`, the locked overlay is built.
- *
- * @param translations - The translations to use for the overlay list.
- * @param styleSheetUrl - The URL of the stylesheet to use for the overlay list.
- * @param theme - The theme to use for the overlay list.
- * @param authStatus - The current authentication status.
- * @param ciphers - The ciphers to display in the overlay list.
- */
- private async initAutofillOverlayList({
- translations,
- styleSheetUrl,
- theme,
- authStatus,
- ciphers,
- }: InitAutofillOverlayListMessage) {
- const linkElement = this.initOverlayPage("list", styleSheetUrl, translations);
-
- const themeClass = `theme_${theme}`;
- globalThis.document.documentElement.classList.add(themeClass);
-
- this.overlayListContainer = globalThis.document.createElement("div");
- this.overlayListContainer.classList.add("overlay-list-container", themeClass);
- this.resizeObserver.observe(this.overlayListContainer);
-
- this.shadowDom.append(linkElement, this.overlayListContainer);
-
- if (authStatus === AuthenticationStatus.Unlocked) {
- this.updateListItems(ciphers);
- return;
- }
-
- this.buildLockedOverlay();
- }
-
- /**
- * Builds the locked overlay, which is displayed when the user is not authenticated.
- * Facilitates the ability to unlock the extension from the overlay.
- */
- private buildLockedOverlay() {
- const lockedOverlay = globalThis.document.createElement("div");
- lockedOverlay.id = "locked-overlay-description";
- lockedOverlay.classList.add("locked-overlay", "overlay-list-message");
- lockedOverlay.textContent = this.getTranslation("unlockYourAccount");
-
- const unlockButtonElement = globalThis.document.createElement("button");
- unlockButtonElement.id = "unlock-button";
- unlockButtonElement.tabIndex = -1;
- unlockButtonElement.classList.add("unlock-button", "overlay-list-button");
- unlockButtonElement.textContent = this.getTranslation("unlockAccount");
- unlockButtonElement.setAttribute(
- "aria-label",
- `${this.getTranslation("unlockAccount")}, ${this.getTranslation("opensInANewWindow")}`,
- );
- unlockButtonElement.prepend(buildSvgDomElement(lockIcon));
- unlockButtonElement.addEventListener(EVENTS.CLICK, this.handleUnlockButtonClick);
-
- const overlayListButtonContainer = globalThis.document.createElement("div");
- overlayListButtonContainer.classList.add("overlay-list-button-container");
- overlayListButtonContainer.appendChild(unlockButtonElement);
-
- this.overlayListContainer.append(lockedOverlay, overlayListButtonContainer);
- }
-
- /**
- * Handles the click event for the unlock button.
- * Sends a message to the parent window to unlock the vault.
- */
- private handleUnlockButtonClick = () => {
- this.postMessageToParent({ command: "unlockVault" });
- };
-
- /**
- * Updates the list items with the passed ciphers.
- * If no ciphers are passed, the no results overlay is built.
- *
- * @param ciphers - The ciphers to display in the overlay list.
- */
- private updateListItems(ciphers: OverlayCipherData[]) {
- this.ciphers = ciphers;
- this.currentCipherIndex = 0;
- if (this.overlayListContainer) {
- this.overlayListContainer.innerHTML = "";
- }
-
- if (!ciphers?.length) {
- this.buildNoResultsOverlayList();
- return;
- }
-
- this.ciphersList = globalThis.document.createElement("ul");
- this.ciphersList.classList.add("overlay-actions-list");
- this.ciphersList.setAttribute("role", "list");
- globalThis.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
-
- this.loadPageOfCiphers();
-
- this.overlayListContainer.appendChild(this.ciphersList);
- }
-
- /**
- * Overlay view that is presented when no ciphers are found for a given page.
- * Facilitates the ability to add a new vault item from the overlay.
- */
- private buildNoResultsOverlayList() {
- const noItemsMessage = globalThis.document.createElement("div");
- noItemsMessage.classList.add("no-items", "overlay-list-message");
- noItemsMessage.textContent = this.getTranslation("noItemsToShow");
-
- const newItemButton = globalThis.document.createElement("button");
- newItemButton.tabIndex = -1;
- newItemButton.id = "new-item-button";
- newItemButton.classList.add("add-new-item-button", "overlay-list-button");
- newItemButton.textContent = this.getTranslation("newItem");
- newItemButton.setAttribute(
- "aria-label",
- `${this.getTranslation("addNewVaultItem")}, ${this.getTranslation("opensInANewWindow")}`,
- );
- newItemButton.prepend(buildSvgDomElement(plusIcon));
- newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
-
- const overlayListButtonContainer = globalThis.document.createElement("div");
- overlayListButtonContainer.classList.add("overlay-list-button-container");
- overlayListButtonContainer.appendChild(newItemButton);
-
- this.overlayListContainer.append(noItemsMessage, overlayListButtonContainer);
- }
-
- /**
- * Handles the click event for the new item button.
- * Sends a message to the parent window to add a new vault item.
- */
- private handeNewItemButtonClick = () => {
- this.postMessageToParent({ command: "addNewVaultItem" });
- };
-
- /**
- * Loads a page of ciphers into the overlay list container.
- */
- private loadPageOfCiphers() {
- const lastIndex = Math.min(
- this.currentCipherIndex + this.showCiphersPerPage,
- this.ciphers.length,
- );
- for (let cipherIndex = this.currentCipherIndex; cipherIndex < lastIndex; cipherIndex++) {
- this.ciphersList.appendChild(this.buildOverlayActionsListItem(this.ciphers[cipherIndex]));
- this.currentCipherIndex++;
- }
-
- if (this.currentCipherIndex >= this.ciphers.length) {
- globalThis.removeEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
- }
- }
-
- /**
- * Handles updating the list of ciphers when the
- * user scrolls to the bottom of the list.
- */
- private handleCiphersListScrollEvent = () => {
- if (this.cipherListScrollIsDebounced) {
- return;
- }
-
- this.cipherListScrollIsDebounced = true;
- if (this.cipherListScrollDebounceTimeout) {
- clearTimeout(this.cipherListScrollDebounceTimeout);
- }
- this.cipherListScrollDebounceTimeout = setTimeout(this.handleDebouncedScrollEvent, 300);
- };
-
- /**
- * Debounced handler for updating the list of ciphers when the user scrolls to
- * the bottom of the list. Triggers at most once every 300ms.
- */
- private handleDebouncedScrollEvent = () => {
- this.cipherListScrollIsDebounced = false;
-
- if (globalThis.scrollY + globalThis.innerHeight >= this.ciphersList.clientHeight - 300) {
- this.loadPageOfCiphers();
- }
- };
-
- /**
- * Builds the list item for a given cipher.
- *
- * @param cipher - The cipher to build the list item for.
- */
- private buildOverlayActionsListItem(cipher: OverlayCipherData) {
- const fillCipherElement = this.buildFillCipherElement(cipher);
- const viewCipherElement = this.buildViewCipherElement(cipher);
-
- const cipherContainerElement = globalThis.document.createElement("div");
- cipherContainerElement.classList.add("cipher-container");
- cipherContainerElement.append(fillCipherElement, viewCipherElement);
-
- const overlayActionsListItem = globalThis.document.createElement("li");
- overlayActionsListItem.setAttribute("role", "listitem");
- overlayActionsListItem.classList.add("overlay-actions-list-item");
- overlayActionsListItem.appendChild(cipherContainerElement);
-
- return overlayActionsListItem;
- }
-
- /**
- * Builds the fill cipher button for a given cipher.
- * Wraps the cipher icon and details.
- *
- * @param cipher - The cipher to build the fill cipher button for.
- */
- private buildFillCipherElement(cipher: OverlayCipherData) {
- const cipherIcon = this.buildCipherIconElement(cipher);
- const cipherDetailsElement = this.buildCipherDetailsElement(cipher);
-
- const fillCipherElement = globalThis.document.createElement("button");
- fillCipherElement.tabIndex = -1;
- fillCipherElement.classList.add("fill-cipher-button");
- fillCipherElement.setAttribute(
- "aria-label",
- `${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
- );
- fillCipherElement.setAttribute(
- "aria-description",
- `${this.getTranslation("partialUsername")}, ${cipher.login.username}`,
- );
- fillCipherElement.append(cipherIcon, cipherDetailsElement);
- fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher));
- fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent);
-
- return fillCipherElement;
- }
-
- /**
- * Handles the click event for the fill cipher button.
- * Sends a message to the parent window to fill the selected cipher.
- *
- * @param cipher - The cipher to fill.
- */
- private handleFillCipherClickEvent = (cipher: OverlayCipherData) => {
- return this.useEventHandlersMemo(
- () =>
- this.postMessageToParent({
- command: "fillSelectedListItem",
- overlayCipherId: cipher.id,
- }),
- `${cipher.id}-fill-cipher-button-click-handler`,
- );
- };
-
- /**
- * Handles the keyup event for the fill cipher button. Facilitates
- * selecting the next/previous cipher item on ArrowDown/ArrowUp. Also
- * facilitates moving keyboard focus to the view cipher button on ArrowRight.
- *
- * @param event - The keyup event.
- */
- private handleFillCipherKeyUpEvent = (event: KeyboardEvent) => {
- const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowRight"]);
- if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
- return;
- }
-
- event.preventDefault();
-
- const currentListItem = event.target.closest(".overlay-actions-list-item") as HTMLElement;
- if (event.code === "ArrowDown") {
- this.focusNextListItem(currentListItem);
- return;
- }
-
- if (event.code === "ArrowUp") {
- this.focusPreviousListItem(currentListItem);
- return;
- }
-
- this.focusViewCipherButton(currentListItem, event.target as HTMLElement);
- };
-
- /**
- * Builds the button that facilitates viewing a cipher in the vault.
- *
- * @param cipher - The cipher to view.
- */
- private buildViewCipherElement(cipher: OverlayCipherData) {
- const viewCipherElement = globalThis.document.createElement("button");
- viewCipherElement.tabIndex = -1;
- viewCipherElement.classList.add("view-cipher-button");
- viewCipherElement.setAttribute(
- "aria-label",
- `${this.getTranslation("view")} ${cipher.name}, ${this.getTranslation("opensInANewWindow")}`,
- );
- viewCipherElement.append(buildSvgDomElement(viewCipherIcon));
- viewCipherElement.addEventListener(EVENTS.CLICK, this.handleViewCipherClickEvent(cipher));
- viewCipherElement.addEventListener(EVENTS.KEYUP, this.handleViewCipherKeyUpEvent);
-
- return viewCipherElement;
- }
-
- /**
- * Handles the click event for the view cipher button. Sends a
- * message to the parent window to view the selected cipher.
- *
- * @param cipher - The cipher to view.
- */
- private handleViewCipherClickEvent = (cipher: OverlayCipherData) => {
- return this.useEventHandlersMemo(
- () => this.postMessageToParent({ command: "viewSelectedCipher", overlayCipherId: cipher.id }),
- `${cipher.id}-view-cipher-button-click-handler`,
- );
- };
-
- /**
- * Handles the keyup event for the view cipher button. Facilitates
- * selecting the next/previous cipher item on ArrowDown/ArrowUp.
- * Also facilitates moving keyboard focus to the current fill
- * cipher button on ArrowLeft.
- *
- * @param event - The keyup event.
- */
- private handleViewCipherKeyUpEvent = (event: KeyboardEvent) => {
- const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowLeft"]);
- if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
- return;
- }
-
- event.preventDefault();
-
- const currentListItem = event.target.closest(".overlay-actions-list-item") as HTMLElement;
- const cipherContainer = currentListItem.querySelector(".cipher-container") as HTMLElement;
- cipherContainer?.classList.remove("remove-outline");
- if (event.code === "ArrowDown") {
- this.focusNextListItem(currentListItem);
- return;
- }
-
- if (event.code === "ArrowUp") {
- this.focusPreviousListItem(currentListItem);
- return;
- }
-
- const previousSibling = event.target.previousElementSibling as HTMLElement;
- previousSibling?.focus();
- };
-
- /**
- * Builds the icon for a given cipher. Prioritizes the favicon from a given cipher url
- * and the default icon element within the extension. If neither are available, the
- * globe icon is used.
- *
- * @param cipher - The cipher to build the icon for.
- */
- private buildCipherIconElement(cipher: OverlayCipherData) {
- const cipherIcon = globalThis.document.createElement("span");
- cipherIcon.classList.add("cipher-icon");
- cipherIcon.setAttribute("aria-hidden", "true");
-
- if (cipher.icon?.image) {
- try {
- const url = new URL(cipher.icon.image);
- cipherIcon.style.backgroundImage = `url(${url.href})`;
-
- const dummyImageElement = globalThis.document.createElement("img");
- dummyImageElement.src = url.href;
- dummyImageElement.addEventListener("error", () => {
- cipherIcon.style.backgroundImage = "";
- cipherIcon.classList.add("cipher-icon");
- cipherIcon.append(buildSvgDomElement(globeIcon));
- });
- dummyImageElement.remove();
-
- return cipherIcon;
- } catch {
- // Silently default to the globe icon element if the image URL is invalid
- }
- }
-
- if (cipher.icon?.icon) {
- const iconClasses = cipher.icon.icon.split(" ");
- cipherIcon.classList.add("cipher-icon", "bwi", ...iconClasses);
-
- return cipherIcon;
- }
-
- cipherIcon.append(buildSvgDomElement(globeIcon));
- return cipherIcon;
- }
-
- /**
- * Builds the details for a given cipher. Includes the cipher name and username login.
- *
- * @param cipher - The cipher to build the details for.
- */
- private buildCipherDetailsElement(cipher: OverlayCipherData) {
- const cipherNameElement = this.buildCipherNameElement(cipher);
- const cipherUserLoginElement = this.buildCipherUserLoginElement(cipher);
-
- const cipherDetailsElement = globalThis.document.createElement("span");
- cipherDetailsElement.classList.add("cipher-details");
- if (cipherNameElement) {
- cipherDetailsElement.appendChild(cipherNameElement);
- }
- if (cipherUserLoginElement) {
- cipherDetailsElement.appendChild(cipherUserLoginElement);
- }
-
- return cipherDetailsElement;
- }
-
- /**
- * Builds the name element for a given cipher.
- *
- * @param cipher - The cipher to build the name element for.
- */
- private buildCipherNameElement(cipher: OverlayCipherData): HTMLSpanElement | null {
- if (!cipher.name) {
- return null;
- }
-
- const cipherNameElement = globalThis.document.createElement("span");
- cipherNameElement.classList.add("cipher-name");
- cipherNameElement.textContent = cipher.name;
- cipherNameElement.setAttribute("title", cipher.name);
-
- return cipherNameElement;
- }
-
- /**
- * Builds the username login element for a given cipher.
- *
- * @param cipher - The cipher to build the username login element for.
- */
- private buildCipherUserLoginElement(cipher: OverlayCipherData): HTMLSpanElement | null {
- if (!cipher.login?.username) {
- return null;
- }
-
- const cipherUserLoginElement = globalThis.document.createElement("span");
- cipherUserLoginElement.classList.add("cipher-user-login");
- cipherUserLoginElement.textContent = cipher.login.username;
- cipherUserLoginElement.setAttribute("title", cipher.login.username);
-
- return cipherUserLoginElement;
- }
-
- /**
- * Validates whether the overlay list iframe is currently focused.
- * If not focused, will check if the button element is focused.
- */
- private checkOverlayListFocused() {
- if (globalThis.document.hasFocus()) {
- return;
- }
-
- this.postMessageToParent({ command: "checkAutofillOverlayButtonFocused" });
- }
-
- /**
- * Focuses the overlay list iframe. The element that receives focus is
- * determined by the presence of the unlock button, new item button, or
- * the first cipher button.
- */
- private focusOverlayList() {
- this.overlayListContainer.setAttribute("role", "dialog");
- this.overlayListContainer.setAttribute("aria-modal", "true");
-
- const unlockButtonElement = this.overlayListContainer.querySelector(
- "#unlock-button",
- ) as HTMLElement;
- if (unlockButtonElement) {
- unlockButtonElement.focus();
- return;
- }
-
- const newItemButtonElement = this.overlayListContainer.querySelector(
- "#new-item-button",
- ) as HTMLElement;
- if (newItemButtonElement) {
- newItemButtonElement.focus();
- return;
- }
-
- const firstCipherElement = this.overlayListContainer.querySelector(
- ".fill-cipher-button",
- ) as HTMLElement;
- firstCipherElement?.focus();
- }
-
- /**
- * Sets up the global listeners for the overlay list iframe.
- */
- private setupOverlayListGlobalListeners() {
- this.setupGlobalListeners(this.overlayListWindowMessageHandlers);
-
- this.resizeObserver = new ResizeObserver(this.handleResizeObserver);
- }
-
- /**
- * Handles the resize observer event. Facilitates updating the height of the
- * overlay list iframe when the height of the list changes.
- *
- * @param entries - The resize observer entries.
- */
- private handleResizeObserver = (entries: ResizeObserverEntry[]) => {
- for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
- const entry = entries[entryIndex];
- if (entry.target !== this.overlayListContainer) {
- continue;
- }
-
- const { height } = entry.contentRect;
- this.postMessageToParent({
- command: "updateAutofillOverlayListHeight",
- styles: { height: `${height}px` },
- });
- break;
- }
- };
-
- /**
- * Establishes a memoized event handler for a given event.
- *
- * @param eventHandler - The event handler to memoize.
- * @param memoIndex - The memo index to use for the event handler.
- */
- private useEventHandlersMemo = (eventHandler: EventListener, memoIndex: string) => {
- return this.eventHandlersMemo[memoIndex] || (this.eventHandlersMemo[memoIndex] = eventHandler);
- };
-
- /**
- * Focuses the next list item in the overlay list. If the current list item is the last
- * item in the list, the first item is focused.
- *
- * @param currentListItem - The current list item.
- */
- private focusNextListItem(currentListItem: HTMLElement) {
- const nextListItem = currentListItem.nextSibling as HTMLElement;
- const nextSibling = nextListItem?.querySelector(".fill-cipher-button") as HTMLElement;
- if (nextSibling) {
- nextSibling.focus();
- return;
- }
-
- const firstListItem = currentListItem.parentElement?.firstChild as HTMLElement;
- const firstSibling = firstListItem?.querySelector(".fill-cipher-button") as HTMLElement;
- firstSibling?.focus();
- }
-
- /**
- * Focuses the previous list item in the overlay list. If the current list item is the first
- * item in the list, the last item is focused.
- *
- * @param currentListItem - The current list item.
- */
- private focusPreviousListItem(currentListItem: HTMLElement) {
- const previousListItem = currentListItem.previousSibling as HTMLElement;
- const previousSibling = previousListItem?.querySelector(".fill-cipher-button") as HTMLElement;
- if (previousSibling) {
- previousSibling.focus();
- return;
- }
-
- const lastListItem = currentListItem.parentElement?.lastChild as HTMLElement;
- const lastSibling = lastListItem?.querySelector(".fill-cipher-button") as HTMLElement;
- lastSibling?.focus();
- }
-
- /**
- * Focuses the view cipher button relative to the current fill cipher button.
- *
- * @param currentListItem - The current list item.
- * @param currentButtonElement - The current button element.
- */
- private focusViewCipherButton(currentListItem: HTMLElement, currentButtonElement: HTMLElement) {
- const cipherContainer = currentListItem.querySelector(".cipher-container") as HTMLElement;
- cipherContainer.classList.add("remove-outline");
-
- const nextSibling = currentButtonElement.nextElementSibling as HTMLElement;
- nextSibling?.focus();
- }
-}
-
-export default AutofillOverlayList;
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts
deleted file mode 100644
index 5d587bd4293..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum";
-
-import AutofillOverlayList from "./autofill-overlay-list.deprecated";
-
-// FIXME: Remove when updating file. Eslint update
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-require("./legacy-list.scss");
-
-(function () {
- globalThis.customElements.define(AutofillOverlayElement.List, AutofillOverlayList);
-})();
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html b/apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html
deleted file mode 100644
index 3edd26f7a50..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Bitwarden vault
-
-
-
-
-
-
-
-
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss b/apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss
deleted file mode 100644
index e42d53316c6..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss
+++ /dev/null
@@ -1,292 +0,0 @@
-@import "../../../../shared/styles/webfonts";
-@import "../../../../shared/styles/variables";
-@import "../../../../../../../../libs/angular/src/scss/icons";
-
-* {
- box-sizing: border-box;
-}
-
-html {
- font-size: 10px;
-}
-
-body {
- width: 100%;
- padding: 0;
- margin: 0;
-
- @include themify($themes) {
- color: themed("textColor");
- background-color: themed("backgroundColor");
- }
-}
-
-.overlay-list-message {
- font-family: $font-family-sans-serif;
- font-weight: 400;
- font-size: 1.4rem;
- line-height: 1.5;
- width: 100%;
- padding: 0.8rem;
-
- @include themify($themes) {
- color: themed("textColor");
- }
-
- &.no-items {
- font-size: 1.6rem;
- }
-}
-
-.overlay-list-button-container {
- width: 100%;
- padding: 0.2rem;
- background: transparent;
- transition: background-color 0.2s ease-in-out;
- border-top-width: 0.1rem;
- border-top-style: solid;
-
- @include themify($themes) {
- border-top-color: themed("borderColor");
- }
-
- &:hover {
- @include themify($themes) {
- background: themed("backgroundOffsetColor");
- }
- }
-}
-
-.overlay-list-button {
- display: flex;
- align-content: center;
- justify-content: flex-start;
- width: 100%;
- font-family: $font-family-sans-serif;
- font-size: 1.6rem;
- font-weight: 700;
- text-align: left;
- background: transparent;
- border: none;
- padding: 0.7rem;
- margin: 0;
- cursor: pointer;
- border-radius: 0.4rem;
-
- @include themify($themes) {
- color: themed("primaryColor");
- }
-
- &:focus:focus-visible {
- outline-width: 0.2rem;
- outline-style: solid;
-
- @include themify($themes) {
- outline-color: themed("focusOutlineColor");
- }
- }
-
- svg {
- position: relative;
- margin-left: 0.4rem;
- margin-right: 0.8rem;
-
- path {
- @include themify($themes) {
- fill: themed("primaryColor") !important;
- }
- }
- }
-}
-
-.unlock-button {
- svg {
- top: 0.2rem;
- width: 1.6rem;
- height: 1.7rem;
- }
-}
-
-.add-new-item-button {
- svg {
- top: 0.2rem;
- width: 1.7rem;
- height: 1.7rem;
- }
-}
-
-.overlay-actions-list {
- padding: 0;
- margin: 0;
-}
-
-.overlay-actions-list-item {
- transition: background-color 0.2s ease-in-out;
- list-style: none;
- padding: 0.2rem;
-
- &:not(:last-child) {
- border-bottom-width: 0.1rem;
- border-bottom-style: solid;
-
- @include themify($themes) {
- border-bottom-color: themed("borderColor");
- }
- }
-
- &:hover {
- @include themify($themes) {
- background: themed("backgroundOffsetColor");
- }
- }
-
- .cipher-container {
- display: flex;
- align-content: flex-start;
- align-items: center;
- justify-content: flex-start;
- padding: 0.7rem 0.3rem 0.7rem 0.7rem;
- border-radius: 0.4rem;
-
- &:focus-within:not(.remove-outline) {
- outline-width: 0.2rem;
- outline-style: solid;
-
- @include themify($themes) {
- outline-color: themed("focusOutlineColor");
- }
- }
- }
-
- .fill-cipher-button,
- .view-cipher-button {
- padding: 0;
- margin: 0;
- line-height: 0;
- background-color: transparent;
- border: none;
- cursor: pointer;
- }
-
- .fill-cipher-button {
- display: flex;
- align-items: center;
- align-content: center;
- justify-content: flex-start;
- width: calc(100% - 4rem);
- outline: none;
- }
-
- .view-cipher-button {
- flex-shrink: 0;
- width: 4rem;
- height: 4rem;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 0.4rem;
-
- &:focus:focus-visible {
- outline-width: 0.2rem;
- outline-style: solid;
-
- @include themify($themes) {
- outline-color: themed("focusOutlineColor");
- }
- }
-
- svg {
- path {
- @include themify($themes) {
- fill: themed("primaryColor") !important;
- }
- }
- }
- }
-
- .cipher-icon {
- display: flex;
- align-content: center;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- width: 3.2rem;
- height: 3.2rem;
- margin: 0 1rem 0 0;
- line-height: 0;
- background-size: 2.6rem;
- background-position: center;
- background-repeat: no-repeat;
-
- @include themify($themes) {
- color: themed("mutedTextColor");
- }
-
- svg {
- width: 100%;
- height: auto;
- flex-shrink: 0;
-
- path {
- @include themify($themes) {
- fill: themed("primaryColor") !important;
- }
- }
- }
-
- &.bwi {
- font-size: 2.6rem;
-
- &:not(.cipher-icon) {
- @include themify($themes) {
- color: themed("primaryColor");
- }
-
- svg {
- path {
- @include themify($themes) {
- fill: themed("primaryColor") !important;
- }
- }
- }
- }
- }
- }
-
- .cipher-details {
- display: block;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- text-align: left;
- }
-
- .cipher-name,
- .cipher-user-login {
- display: block;
- width: 100%;
- line-height: 1.5;
- font-family: $font-family-sans-serif;
- font-weight: 400;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- cursor: pointer;
- }
-
- .cipher-name {
- font-size: 1.6rem;
-
- @include themify($themes) {
- color: themed("textColor");
- }
- }
-
- .cipher-user-login {
- font-size: 1.4rem;
-
- @include themify($themes) {
- color: themed("mutedTextColor");
- }
- }
-}
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts
deleted file mode 100644
index b541bda0fa9..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-import { mock } from "jest-mock-extended";
-
-import { OverlayButtonWindowMessageHandlers } from "../../abstractions/autofill-overlay-button.deprecated";
-
-import AutofillOverlayPageElementDeprecated from "./autofill-overlay-page-element.deprecated";
-
-describe("AutofillOverlayPageElement", () => {
- globalThis.customElements.define(
- "autofill-overlay-page-element",
- AutofillOverlayPageElementDeprecated,
- );
- let autofillOverlayPageElement: AutofillOverlayPageElementDeprecated;
- const translations = {
- locale: "en",
- buttonPageTitle: "buttonPageTitle",
- listPageTitle: "listPageTitle",
- };
-
- beforeEach(() => {
- jest.spyOn(globalThis.parent, "postMessage");
- jest.spyOn(globalThis, "addEventListener");
- jest.spyOn(globalThis.document, "addEventListener");
- document.body.innerHTML = "";
- autofillOverlayPageElement = document.querySelector("autofill-overlay-page-element");
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("initOverlayPage", () => {
- beforeEach(() => {
- jest.spyOn(globalThis.document.documentElement, "setAttribute");
- jest.spyOn(globalThis.document, "createElement");
- });
-
- it("initializes the button overlay page", () => {
- const linkElement = autofillOverlayPageElement["initOverlayPage"](
- "button",
- "https://jest-testing-website.com",
- translations,
- );
-
- expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith(
- "lang",
- translations.locale,
- );
- expect(globalThis.document.head.title).toEqual(translations.buttonPageTitle);
- expect(globalThis.document.createElement).toHaveBeenCalledWith("link");
- expect(linkElement.getAttribute("rel")).toEqual("stylesheet");
- expect(linkElement.getAttribute("href")).toEqual("https://jest-testing-website.com");
- });
- });
-
- describe("postMessageToParent", () => {
- it("skips posting a message to the parent if the message origin in not set", () => {
- autofillOverlayPageElement["postMessageToParent"]({ command: "test" });
-
- expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
- });
-
- it("posts a message to the parent", () => {
- autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
- autofillOverlayPageElement["postMessageToParent"]({ command: "test" });
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "test" },
- "https://jest-testing-website.com",
- );
- });
- });
-
- describe("getTranslation", () => {
- it("returns an empty value if the translation doesn't exist in the translations object", () => {
- autofillOverlayPageElement["translations"] = translations;
-
- expect(autofillOverlayPageElement["getTranslation"]("test")).toEqual("");
- });
- });
-
- describe("global event listeners", () => {
- it("sets up global event listeners", () => {
- const handleWindowMessageSpy = jest.spyOn(
- autofillOverlayPageElement as any,
- "handleWindowMessage",
- );
- const handleWindowBlurEventSpy = jest.spyOn(
- autofillOverlayPageElement as any,
- "handleWindowBlurEvent",
- );
- const handleDocumentKeyDownEventSpy = jest.spyOn(
- autofillOverlayPageElement as any,
- "handleDocumentKeyDownEvent",
- );
- autofillOverlayPageElement["setupGlobalListeners"](
- mock(),
- );
-
- expect(globalThis.addEventListener).toHaveBeenCalledWith("message", handleWindowMessageSpy);
- expect(globalThis.addEventListener).toHaveBeenCalledWith("blur", handleWindowBlurEventSpy);
- expect(globalThis.document.addEventListener).toHaveBeenCalledWith(
- "keydown",
- handleDocumentKeyDownEventSpy,
- );
- });
-
- it("sets the message origin when handling the first passed window message", () => {
- const initAutofillOverlayButtonSpy = jest.fn();
- autofillOverlayPageElement["setupGlobalListeners"](
- mock({
- initAutofillOverlayButton: initAutofillOverlayButtonSpy,
- }),
- );
-
- globalThis.dispatchEvent(
- new MessageEvent("message", {
- data: { command: "initAutofillOverlayButton" },
- origin: "https://jest-testing-website.com",
- }),
- );
-
- expect(autofillOverlayPageElement["messageOrigin"]).toEqual(
- "https://jest-testing-website.com",
- );
- });
-
- it("handles window messages that are part of the passed windowMessageHandlers object", () => {
- const initAutofillOverlayButtonSpy = jest.fn();
- autofillOverlayPageElement["setupGlobalListeners"](
- mock({
- initAutofillOverlayButton: initAutofillOverlayButtonSpy,
- }),
- );
- const data = { command: "initAutofillOverlayButton" };
-
- globalThis.dispatchEvent(new MessageEvent("message", { data }));
-
- expect(initAutofillOverlayButtonSpy).toHaveBeenCalledWith({ message: data });
- });
-
- it("skips attempting to handle window messages that are not part of the passed windowMessageHandlers object", () => {
- const initAutofillOverlayButtonSpy = jest.fn();
- autofillOverlayPageElement["setupGlobalListeners"](
- mock({
- initAutofillOverlayButton: initAutofillOverlayButtonSpy,
- }),
- );
-
- globalThis.dispatchEvent(new MessageEvent("message", { data: { command: "test" } }));
-
- expect(initAutofillOverlayButtonSpy).not.toHaveBeenCalled();
- });
-
- it("posts a message to the parent when the window is blurred", () => {
- autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
- autofillOverlayPageElement["setupGlobalListeners"](
- mock(),
- );
-
- globalThis.dispatchEvent(new Event("blur"));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "overlayPageBlurred" },
- "https://jest-testing-website.com",
- );
- });
-
- it("skips redirecting keyboard focus when a KeyDown event triggers and the key is not a `Tab` or `Escape` key", () => {
- autofillOverlayPageElement["setupGlobalListeners"](
- mock(),
- );
-
- globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "test" }));
-
- expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
- });
-
- it("redirects the overlay focus out to the previous element on KeyDown of the `Tab+Shift` keys", () => {
- autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
- autofillOverlayPageElement["setupGlobalListeners"](
- mock(),
- );
-
- globalThis.document.dispatchEvent(
- new KeyboardEvent("keydown", { code: "Tab", shiftKey: true }),
- );
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "redirectOverlayFocusOut", direction: "previous" },
- "https://jest-testing-website.com",
- );
- });
-
- it("redirects the overlay focus out to the next element on KeyDown of the `Tab` key", () => {
- autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
- autofillOverlayPageElement["setupGlobalListeners"](
- mock(),
- );
-
- globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "redirectOverlayFocusOut", direction: "next" },
- "https://jest-testing-website.com",
- );
- });
-
- it("redirects the overlay focus out to the current element on KeyDown of the `Escape` key", () => {
- autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
- autofillOverlayPageElement["setupGlobalListeners"](
- mock(),
- );
-
- globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
-
- expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
- { command: "redirectOverlayFocusOut", direction: "current" },
- "https://jest-testing-website.com",
- );
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts
deleted file mode 100644
index c388c9c307c..00000000000
--- a/apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { EVENTS } from "@bitwarden/common/autofill/constants";
-
-import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum";
-import {
- AutofillOverlayPageElementWindowMessage,
- WindowMessageHandlers,
-} from "../../abstractions/autofill-overlay-page-element.deprecated";
-
-class AutofillOverlayPageElement extends HTMLElement {
- protected shadowDom: ShadowRoot;
- protected messageOrigin: string;
- protected translations: Record;
- protected windowMessageHandlers: WindowMessageHandlers;
-
- constructor() {
- super();
-
- this.shadowDom = this.attachShadow({ mode: "closed" });
- }
-
- /**
- * Initializes the overlay page element. Facilitates ensuring that the page
- * is set up with the expected styles and translations.
- *
- * @param elementName - The name of the element, e.g. "button" or "list"
- * @param styleSheetUrl - The URL of the stylesheet to apply to the page
- * @param translations - The translations to apply to the page
- */
- protected initOverlayPage(
- elementName: "button" | "list",
- styleSheetUrl: string,
- translations: Record,
- ): HTMLLinkElement {
- this.translations = translations;
- globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
- globalThis.document.head.title = this.getTranslation(`${elementName}PageTitle`);
-
- this.shadowDom.innerHTML = "";
- const linkElement = globalThis.document.createElement("link");
- linkElement.setAttribute("rel", "stylesheet");
- linkElement.setAttribute("href", styleSheetUrl);
-
- return linkElement;
- }
-
- /**
- * Posts a window message to the parent window.
- *
- * @param message - The message to post
- */
- protected postMessageToParent(message: AutofillOverlayPageElementWindowMessage) {
- if (!this.messageOrigin) {
- return;
- }
-
- globalThis.parent.postMessage(message, this.messageOrigin);
- }
-
- /**
- * Gets a translation from the translations object.
- *
- * @param key
- * @protected
- */
- protected getTranslation(key: string): string {
- return this.translations[key] || "";
- }
-
- /**
- * Sets up global listeners for the window message, window blur, and
- * document keydown events.
- *
- * @param windowMessageHandlers - The window message handlers to use
- */
- protected setupGlobalListeners(windowMessageHandlers: WindowMessageHandlers) {
- this.windowMessageHandlers = windowMessageHandlers;
-
- globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessage);
- globalThis.addEventListener(EVENTS.BLUR, this.handleWindowBlurEvent);
- globalThis.document.addEventListener(EVENTS.KEYDOWN, this.handleDocumentKeyDownEvent);
- }
-
- /**
- * Handles window messages from the parent window.
- *
- * @param event - The window message event
- */
- private handleWindowMessage = (event: MessageEvent) => {
- if (!this.windowMessageHandlers) {
- return;
- }
-
- if (!this.messageOrigin) {
- this.messageOrigin = event.origin;
- }
-
- if (event.origin !== this.messageOrigin) {
- return;
- }
-
- const message = event?.data;
- const handler = this.windowMessageHandlers[message?.command];
- if (!handler) {
- return;
- }
-
- handler({ message });
- };
-
- /**
- * Handles the window blur event.
- */
- private handleWindowBlurEvent = () => {
- this.postMessageToParent({ command: "overlayPageBlurred" });
- };
-
- /**
- * Handles the document keydown event. Facilitates redirecting the
- * user focus in the right direction out of the overlay. Also facilitates
- * closing the overlay when the user presses the Escape key.
- *
- * @param event - The document keydown event
- */
- private handleDocumentKeyDownEvent = (event: KeyboardEvent) => {
- const listenedForKeys = new Set(["Tab", "Escape"]);
- if (!listenedForKeys.has(event.code)) {
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- if (event.code === "Tab") {
- this.redirectOverlayFocusOutMessage(
- event.shiftKey ? RedirectFocusDirection.Previous : RedirectFocusDirection.Next,
- );
- return;
- }
-
- this.redirectOverlayFocusOutMessage(RedirectFocusDirection.Current);
- };
-
- /**
- * Redirects the overlay focus out to the previous element on KeyDown of the `Tab+Shift` keys.
- * Redirects the overlay focus out to the next element on KeyDown of the `Tab` key.
- * Redirects the overlay focus out to the current element on KeyDown of the `Escape` key.
- *
- * @param direction - The direction to redirect the focus out
- */
- private redirectOverlayFocusOutMessage(direction: string) {
- this.postMessageToParent({ command: "redirectOverlayFocusOut", direction });
- }
-}
-
-export default AutofillOverlayPageElement;
diff --git a/apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts b/apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts
deleted file mode 100644
index 73acb430d71..00000000000
--- a/apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-
-import AutofillField from "../../../models/autofill-field";
-import AutofillPageDetails from "../../../models/autofill-page-details";
-import { AutofillOverlayContentService } from "../../../services/abstractions/autofill-overlay-content.service";
-import { ElementWithOpId, FormFieldElement } from "../../../types";
-
-type OpenAutofillOverlayOptions = {
- isFocusingFieldElement?: boolean;
- isOpeningFullOverlay?: boolean;
- authStatus?: AuthenticationStatus;
-};
-
-interface LegacyAutofillOverlayContentService extends AutofillOverlayContentService {
- isFieldCurrentlyFocused: boolean;
- isCurrentlyFilling: boolean;
- isOverlayCiphersPopulated: boolean;
- pageDetailsUpdateRequired: boolean;
- autofillOverlayVisibility: number;
- init(): void;
- setupAutofillOverlayListenerOnField(
- autofillFieldElement: ElementWithOpId,
- autofillFieldData: AutofillField,
- pageDetails: AutofillPageDetails,
- ): Promise;
- openAutofillOverlay(options: OpenAutofillOverlayOptions): void;
- removeAutofillOverlay(): void;
- removeAutofillOverlayButton(): void;
- removeAutofillOverlayList(): void;
- addNewVaultItem(): void;
- redirectOverlayFocusOut(direction: "previous" | "next"): void;
- focusMostRecentOverlayField(): void;
- blurMostRecentOverlayField(): void;
- destroy(): void;
-}
-
-export { OpenAutofillOverlayOptions, LegacyAutofillOverlayContentService };
diff --git a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts
deleted file mode 100644
index 28307834599..00000000000
--- a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts
+++ /dev/null
@@ -1,1743 +0,0 @@
-import { mock } from "jest-mock-extended";
-
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
-
-import { AutofillOverlayElement, RedirectFocusDirection } from "../../enums/autofill-overlay.enum";
-import AutofillField from "../../models/autofill-field";
-import AutofillForm from "../../models/autofill-form";
-import AutofillPageDetails from "../../models/autofill-page-details";
-import { AutoFillConstants } from "../../services/autofill-constants";
-import { createAutofillFieldMock } from "../../spec/autofill-mocks";
-import { flushPromises } from "../../spec/testing-utils";
-import { ElementWithOpId, FormFieldElement } from "../../types";
-
-import AutofillOverlayContentServiceDeprecated from "./autofill-overlay-content.service.deprecated";
-
-function createMutationRecordMock(customFields = {}): MutationRecord {
- return {
- addedNodes: mock(),
- attributeName: "default-attributeName",
- attributeNamespace: "default-attributeNamespace",
- nextSibling: null,
- oldValue: "default-oldValue",
- previousSibling: null,
- removedNodes: mock(),
- target: null,
- type: "attributes",
- ...customFields,
- };
-}
-
-const defaultWindowReadyState = document.readyState;
-const defaultDocumentVisibilityState = document.visibilityState;
-describe("AutofillOverlayContentService", () => {
- let autofillOverlayContentService: AutofillOverlayContentServiceDeprecated;
- let sendExtensionMessageSpy: jest.SpyInstance;
-
- beforeEach(() => {
- autofillOverlayContentService = new AutofillOverlayContentServiceDeprecated();
- sendExtensionMessageSpy = jest
- .spyOn(autofillOverlayContentService as any, "sendExtensionMessage")
- .mockResolvedValue(undefined);
- Object.defineProperty(document, "readyState", {
- value: defaultWindowReadyState,
- writable: true,
- });
- Object.defineProperty(document, "visibilityState", {
- value: defaultDocumentVisibilityState,
- writable: true,
- });
- Object.defineProperty(document, "activeElement", {
- value: null,
- writable: true,
- });
- Object.defineProperty(window, "innerHeight", {
- value: 1080,
- writable: true,
- });
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("init", () => {
- let setupGlobalEventListenersSpy: jest.SpyInstance;
- let setupMutationObserverSpy: jest.SpyInstance;
-
- beforeEach(() => {
- jest.spyOn(document, "addEventListener");
- jest.spyOn(window, "addEventListener");
- setupGlobalEventListenersSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "setupGlobalEventListeners",
- );
- setupMutationObserverSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "setupMutationObserver",
- );
- });
-
- it("sets up a DOMContentLoaded event listener that triggers setting up the mutation observers", () => {
- Object.defineProperty(document, "readyState", {
- value: "loading",
- writable: true,
- });
-
- autofillOverlayContentService.init();
-
- expect(document.addEventListener).toHaveBeenCalledWith(
- "DOMContentLoaded",
- setupGlobalEventListenersSpy,
- );
- expect(setupGlobalEventListenersSpy).not.toHaveBeenCalled();
- });
-
- it("sets up a visibility change listener for the DOM", () => {
- const handleVisibilityChangeEventSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleVisibilityChangeEvent",
- );
-
- autofillOverlayContentService.init();
-
- expect(document.addEventListener).toHaveBeenCalledWith(
- "visibilitychange",
- handleVisibilityChangeEventSpy,
- );
- });
-
- it("sets up a focus out listener for the window", () => {
- const handleFormFieldBlurEventSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleFormFieldBlurEvent",
- );
-
- autofillOverlayContentService.init();
-
- expect(window.addEventListener).toHaveBeenCalledWith("focusout", handleFormFieldBlurEventSpy);
- });
-
- it("sets up mutation observers for the body element", () => {
- jest
- .spyOn(globalThis, "MutationObserver")
- .mockImplementation(() => mock({ observe: jest.fn() }));
- const handleOverlayElementMutationObserverUpdateSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleOverlayElementMutationObserverUpdate",
- );
- const handleBodyElementMutationObserverUpdateSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleBodyElementMutationObserverUpdate",
- );
- autofillOverlayContentService.init();
-
- expect(setupMutationObserverSpy).toHaveBeenCalledTimes(1);
- expect(globalThis.MutationObserver).toHaveBeenNthCalledWith(
- 1,
- handleOverlayElementMutationObserverUpdateSpy,
- );
- expect(globalThis.MutationObserver).toHaveBeenNthCalledWith(
- 2,
- handleBodyElementMutationObserverUpdateSpy,
- );
- });
- });
-
- describe("setupAutofillOverlayListenerOnField", () => {
- let autofillFieldElement: ElementWithOpId;
- let autofillFieldData: AutofillField;
- let pageDetailsMock: AutofillPageDetails;
-
- beforeEach(() => {
- document.body.innerHTML = `
-
- `;
-
- autofillFieldElement = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- autofillFieldElement.opid = "op-1";
- jest.spyOn(autofillFieldElement, "addEventListener");
- autofillFieldData = createAutofillFieldMock({
- opid: "username-field",
- form: "validFormId",
- placeholder: "username",
- elementNumber: 1,
- });
- const passwordFieldData = createAutofillFieldMock({
- opid: "password-field",
- form: "validFormId",
- elementNumber: 2,
- autocompleteType: "current-password",
- type: "password",
- });
- pageDetailsMock = mock({
- forms: { validFormId: mock() },
- fields: [autofillFieldData, passwordFieldData],
- });
- });
-
- describe("skips setup for ignored form fields", () => {
- beforeEach(() => {
- autofillFieldData = mock({
- type: "text",
- htmlName: "username",
- htmlID: "username",
- placeholder: "username",
- });
- });
-
- it("ignores fields that are readonly", async () => {
- autofillFieldData.readonly = true;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that contain a disabled attribute", async () => {
- autofillFieldData.disabled = true;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that are not viewable", async () => {
- autofillFieldData.viewable = false;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that are part of the ExcludedOverlayTypes", () => {
- AutoFillConstants.ExcludedInlineMenuTypes.forEach(async (excludedType) => {
- autofillFieldData.type = excludedType;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
- });
-
- it("ignores fields that contain the keyword `search`", async () => {
- autofillFieldData.placeholder = "search";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that contain the keyword `captcha` ", async () => {
- autofillFieldData.placeholder = "captcha";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that do not appear as a login field", async () => {
- autofillFieldData.placeholder = "not-a-login-field";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
- });
-
- it("skips setup on fields that have been previously set up", async () => {
- autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- describe("identifies the overlay visibility setting", () => {
- it("defaults the overlay visibility setting to `OnFieldFocus` if a value is not set", async () => {
- sendExtensionMessageSpy.mockResolvedValueOnce(undefined);
- autofillOverlayContentService["autofillOverlayVisibility"] = undefined;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("getAutofillOverlayVisibility");
- expect(autofillOverlayContentService["autofillOverlayVisibility"]).toEqual(
- AutofillOverlayVisibility.OnFieldFocus,
- );
- });
-
- it("sets the overlay visibility setting to the value returned from the background script", async () => {
- sendExtensionMessageSpy.mockResolvedValueOnce(AutofillOverlayVisibility.OnFieldFocus);
- autofillOverlayContentService["autofillOverlayVisibility"] = undefined;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillOverlayContentService["autofillOverlayVisibility"]).toEqual(
- AutofillOverlayVisibility.OnFieldFocus,
- );
- });
- });
-
- describe("sets up form field element listeners", () => {
- it("removes all cached event listeners from the form field element", async () => {
- jest.spyOn(autofillFieldElement, "removeEventListener");
- const inputHandler = jest.fn();
- const clickHandler = jest.fn();
- const focusHandler = jest.fn();
- autofillOverlayContentService["eventHandlersMemo"] = {
- "op-1-username-field-input-handler": inputHandler,
- "op-1-username-field-click-handler": clickHandler,
- "op-1-username-field-focus-handler": focusHandler,
- };
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillFieldElement.removeEventListener).toHaveBeenNthCalledWith(
- 1,
- "input",
- inputHandler,
- );
- expect(autofillFieldElement.removeEventListener).toHaveBeenNthCalledWith(
- 2,
- "click",
- clickHandler,
- );
- expect(autofillFieldElement.removeEventListener).toHaveBeenNthCalledWith(
- 3,
- "focus",
- focusHandler,
- );
- });
-
- describe("form field blur event listener", () => {
- beforeEach(async () => {
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- });
-
- it("updates the isFieldCurrentlyFocused value to false", async () => {
- autofillOverlayContentService["isFieldCurrentlyFocused"] = true;
-
- autofillFieldElement.dispatchEvent(new Event("blur"));
-
- expect(autofillOverlayContentService["isFieldCurrentlyFocused"]).toEqual(false);
- });
-
- it("sends a message to the background to check if the overlay is focused", () => {
- autofillFieldElement.dispatchEvent(new Event("blur"));
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("checkAutofillOverlayFocused");
- });
- });
-
- describe("form field keyup event listener", () => {
- beforeEach(async () => {
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- jest.spyOn(globalThis.customElements, "define").mockImplementation();
- });
-
- it("removes the autofill overlay when the `Escape` key is pressed", () => {
- jest.spyOn(autofillOverlayContentService as any, "removeAutofillOverlay");
-
- autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Escape" }));
-
- expect(autofillOverlayContentService.removeAutofillOverlay).toHaveBeenCalled();
- });
-
- it("repositions the overlay if autofill is not currently filling when the `Enter` key is pressed", () => {
- const handleOverlayRepositionEventSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleOverlayRepositionEvent",
- );
- autofillOverlayContentService["isCurrentlyFilling"] = false;
-
- autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
-
- expect(handleOverlayRepositionEventSpy).toHaveBeenCalled();
- });
-
- it("skips repositioning the overlay if autofill is currently filling when the `Enter` key is pressed", () => {
- const handleOverlayRepositionEventSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleOverlayRepositionEvent",
- );
- autofillOverlayContentService["isCurrentlyFilling"] = true;
-
- autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
-
- expect(handleOverlayRepositionEventSpy).not.toHaveBeenCalled();
- });
-
- it("opens the overlay list and focuses it after a delay if it is not visible when the `ArrowDown` key is pressed", async () => {
- jest.useFakeTimers();
- const updateMostRecentlyFocusedFieldSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "updateMostRecentlyFocusedField",
- );
- const openAutofillOverlaySpy = jest.spyOn(
- autofillOverlayContentService as any,
- "openAutofillOverlay",
- );
- autofillOverlayContentService["isOverlayListVisible"] = false;
-
- autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
- await flushPromises();
-
- expect(updateMostRecentlyFocusedFieldSpy).toHaveBeenCalledWith(autofillFieldElement);
- expect(openAutofillOverlaySpy).toHaveBeenCalledWith({ isOpeningFullOverlay: true });
- expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("focusAutofillOverlayList");
-
- jest.advanceTimersByTime(150);
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("focusAutofillOverlayList");
- });
-
- it("focuses the overlay list when the `ArrowDown` key is pressed", () => {
- autofillOverlayContentService["isOverlayListVisible"] = true;
-
- autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("focusAutofillOverlayList");
- });
- });
-
- describe("form field input change event listener", () => {
- beforeEach(() => {
- jest.spyOn(globalThis.customElements, "define").mockImplementation();
- });
-
- it("ignores span elements that trigger the listener", async () => {
- const spanAutofillFieldElement = document.createElement(
- "span",
- ) as ElementWithOpId;
- jest.spyOn(autofillOverlayContentService as any, "storeModifiedFormElement");
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- spanAutofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- spanAutofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(autofillOverlayContentService["storeModifiedFormElement"]).not.toHaveBeenCalled();
- });
-
- it("stores the field as a user filled field if the form field data indicates that it is for a username", async () => {
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(autofillOverlayContentService["userFilledFields"].username).toEqual(
- autofillFieldElement,
- );
- });
-
- it("stores the field as a user filled field if the form field is of type password", async () => {
- const passwordFieldElement = document.getElementById(
- "password-field",
- ) as ElementWithOpId;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- passwordFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- passwordFieldElement.dispatchEvent(new Event("input"));
-
- expect(autofillOverlayContentService["userFilledFields"].password).toEqual(
- passwordFieldElement,
- );
- });
-
- it("removes the overlay if the form field element has a value and the user is not authed", async () => {
- jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(false);
- const removeAutofillOverlayListSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "removeAutofillOverlayList",
- );
- (autofillFieldElement as HTMLInputElement).value = "test";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(removeAutofillOverlayListSpy).toHaveBeenCalled();
- });
-
- it("removes the overlay if the form field element has a value and the overlay ciphers are populated", async () => {
- jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
- autofillOverlayContentService["isOverlayCiphersPopulated"] = true;
- const removeAutofillOverlayListSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "removeAutofillOverlayList",
- );
- (autofillFieldElement as HTMLInputElement).value = "test";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(removeAutofillOverlayListSpy).toHaveBeenCalled();
- });
-
- it("opens the autofill overlay if the form field is empty", async () => {
- jest.spyOn(autofillOverlayContentService as any, "openAutofillOverlay");
- (autofillFieldElement as HTMLInputElement).value = "";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(autofillOverlayContentService["openAutofillOverlay"]).toHaveBeenCalled();
- });
-
- it("opens the autofill overlay if the form field is empty and the user is authed", async () => {
- jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
- jest.spyOn(autofillOverlayContentService as any, "openAutofillOverlay");
- (autofillFieldElement as HTMLInputElement).value = "";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(autofillOverlayContentService["openAutofillOverlay"]).toHaveBeenCalled();
- });
-
- it("opens the autofill overlay if the form field is empty and the overlay ciphers are not populated", async () => {
- jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(false);
- autofillOverlayContentService["isOverlayCiphersPopulated"] = false;
- jest.spyOn(autofillOverlayContentService as any, "openAutofillOverlay");
- (autofillFieldElement as HTMLInputElement).value = "";
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillFieldElement.dispatchEvent(new Event("input"));
-
- expect(autofillOverlayContentService["openAutofillOverlay"]).toHaveBeenCalled();
- });
- });
-
- describe("form field click event listener", () => {
- beforeEach(async () => {
- jest
- .spyOn(autofillOverlayContentService as any, "triggerFormFieldFocusedAction")
- .mockImplementation();
- autofillOverlayContentService["isOverlayListVisible"] = false;
- autofillOverlayContentService["isOverlayListVisible"] = false;
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- });
-
- it("triggers the field focused handler if the overlay is not visible", async () => {
- autofillFieldElement.dispatchEvent(new Event("click"));
-
- expect(autofillOverlayContentService["triggerFormFieldFocusedAction"]).toHaveBeenCalled();
- });
-
- it("skips triggering the field focused handler if the overlay list is visible", () => {
- autofillOverlayContentService["isOverlayListVisible"] = true;
-
- autofillFieldElement.dispatchEvent(new Event("click"));
-
- expect(
- autofillOverlayContentService["triggerFormFieldFocusedAction"],
- ).not.toHaveBeenCalled();
- });
-
- it("skips triggering the field focused handler if the overlay button is visible", () => {
- autofillOverlayContentService["isOverlayButtonVisible"] = true;
-
- autofillFieldElement.dispatchEvent(new Event("click"));
-
- expect(
- autofillOverlayContentService["triggerFormFieldFocusedAction"],
- ).not.toHaveBeenCalled();
- });
- });
-
- describe("form field focus event listener", () => {
- let updateMostRecentlyFocusedFieldSpy: jest.SpyInstance;
-
- beforeEach(() => {
- jest.spyOn(globalThis.customElements, "define").mockImplementation();
- updateMostRecentlyFocusedFieldSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "updateMostRecentlyFocusedField",
- );
- autofillOverlayContentService["isCurrentlyFilling"] = false;
- });
-
- it("skips triggering the handler logic if autofill is currently filling", async () => {
- autofillOverlayContentService["isCurrentlyFilling"] = true;
- autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnFieldFocus;
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
-
- expect(updateMostRecentlyFocusedFieldSpy).not.toHaveBeenCalled();
- });
-
- it("updates the most recently focused field", async () => {
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
-
- expect(updateMostRecentlyFocusedFieldSpy).toHaveBeenCalledWith(autofillFieldElement);
- expect(autofillOverlayContentService["mostRecentlyFocusedField"]).toEqual(
- autofillFieldElement,
- );
- });
-
- it("removes the overlay list if the autofill visibility is set to onClick", async () => {
- autofillOverlayContentService["overlayListElement"] = document.createElement("div");
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnButtonClick;
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
- await flushPromises();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
- overlayElement: AutofillOverlayElement.List,
- });
- });
-
- it("removes the overlay list if the form element has a value and the focused field is newly focused", async () => {
- autofillOverlayContentService["overlayListElement"] = document.createElement("div");
- autofillOverlayContentService["mostRecentlyFocusedField"] = document.createElement(
- "input",
- ) as ElementWithOpId;
- (autofillFieldElement as HTMLInputElement).value = "test";
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
- await flushPromises();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
- overlayElement: AutofillOverlayElement.List,
- });
- });
-
- it("opens the autofill overlay if the form element has no value", async () => {
- autofillOverlayContentService["overlayListElement"] = document.createElement("div");
- (autofillFieldElement as HTMLInputElement).value = "";
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnFieldFocus;
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
- await flushPromises();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillOverlay");
- });
-
- it("opens the autofill overlay if the overlay ciphers are not populated and the user is authed", async () => {
- autofillOverlayContentService["overlayListElement"] = document.createElement("div");
- (autofillFieldElement as HTMLInputElement).value = "";
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnFieldFocus;
- jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
- await flushPromises();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillOverlay");
- });
-
- it("updates the overlay button position if the focus event is not opening the overlay", async () => {
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnFieldFocus;
- (autofillFieldElement as HTMLInputElement).value = "test";
- autofillOverlayContentService["isOverlayCiphersPopulated"] = true;
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- autofillFieldElement.dispatchEvent(new Event("focus"));
- await flushPromises();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.Button,
- });
- });
- });
- });
-
- it("triggers the form field focused handler if the current active element in the document is the passed form field", async () => {
- const documentRoot = autofillFieldElement.getRootNode() as Document;
- Object.defineProperty(documentRoot, "activeElement", {
- value: autofillFieldElement,
- writable: true,
- });
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillOverlay");
- expect(autofillOverlayContentService["mostRecentlyFocusedField"]).toEqual(
- autofillFieldElement,
- );
- });
-
- it("sets the most recently focused field to the passed form field element if the value is not set", async () => {
- autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
-
- await autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
-
- expect(autofillOverlayContentService["mostRecentlyFocusedField"]).toEqual(
- autofillFieldElement,
- );
- });
- });
-
- describe("openAutofillOverlay", () => {
- let autofillFieldElement: ElementWithOpId;
-
- beforeEach(() => {
- document.body.innerHTML = `
-
- `;
-
- autofillFieldElement = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- autofillFieldElement.opid = "op-1";
- autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
- });
-
- it("skips opening the overlay if a field has not been recently focused", () => {
- autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
-
- autofillOverlayContentService["openAutofillOverlay"]();
-
- expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
- });
-
- it("focuses the most recent overlay field if the field is not focused", () => {
- jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
- Object.defineProperty(document, "activeElement", {
- value: document.createElement("div"),
- writable: true,
- });
- const focusMostRecentOverlayFieldSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "focusMostRecentOverlayField",
- );
-
- autofillOverlayContentService["openAutofillOverlay"]({ isFocusingFieldElement: true });
-
- expect(focusMostRecentOverlayFieldSpy).toHaveBeenCalled();
- });
-
- it("skips focusing the most recent overlay field if the field is already focused", () => {
- jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
- Object.defineProperty(document, "activeElement", {
- value: autofillFieldElement,
- writable: true,
- });
- const focusMostRecentOverlayFieldSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "focusMostRecentOverlayField",
- );
-
- autofillOverlayContentService["openAutofillOverlay"]({ isFocusingFieldElement: true });
-
- expect(focusMostRecentOverlayFieldSpy).not.toHaveBeenCalled();
- });
-
- it("stores the user's auth status", () => {
- autofillOverlayContentService["authStatus"] = undefined;
-
- autofillOverlayContentService["openAutofillOverlay"]({
- authStatus: AuthenticationStatus.Unlocked,
- });
-
- expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked);
- });
-
- it("opens both autofill overlay elements", () => {
- autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
-
- autofillOverlayContentService["openAutofillOverlay"]();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.Button,
- });
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.List,
- });
- });
-
- it("opens the autofill overlay button only if overlay visibility is set for onButtonClick", () => {
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnButtonClick;
-
- autofillOverlayContentService["openAutofillOverlay"]({ isOpeningFullOverlay: false });
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.Button,
- });
- expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.List,
- });
- });
-
- it("overrides the onButtonClick visibility setting to open both overlay elements", () => {
- autofillOverlayContentService["autofillOverlayVisibility"] =
- AutofillOverlayVisibility.OnButtonClick;
-
- autofillOverlayContentService["openAutofillOverlay"]({ isOpeningFullOverlay: true });
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.Button,
- });
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.List,
- });
- });
-
- it("sends an extension message requesting an re-collection of page details if they need to update", () => {
- jest.spyOn(autofillOverlayContentService as any, "sendExtensionMessage");
- autofillOverlayContentService.pageDetailsUpdateRequired = true;
-
- autofillOverlayContentService["openAutofillOverlay"]();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", {
- sender: "autofillOverlayContentService",
- });
- });
-
- it("builds the overlay elements as custom web components if the user's browser is not Firefox", () => {
- let namesIndex = 0;
- const customNames = ["op-autofill-overlay-button", "op-autofill-overlay-list"];
-
- jest
- .spyOn(autofillOverlayContentService as any, "generateRandomCustomElementName")
- .mockImplementation(() => {
- if (namesIndex > 1) {
- return "";
- }
- const customName = customNames[namesIndex];
- namesIndex++;
-
- return customName;
- });
- autofillOverlayContentService["isFirefoxBrowser"] = false;
-
- autofillOverlayContentService.openAutofillOverlay();
-
- expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLElement);
- expect(autofillOverlayContentService["overlayButtonElement"].tagName).toEqual(
- customNames[0].toUpperCase(),
- );
- expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLElement);
- expect(autofillOverlayContentService["overlayListElement"].tagName).toEqual(
- customNames[1].toUpperCase(),
- );
- });
-
- it("builds the overlay elements as `div` elements if the user's browser is Firefox", () => {
- autofillOverlayContentService["isFirefoxBrowser"] = true;
-
- autofillOverlayContentService.openAutofillOverlay();
-
- expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLDivElement);
- expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLDivElement);
- });
- });
-
- describe("focusMostRecentOverlayField", () => {
- it("focuses the most recently focused overlay field", () => {
- const mostRecentlyFocusedField = document.createElement(
- "input",
- ) as ElementWithOpId;
- autofillOverlayContentService["mostRecentlyFocusedField"] = mostRecentlyFocusedField;
- jest.spyOn(mostRecentlyFocusedField, "focus");
-
- autofillOverlayContentService["focusMostRecentOverlayField"]();
-
- expect(mostRecentlyFocusedField.focus).toHaveBeenCalled();
- });
- });
-
- describe("blurMostRecentOverlayField", () => {
- it("removes focus from the most recently focused overlay field", () => {
- const mostRecentlyFocusedField = document.createElement(
- "input",
- ) as ElementWithOpId;
- autofillOverlayContentService["mostRecentlyFocusedField"] = mostRecentlyFocusedField;
- jest.spyOn(mostRecentlyFocusedField, "blur");
-
- autofillOverlayContentService["blurMostRecentOverlayField"]();
-
- expect(mostRecentlyFocusedField.blur).toHaveBeenCalled();
- });
- });
-
- describe("removeAutofillOverlay", () => {
- it("disconnects the body's mutation observer", () => {
- const bodyMutationObserver = mock();
- autofillOverlayContentService["bodyElementMutationObserver"] = bodyMutationObserver;
-
- autofillOverlayContentService.removeAutofillOverlay();
-
- expect(bodyMutationObserver.disconnect).toHaveBeenCalled();
- });
- });
-
- describe("removeAutofillOverlayButton", () => {
- beforeEach(() => {
- document.body.innerHTML = ``;
- autofillOverlayContentService["overlayButtonElement"] = document.querySelector(
- ".overlay-button",
- ) as HTMLElement;
- });
-
- it("removes the overlay button from the DOM", () => {
- const overlayButtonElement = document.querySelector(".overlay-button") as HTMLElement;
- autofillOverlayContentService["isOverlayButtonVisible"] = true;
-
- autofillOverlayContentService.removeAutofillOverlay();
-
- expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(false);
- expect(document.body.contains(overlayButtonElement)).toEqual(false);
- });
-
- it("sends a message to the background indicating that the overlay button has been closed", () => {
- autofillOverlayContentService.removeAutofillOverlay();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
- overlayElement: AutofillOverlayElement.Button,
- });
- });
-
- it("removes the overlay reposition event listeners", () => {
- jest.spyOn(globalThis.document.body, "removeEventListener");
- jest.spyOn(globalThis, "removeEventListener");
- const handleOverlayRepositionEventSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "handleOverlayRepositionEvent",
- );
-
- autofillOverlayContentService.removeAutofillOverlay();
-
- expect(globalThis.removeEventListener).toHaveBeenCalledWith(
- EVENTS.SCROLL,
- handleOverlayRepositionEventSpy,
- {
- capture: true,
- },
- );
- expect(globalThis.removeEventListener).toHaveBeenCalledWith(
- EVENTS.RESIZE,
- handleOverlayRepositionEventSpy,
- );
- });
- });
-
- describe("removeAutofillOverlayList", () => {
- beforeEach(() => {
- document.body.innerHTML = ``;
- autofillOverlayContentService["overlayListElement"] = document.querySelector(
- ".overlay-list",
- ) as HTMLElement;
- });
-
- it("removes the overlay list element from the dom", () => {
- const overlayListElement = document.querySelector(".overlay-list") as HTMLElement;
- autofillOverlayContentService["isOverlayListVisible"] = true;
-
- autofillOverlayContentService.removeAutofillOverlay();
-
- expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(false);
- expect(document.body.contains(overlayListElement)).toEqual(false);
- });
-
- it("sends a message to the extension background indicating that the overlay list has closed", () => {
- autofillOverlayContentService.removeAutofillOverlay();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
- overlayElement: AutofillOverlayElement.List,
- });
- });
- });
-
- describe("addNewVaultItem", () => {
- it("skips sending the message if the overlay list is not visible", () => {
- autofillOverlayContentService["isOverlayListVisible"] = false;
-
- autofillOverlayContentService.addNewVaultItem();
-
- expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
- });
-
- it("sends a message that facilitates adding a new vault item with empty fields", () => {
- autofillOverlayContentService["isOverlayListVisible"] = true;
-
- autofillOverlayContentService.addNewVaultItem();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
- login: {
- username: "",
- password: "",
- uri: "http://localhost/",
- hostname: "localhost",
- },
- });
- });
-
- it("sends a message that facilitates adding a new vault item with data from user filled fields", () => {
- document.body.innerHTML = `
-
- `;
- const usernameField = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- const passwordField = document.getElementById(
- "password-field",
- ) as ElementWithOpId;
- usernameField.value = "test-username";
- passwordField.value = "test-password";
- autofillOverlayContentService["isOverlayListVisible"] = true;
- autofillOverlayContentService["userFilledFields"] = {
- username: usernameField,
- password: passwordField,
- };
-
- autofillOverlayContentService.addNewVaultItem();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
- login: {
- username: "test-username",
- password: "test-password",
- uri: "http://localhost/",
- hostname: "localhost",
- },
- });
- });
- });
-
- describe("redirectOverlayFocusOut", () => {
- let autofillFieldElement: ElementWithOpId;
- let autofillFieldFocusSpy: jest.SpyInstance;
- let findTabsSpy: jest.SpyInstance;
- let previousFocusableElement: HTMLElement;
- let nextFocusableElement: HTMLElement;
-
- beforeEach(() => {
- document.body.innerHTML = `
-
-
-
- `;
- autofillFieldElement = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- autofillFieldElement.opid = "op-1";
- previousFocusableElement = document.querySelector(
- ".previous-focusable-element",
- ) as HTMLElement;
- nextFocusableElement = document.querySelector(".next-focusable-element") as HTMLElement;
- autofillFieldFocusSpy = jest.spyOn(autofillFieldElement, "focus");
- findTabsSpy = jest.spyOn(autofillOverlayContentService as any, "findTabs");
- autofillOverlayContentService["isOverlayListVisible"] = true;
- autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
- autofillOverlayContentService["focusableElements"] = [
- previousFocusableElement,
- autofillFieldElement,
- nextFocusableElement,
- ];
- });
-
- it("skips focusing an element if the overlay is not visible", () => {
- autofillOverlayContentService["isOverlayListVisible"] = false;
-
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Next);
-
- expect(findTabsSpy).not.toHaveBeenCalled();
- });
-
- it("skips focusing an element if no recently focused field exists", () => {
- autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
-
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Next);
-
- expect(findTabsSpy).not.toHaveBeenCalled();
- });
-
- it("focuses the most recently focused field if the focus direction is `Current`", () => {
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Current);
-
- expect(findTabsSpy).not.toHaveBeenCalled();
- expect(autofillFieldFocusSpy).toHaveBeenCalled();
- });
-
- it("removes the overlay if the focus direction is `Current`", () => {
- jest.useFakeTimers();
- const removeAutofillOverlaySpy = jest.spyOn(
- autofillOverlayContentService as any,
- "removeAutofillOverlay",
- );
-
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Current);
- jest.advanceTimersByTime(150);
-
- expect(removeAutofillOverlaySpy).toHaveBeenCalled();
- });
-
- it("finds all focusable tabs if the focusable elements array is not populated", () => {
- autofillOverlayContentService["focusableElements"] = [];
- findTabsSpy.mockReturnValue([
- previousFocusableElement,
- autofillFieldElement,
- nextFocusableElement,
- ]);
-
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Next);
-
- expect(findTabsSpy).toHaveBeenCalledWith(globalThis.document.body, { getShadowRoot: true });
- });
-
- it("focuses the previous focusable element if the focus direction is `Previous`", () => {
- jest.spyOn(previousFocusableElement, "focus");
-
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Previous);
-
- expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
- expect(previousFocusableElement.focus).toHaveBeenCalled();
- });
-
- it("focuses the next focusable element if the focus direction is `Next`", () => {
- jest.spyOn(nextFocusableElement, "focus");
-
- autofillOverlayContentService.redirectOverlayFocusOut(RedirectFocusDirection.Next);
-
- expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
- expect(nextFocusableElement.focus).toHaveBeenCalled();
- });
- });
-
- describe("handleOverlayRepositionEvent", () => {
- beforeEach(() => {
- document.body.innerHTML = `
-
- `;
- const usernameField = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- autofillOverlayContentService["mostRecentlyFocusedField"] = usernameField;
- autofillOverlayContentService["setOverlayRepositionEventListeners"]();
- autofillOverlayContentService["isOverlayButtonVisible"] = true;
- autofillOverlayContentService["isOverlayListVisible"] = true;
- jest
- .spyOn(autofillOverlayContentService as any, "recentlyFocusedFieldIsCurrentlyFocused")
- .mockReturnValue(true);
- });
-
- it("skips handling the overlay reposition event if the overlay button and list elements are not visible", () => {
- autofillOverlayContentService["isOverlayButtonVisible"] = false;
- autofillOverlayContentService["isOverlayListVisible"] = false;
-
- globalThis.dispatchEvent(new Event(EVENTS.RESIZE));
-
- expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
- });
-
- it("hides the overlay elements", () => {
- globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayHidden", {
- display: "none",
- });
- expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(false);
- expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(false);
- });
-
- it("clears the user interaction timeout", () => {
- jest.useFakeTimers();
- const clearTimeoutSpy = jest.spyOn(globalThis, "clearTimeout");
- autofillOverlayContentService["userInteractionEventTimeout"] = setTimeout(jest.fn(), 123);
-
- globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
-
- expect(clearTimeoutSpy).toHaveBeenCalledWith(expect.anything());
- });
-
- it("removes the overlay completely if the field is not focused", () => {
- jest.useFakeTimers();
- jest
- .spyOn(autofillOverlayContentService as any, "recentlyFocusedFieldIsCurrentlyFocused")
- .mockReturnValue(false);
- const removeAutofillOverlaySpy = jest.spyOn(
- autofillOverlayContentService as any,
- "removeAutofillOverlay",
- );
-
- autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
- autofillOverlayContentService["overlayButtonElement"] = document.createElement("div");
- autofillOverlayContentService["overlayListElement"] = document.createElement("div");
-
- globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
- jest.advanceTimersByTime(800);
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayHidden", {
- display: "block",
- });
- expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(false);
- expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(false);
- expect(removeAutofillOverlaySpy).toHaveBeenCalled();
- });
-
- it("updates the overlay position if the most recently focused field is still within the viewport", async () => {
- jest.useFakeTimers();
- jest
- .spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField")
- .mockImplementation(() => {
- autofillOverlayContentService["focusedFieldData"] = {
- focusedFieldRects: {
- top: 100,
- },
- focusedFieldStyles: {},
- };
- });
- const clearUserInteractionEventTimeoutSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "clearUserInteractionEventTimeout",
- );
-
- globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
- jest.advanceTimersByTime(800);
- await flushPromises();
-
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.Button,
- });
- expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.List,
- });
- expect(clearUserInteractionEventTimeoutSpy).toHaveBeenCalled();
- });
-
- it("removes the autofill overlay if the focused field is outside of the viewport", async () => {
- jest.useFakeTimers();
- jest
- .spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField")
- .mockImplementation(() => {
- autofillOverlayContentService["focusedFieldData"] = {
- focusedFieldRects: {
- top: 4000,
- },
- focusedFieldStyles: {},
- };
- });
- const removeAutofillOverlaySpy = jest.spyOn(
- autofillOverlayContentService as any,
- "removeAutofillOverlay",
- );
-
- globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
- jest.advanceTimersByTime(800);
- await flushPromises();
-
- expect(removeAutofillOverlaySpy).toHaveBeenCalled();
- });
-
- it("defaults overlay elements to a visibility of `false` if the element is not rendered on the page", async () => {
- jest.useFakeTimers();
- jest
- .spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField")
- .mockImplementation(() => {
- autofillOverlayContentService["focusedFieldData"] = {
- focusedFieldRects: {
- top: 100,
- },
- focusedFieldStyles: {},
- };
- });
- jest
- .spyOn(autofillOverlayContentService as any, "updateOverlayElementsPosition")
- .mockImplementation();
- autofillOverlayContentService["overlayButtonElement"] = document.createElement("div");
- autofillOverlayContentService["overlayListElement"] = undefined;
-
- globalThis.dispatchEvent(new Event(EVENTS.SCROLL));
- jest.advanceTimersByTime(800);
- await flushPromises();
-
- expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(true);
- expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(false);
- });
- });
-
- describe("handleOverlayElementMutationObserverUpdate", () => {
- let usernameField: ElementWithOpId;
-
- beforeEach(() => {
- document.body.innerHTML = `
-
- `;
- usernameField = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- usernameField.style.setProperty("display", "block", "important");
- jest.spyOn(usernameField, "removeAttribute");
- jest.spyOn(usernameField.style, "setProperty");
- jest
- .spyOn(
- autofillOverlayContentService as any,
- "isTriggeringExcessiveMutationObserverIterations",
- )
- .mockReturnValue(false);
- });
-
- it("skips handling the mutation if excessive mutation observer events are triggered", () => {
- jest
- .spyOn(
- autofillOverlayContentService as any,
- "isTriggeringExcessiveMutationObserverIterations",
- )
- .mockReturnValue(true);
-
- autofillOverlayContentService["handleOverlayElementMutationObserverUpdate"]([
- createMutationRecordMock({ target: usernameField }),
- ]);
-
- expect(usernameField.removeAttribute).not.toHaveBeenCalled();
- });
-
- it("skips handling the mutation if the record type is not for `attributes`", () => {
- autofillOverlayContentService["handleOverlayElementMutationObserverUpdate"]([
- createMutationRecordMock({ target: usernameField, type: "childList" }),
- ]);
-
- expect(usernameField.removeAttribute).not.toHaveBeenCalled();
- });
-
- it("removes all element attributes that are not the style attribute", () => {
- autofillOverlayContentService["handleOverlayElementMutationObserverUpdate"]([
- createMutationRecordMock({
- target: usernameField,
- type: "attributes",
- attributeName: "placeholder",
- }),
- ]);
-
- expect(usernameField.removeAttribute).toHaveBeenCalledWith("placeholder");
- });
-
- it("removes all attached style attributes and sets the default styles", () => {
- autofillOverlayContentService["handleOverlayElementMutationObserverUpdate"]([
- createMutationRecordMock({
- target: usernameField,
- type: "attributes",
- attributeName: "style",
- }),
- ]);
-
- expect(usernameField.removeAttribute).toHaveBeenCalledWith("style");
- expect(usernameField.style.setProperty).toHaveBeenCalledWith("all", "initial", "important");
- expect(usernameField.style.setProperty).toHaveBeenCalledWith(
- "position",
- "fixed",
- "important",
- );
- expect(usernameField.style.setProperty).toHaveBeenCalledWith("display", "block", "important");
- });
- });
-
- describe("handleBodyElementMutationObserverUpdate", () => {
- let overlayButtonElement: HTMLElement;
- let overlayListElement: HTMLElement;
-
- beforeEach(() => {
- document.body.innerHTML = `
-
-
- `;
- overlayButtonElement = document.querySelector(".overlay-button") as HTMLElement;
- overlayListElement = document.querySelector(".overlay-list") as HTMLElement;
- autofillOverlayContentService["overlayButtonElement"] = overlayButtonElement;
- autofillOverlayContentService["overlayListElement"] = overlayListElement;
- autofillOverlayContentService["isOverlayListVisible"] = true;
- jest.spyOn(globalThis.document.body, "insertBefore");
- jest
- .spyOn(
- autofillOverlayContentService as any,
- "isTriggeringExcessiveMutationObserverIterations",
- )
- .mockReturnValue(false);
- });
-
- it("skips handling the mutation if the overlay elements are not present in the DOM", () => {
- autofillOverlayContentService["overlayButtonElement"] = undefined;
- autofillOverlayContentService["overlayListElement"] = undefined;
-
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
- });
-
- it("skips handling the mutation if excessive mutations are being triggered", () => {
- jest
- .spyOn(
- autofillOverlayContentService as any,
- "isTriggeringExcessiveMutationObserverIterations",
- )
- .mockReturnValue(true);
-
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
- });
-
- it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", () => {
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
- });
-
- it("skips re-arranging the DOM elements if the last child is the overlay button and the overlay list is not visible", () => {
- overlayListElement.remove();
- autofillOverlayContentService["isOverlayListVisible"] = false;
-
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
- });
-
- it("positions the overlay button before the overlay list if an element has inserted itself after the button element", () => {
- const injectedElement = document.createElement("div");
- document.body.insertBefore(injectedElement, overlayListElement);
-
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
- overlayButtonElement,
- overlayListElement,
- );
- });
-
- it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", () => {
- document.body.appendChild(overlayButtonElement);
-
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
- overlayButtonElement,
- overlayListElement,
- );
- });
-
- it("positions the last child before the overlay button if it is not the overlay list", () => {
- const injectedElement = document.createElement("div");
- document.body.appendChild(injectedElement);
-
- autofillOverlayContentService["handleBodyElementMutationObserverUpdate"]();
-
- expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
- injectedElement,
- overlayButtonElement,
- );
- });
- });
-
- describe("isTriggeringExcessiveMutationObserverIterations", () => {
- it("clears any existing reset timeout", () => {
- jest.useFakeTimers();
- const clearTimeoutSpy = jest.spyOn(globalThis, "clearTimeout");
- autofillOverlayContentService["mutationObserverIterationsResetTimeout"] = setTimeout(
- jest.fn(),
- 123,
- );
-
- autofillOverlayContentService["isTriggeringExcessiveMutationObserverIterations"]();
-
- expect(clearTimeoutSpy).toHaveBeenCalledWith(expect.anything());
- });
-
- it("will reset the number of mutationObserverIterations after two seconds", () => {
- jest.useFakeTimers();
- autofillOverlayContentService["mutationObserverIterations"] = 10;
-
- autofillOverlayContentService["isTriggeringExcessiveMutationObserverIterations"]();
- jest.advanceTimersByTime(2000);
-
- expect(autofillOverlayContentService["mutationObserverIterations"]).toEqual(0);
- });
-
- it("will blur the overlay field and remove the autofill overlay if excessive mutation observer iterations are triggering", async () => {
- autofillOverlayContentService["mutationObserverIterations"] = 101;
- const blurMostRecentOverlayFieldSpy = jest.spyOn(
- autofillOverlayContentService as any,
- "blurMostRecentOverlayField",
- );
- const removeAutofillOverlaySpy = jest.spyOn(
- autofillOverlayContentService as any,
- "removeAutofillOverlay",
- );
-
- autofillOverlayContentService["isTriggeringExcessiveMutationObserverIterations"]();
- await flushPromises();
-
- expect(blurMostRecentOverlayFieldSpy).toHaveBeenCalled();
- expect(removeAutofillOverlaySpy).toHaveBeenCalled();
- });
- });
-
- describe("handleVisibilityChangeEvent", () => {
- it("skips removing the overlay if the document is visible", () => {
- jest.spyOn(autofillOverlayContentService as any, "removeAutofillOverlay");
-
- autofillOverlayContentService["handleVisibilityChangeEvent"]();
-
- expect(autofillOverlayContentService["removeAutofillOverlay"]).not.toHaveBeenCalled();
- });
-
- it("removes the overlay if the document is not visible", () => {
- Object.defineProperty(document, "visibilityState", {
- value: "hidden",
- writable: true,
- });
- jest.spyOn(autofillOverlayContentService as any, "removeAutofillOverlay");
-
- autofillOverlayContentService["handleVisibilityChangeEvent"]();
-
- expect(autofillOverlayContentService["removeAutofillOverlay"]).toHaveBeenCalled();
- });
- });
-
- describe("destroy", () => {
- let autofillFieldElement: ElementWithOpId;
- let autofillFieldData: AutofillField;
- let pageDetailsMock: AutofillPageDetails;
-
- beforeEach(() => {
- document.body.innerHTML = `
-
- `;
-
- autofillFieldElement = document.getElementById(
- "username-field",
- ) as ElementWithOpId;
- autofillFieldElement.opid = "op-1";
- autofillFieldData = createAutofillFieldMock({
- opid: "username-field",
- form: "validFormId",
- placeholder: "username",
- elementNumber: 1,
- });
- const passwordFieldData = createAutofillFieldMock({
- opid: "password-field",
- form: "validFormId",
- elementNumber: 2,
- autocompleteType: "current-password",
- type: "password",
- });
- pageDetailsMock = mock({
- forms: { validFormId: mock() },
- fields: [autofillFieldData, passwordFieldData],
- });
- void autofillOverlayContentService.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetailsMock,
- );
- autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
- });
-
- it("disconnects all mutation observers", () => {
- autofillOverlayContentService["setupMutationObserver"]();
- jest.spyOn(autofillOverlayContentService["bodyElementMutationObserver"], "disconnect");
-
- autofillOverlayContentService.destroy();
-
- expect(
- autofillOverlayContentService["bodyElementMutationObserver"].disconnect,
- ).toHaveBeenCalled();
- });
-
- it("clears the user interaction event timeout", () => {
- jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout");
-
- autofillOverlayContentService.destroy();
-
- expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled();
- });
-
- it("de-registers all global event listeners", () => {
- jest.spyOn(globalThis.document, "removeEventListener");
- jest.spyOn(globalThis, "removeEventListener");
- jest.spyOn(autofillOverlayContentService as any, "removeOverlayRepositionEventListeners");
-
- autofillOverlayContentService.destroy();
-
- expect(globalThis.document.removeEventListener).toHaveBeenCalledWith(
- EVENTS.VISIBILITYCHANGE,
- autofillOverlayContentService["handleVisibilityChangeEvent"],
- );
- expect(globalThis.removeEventListener).toHaveBeenCalledWith(
- EVENTS.FOCUSOUT,
- autofillOverlayContentService["handleFormFieldBlurEvent"],
- );
- expect(
- autofillOverlayContentService["removeOverlayRepositionEventListeners"],
- ).toHaveBeenCalled();
- });
-
- it("de-registers any event listeners that are attached to the form field elements", () => {
- jest.spyOn(autofillOverlayContentService as any, "removeCachedFormFieldEventListeners");
- jest.spyOn(autofillFieldElement, "removeEventListener");
- jest.spyOn(autofillOverlayContentService["formFieldElements"], "delete");
-
- autofillOverlayContentService.destroy();
-
- expect(
- autofillOverlayContentService["removeCachedFormFieldEventListeners"],
- ).toHaveBeenCalledWith(autofillFieldElement);
- expect(autofillFieldElement.removeEventListener).toHaveBeenCalledWith(
- EVENTS.BLUR,
- autofillOverlayContentService["handleFormFieldBlurEvent"],
- );
- expect(autofillFieldElement.removeEventListener).toHaveBeenCalledWith(
- EVENTS.KEYUP,
- autofillOverlayContentService["handleFormFieldKeyupEvent"],
- );
- expect(autofillOverlayContentService["formFieldElements"].delete).toHaveBeenCalledWith(
- autofillFieldElement,
- );
- });
- });
-});
diff --git a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts
deleted file mode 100644
index cb281977f14..00000000000
--- a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts
+++ /dev/null
@@ -1,1139 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import "@webcomponents/custom-elements";
-import "lit/polyfill-support.js";
-import { FocusableElement, tabbable } from "tabbable";
-
-import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
-import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
-
-import { FocusedFieldData } from "../../background/abstractions/overlay.background";
-import { AutofillOverlayElement, RedirectFocusDirection } from "../../enums/autofill-overlay.enum";
-import AutofillField from "../../models/autofill-field";
-import AutofillPageDetails from "../../models/autofill-page-details";
-import { AutofillOverlayContentExtensionMessageHandlers } from "../../services/abstractions/autofill-overlay-content.service";
-import { AutoFillConstants } from "../../services/autofill-constants";
-import { InlineMenuFieldQualificationService } from "../../services/inline-menu-field-qualification.service";
-import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../../types";
-import {
- elementIsFillableFormField,
- generateRandomCustomElementName,
- sendExtensionMessage,
- setElementStyles,
-} from "../../utils";
-import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe.deprecated";
-import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe.deprecated";
-
-import {
- LegacyAutofillOverlayContentService as LegacyAutofillOverlayContentServiceInterface,
- OpenAutofillOverlayOptions,
-} from "./abstractions/autofill-overlay-content.service";
-
-class LegacyAutofillOverlayContentService implements LegacyAutofillOverlayContentServiceInterface {
- private readonly inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
- isFieldCurrentlyFocused = false;
- isCurrentlyFilling = false;
- isOverlayCiphersPopulated = false;
- pageDetailsUpdateRequired = false;
- autofillOverlayVisibility: number;
- private isFirefoxBrowser =
- globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
- globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
- private readonly generateRandomCustomElementName = generateRandomCustomElementName;
- private readonly findTabs = tabbable;
- private readonly sendExtensionMessage = sendExtensionMessage;
- private formFieldElements: Set> = new Set([]);
- private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedInlineMenuTypes);
- private userFilledFields: Record = {};
- private authStatus: AuthenticationStatus;
- private focusableElements: FocusableElement[] = [];
- private isOverlayButtonVisible = false;
- private isOverlayListVisible = false;
- private overlayButtonElement: HTMLElement;
- private overlayListElement: HTMLElement;
- private mostRecentlyFocusedField: ElementWithOpId;
- private focusedFieldData: FocusedFieldData;
- private userInteractionEventTimeout: number | NodeJS.Timeout;
- private overlayElementsMutationObserver: MutationObserver;
- private bodyElementMutationObserver: MutationObserver;
- private documentElementMutationObserver: MutationObserver;
- private mutationObserverIterations = 0;
- private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
- private eventHandlersMemo: { [key: string]: EventListener } = {};
- private readonly customElementDefaultStyles: Partial = {
- all: "initial",
- position: "fixed",
- display: "block",
- zIndex: "2147483647",
- };
-
- constructor() {
- this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
- }
-
- /**
- * Satisfy the AutofillOverlayContentService interface.
- */
- messageHandlers = {} as AutofillOverlayContentExtensionMessageHandlers;
- clearUserFilledFields() {
- // do nothing
- }
-
- async setupOverlayListeners(
- autofillFieldElement: ElementWithOpId,
- autofillFieldData: AutofillField,
- pageDetails: AutofillPageDetails,
- ) {
- await this.setupAutofillOverlayListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- pageDetails,
- );
- }
- async blurMostRecentlyFocusedField(_isClosingInlineMenu: boolean) {
- this.blurMostRecentOverlayField();
- }
-
- /**
- * Initializes the autofill overlay content service by setting up the mutation observers.
- * The observers will be instantiated on DOMContentLoaded if the page is current loading.
- */
- init() {
- if (globalThis.document.readyState === "loading") {
- globalThis.document.addEventListener(EVENTS.DOMCONTENTLOADED, this.setupGlobalEventListeners);
- return;
- }
-
- this.setupGlobalEventListeners();
- }
-
- /**
- * Sets up the autofill overlay listener on the form field element. This method is called
- * during the page details collection process.
- *
- * @param formFieldElement - Form field elements identified during the page details collection process.
- * @param autofillFieldData - Autofill field data captured from the form field element.
- * @param pageDetails - The collected page details from the tab.
- */
- async setupAutofillOverlayListenerOnField(
- formFieldElement: ElementWithOpId,
- autofillFieldData: AutofillField,
- pageDetails: AutofillPageDetails,
- ) {
- if (
- this.formFieldElements.has(formFieldElement) ||
- this.isIgnoredField(autofillFieldData, pageDetails)
- ) {
- return;
- }
-
- this.formFieldElements.add(formFieldElement);
-
- if (!this.autofillOverlayVisibility) {
- await this.getAutofillOverlayVisibility();
- }
-
- this.setupFormFieldElementEventListeners(formFieldElement);
-
- if (this.getRootNodeActiveElement(formFieldElement) === formFieldElement) {
- await this.triggerFormFieldFocusedAction(formFieldElement);
- return;
- }
-
- if (!this.mostRecentlyFocusedField) {
- await this.updateMostRecentlyFocusedField(formFieldElement);
- }
- }
-
- /**
- * Handles opening the autofill overlay. Will conditionally open
- * the overlay based on the current autofill overlay visibility setting.
- * Allows you to optionally focus the field element when opening the overlay.
- * Will also optionally ignore the overlay visibility setting and open the
- *
- * @param options - Options for opening the autofill overlay.
- */
- openAutofillOverlay(options: OpenAutofillOverlayOptions = {}) {
- const { isFocusingFieldElement, isOpeningFullOverlay, authStatus } = options;
- if (!this.mostRecentlyFocusedField) {
- return;
- }
-
- if (this.pageDetailsUpdateRequired) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("bgCollectPageDetails", {
- sender: "autofillOverlayContentService",
- });
- this.pageDetailsUpdateRequired = false;
- }
-
- if (isFocusingFieldElement && !this.recentlyFocusedFieldIsCurrentlyFocused()) {
- this.focusMostRecentOverlayField();
- }
-
- if (typeof authStatus !== "undefined") {
- this.authStatus = authStatus;
- }
-
- if (
- this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick &&
- !isOpeningFullOverlay
- ) {
- this.updateOverlayButtonPosition();
- return;
- }
-
- this.updateOverlayElementsPosition();
- }
-
- /**
- * Focuses the most recently focused field element.
- */
- focusMostRecentOverlayField() {
- this.mostRecentlyFocusedField?.focus();
- }
-
- /**
- * Removes focus from the most recently focused field element.
- */
- blurMostRecentOverlayField() {
- this.mostRecentlyFocusedField?.blur();
- }
-
- /**
- * Removes the autofill overlay from the page. This will initially
- * unobserve the body element to ensure the mutation observer no
- * longer triggers.
- */
- removeAutofillOverlay = () => {
- this.removeBodyElementObserver();
- this.removeAutofillOverlayButton();
- this.removeAutofillOverlayList();
- };
-
- /**
- * Removes the overlay button from the DOM if it is currently present. Will
- * also remove the overlay reposition event listeners.
- */
- removeAutofillOverlayButton() {
- if (!this.overlayButtonElement) {
- return;
- }
-
- this.overlayButtonElement.remove();
- this.isOverlayButtonVisible = false;
- void this.sendExtensionMessage("autofillOverlayElementClosed", {
- overlayElement: AutofillOverlayElement.Button,
- });
- this.removeOverlayRepositionEventListeners();
- }
-
- /**
- * Removes the overlay list from the DOM if it is currently present.
- */
- removeAutofillOverlayList() {
- if (!this.overlayListElement) {
- return;
- }
-
- this.overlayListElement.remove();
- this.isOverlayListVisible = false;
- void this.sendExtensionMessage("autofillOverlayElementClosed", {
- overlayElement: AutofillOverlayElement.List,
- });
- }
-
- /**
- * Formats any found user filled fields for a login cipher and sends a message
- * to the background script to add a new cipher.
- */
- addNewVaultItem() {
- if (!this.isOverlayListVisible) {
- return;
- }
-
- const login = {
- username: this.userFilledFields["username"]?.value || "",
- password: this.userFilledFields["password"]?.value || "",
- uri: globalThis.document.URL,
- hostname: globalThis.document.location.hostname,
- };
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
- }
-
- /**
- * Redirects the keyboard focus out of the overlay, selecting the element that is
- * either previous or next in the tab order. If the direction is current, the most
- * recently focused field will be focused.
- *
- * @param direction - The direction to redirect the focus.
- */
- redirectOverlayFocusOut(direction: string) {
- if (!this.isOverlayListVisible || !this.mostRecentlyFocusedField) {
- return;
- }
-
- if (direction === RedirectFocusDirection.Current) {
- this.focusMostRecentOverlayField();
- setTimeout(this.removeAutofillOverlay, 100);
- return;
- }
-
- if (!this.focusableElements.length) {
- this.focusableElements = this.findTabs(globalThis.document.body, { getShadowRoot: true });
- }
-
- const focusedElementIndex = this.focusableElements.findIndex(
- (element) => element === this.mostRecentlyFocusedField,
- );
-
- const indexOffset = direction === RedirectFocusDirection.Previous ? -1 : 1;
- const redirectFocusElement = this.focusableElements[focusedElementIndex + indexOffset];
- redirectFocusElement?.focus();
- }
-
- /**
- * Sets up the event listeners that facilitate interaction with the form field elements.
- * Will clear any cached form field element handlers that are encountered when setting
- * up a form field element to the overlay.
- *
- * @param formFieldElement - The form field element to set up the event listeners for.
- */
- private setupFormFieldElementEventListeners(formFieldElement: ElementWithOpId) {
- this.removeCachedFormFieldEventListeners(formFieldElement);
-
- formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
- formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
- formFieldElement.addEventListener(
- EVENTS.INPUT,
- this.handleFormFieldInputEvent(formFieldElement),
- );
- formFieldElement.addEventListener(
- EVENTS.CLICK,
- this.handleFormFieldClickEvent(formFieldElement),
- );
- formFieldElement.addEventListener(
- EVENTS.FOCUS,
- this.handleFormFieldFocusEvent(formFieldElement),
- );
- }
-
- /**
- * Removes any cached form field element handlers that are encountered
- * when setting up a form field element to present the overlay.
- *
- * @param formFieldElement - The form field element to remove the cached handlers for.
- */
- private removeCachedFormFieldEventListeners(formFieldElement: ElementWithOpId) {
- const handlers = [EVENTS.INPUT, EVENTS.CLICK, EVENTS.FOCUS];
- for (let index = 0; index < handlers.length; index++) {
- const event = handlers[index];
- const memoIndex = this.getFormFieldHandlerMemoIndex(formFieldElement, event);
- const existingHandler = this.eventHandlersMemo[memoIndex];
- if (!existingHandler) {
- return;
- }
-
- formFieldElement.removeEventListener(event, existingHandler);
- delete this.eventHandlersMemo[memoIndex];
- }
- }
-
- /**
- * Helper method that facilitates registration of an event handler to a form field element.
- *
- * @param eventHandler - The event handler to memoize.
- * @param memoIndex - The memo index to use for the event handler.
- */
- private useEventHandlersMemo = (eventHandler: EventListener, memoIndex: string) => {
- return this.eventHandlersMemo[memoIndex] || (this.eventHandlersMemo[memoIndex] = eventHandler);
- };
-
- /**
- * Formats the memoIndex for the form field event handler.
- *
- * @param formFieldElement - The form field element to format the memo index for.
- * @param event - The event to format the memo index for.
- */
- private getFormFieldHandlerMemoIndex(
- formFieldElement: ElementWithOpId,
- event: string,
- ) {
- return `${formFieldElement.opid}-${formFieldElement.id}-${event}-handler`;
- }
-
- /**
- * Form Field blur event handler. Updates the value identifying whether
- * the field is focused and sends a message to check if the overlay itself
- * is currently focused.
- */
- private handleFormFieldBlurEvent = () => {
- this.isFieldCurrentlyFocused = false;
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("checkAutofillOverlayFocused");
- };
-
- /**
- * Form field keyup event handler. Facilitates the ability to remove the
- * autofill overlay using the escape key, focusing the overlay list using
- * the ArrowDown key, and ensuring that the overlay is repositioned when
- * the form is submitted using the Enter key.
- *
- * @param event - The keyup event.
- */
- private handleFormFieldKeyupEvent = (event: KeyboardEvent) => {
- const eventCode = event.code;
- if (eventCode === "Escape") {
- this.removeAutofillOverlay();
- return;
- }
-
- if (eventCode === "Enter" && !this.isCurrentlyFilling) {
- this.handleOverlayRepositionEvent();
- return;
- }
-
- if (eventCode === "ArrowDown") {
- event.preventDefault();
- event.stopPropagation();
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.focusOverlayList();
- }
- };
-
- /**
- * Triggers a focus of the overlay list, if it is visible. If the list is not visible,
- * the overlay will be opened and the list will be focused after a short delay. Ensures
- * that the overlay list is focused when the user presses the down arrow key.
- */
- private async focusOverlayList() {
- if (!this.isOverlayListVisible && this.mostRecentlyFocusedField) {
- await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
- this.openAutofillOverlay({ isOpeningFullOverlay: true });
- setTimeout(() => this.sendExtensionMessage("focusAutofillOverlayList"), 125);
- return;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("focusAutofillOverlayList");
- }
-
- /**
- * Sets up and memoizes the form field input event handler.
- *
- * @param formFieldElement - The form field element that triggered the input event.
- */
- private handleFormFieldInputEvent = (formFieldElement: ElementWithOpId) => {
- return this.useEventHandlersMemo(
- () => this.triggerFormFieldInput(formFieldElement),
- this.getFormFieldHandlerMemoIndex(formFieldElement, EVENTS.INPUT),
- );
- };
-
- /**
- * Triggers when the form field element receives an input event. This method will
- * store the modified form element data for use when the user attempts to add a new
- * vault item. It also acts to remove the overlay list while the user is typing.
- *
- * @param formFieldElement - The form field element that triggered the input event.
- */
- private triggerFormFieldInput(formFieldElement: ElementWithOpId) {
- if (!elementIsFillableFormField(formFieldElement)) {
- return;
- }
-
- this.storeModifiedFormElement(formFieldElement);
-
- if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) {
- this.removeAutofillOverlayList();
- return;
- }
-
- this.openAutofillOverlay();
- }
-
- /**
- * Stores the modified form element data for use when the user attempts to add a new
- * vault item. This method will also store the most recently focused field, if it is
- * not already stored.
- *
- * @param formFieldElement
- * @private
- */
- private storeModifiedFormElement(formFieldElement: ElementWithOpId) {
- if (formFieldElement === this.mostRecentlyFocusedField) {
- this.mostRecentlyFocusedField = formFieldElement;
- }
-
- if (formFieldElement.type === "password") {
- this.userFilledFields.password = formFieldElement;
- return;
- }
-
- this.userFilledFields.username = formFieldElement;
- }
-
- /**
- * Sets up and memoizes the form field click event handler.
- *
- * @param formFieldElement - The form field element that triggered the click event.
- */
- private handleFormFieldClickEvent = (formFieldElement: ElementWithOpId) => {
- return this.useEventHandlersMemo(
- () => this.triggerFormFieldClickedAction(formFieldElement),
- this.getFormFieldHandlerMemoIndex(formFieldElement, EVENTS.CLICK),
- );
- };
-
- /**
- * Triggers when the form field element receives a click event. This method will
- * trigger the focused action for the form field element if the overlay is not visible.
- *
- * @param formFieldElement - The form field element that triggered the click event.
- */
- private async triggerFormFieldClickedAction(formFieldElement: ElementWithOpId) {
- if (this.isOverlayButtonVisible || this.isOverlayListVisible) {
- return;
- }
-
- await this.triggerFormFieldFocusedAction(formFieldElement);
- }
-
- /**
- * Sets up and memoizes the form field focus event handler.
- *
- * @param formFieldElement - The form field element that triggered the focus event.
- */
- private handleFormFieldFocusEvent = (formFieldElement: ElementWithOpId) => {
- return this.useEventHandlersMemo(
- () => this.triggerFormFieldFocusedAction(formFieldElement),
- this.getFormFieldHandlerMemoIndex(formFieldElement, EVENTS.FOCUS),
- );
- };
-
- /**
- * Triggers when the form field element receives a focus event. This method will
- * update the most recently focused field and open the autofill overlay if the
- * autofill process is not currently active.
- *
- * @param formFieldElement - The form field element that triggered the focus event.
- */
- private async triggerFormFieldFocusedAction(formFieldElement: ElementWithOpId) {
- if (this.isCurrentlyFilling) {
- return;
- }
-
- this.isFieldCurrentlyFocused = true;
- this.clearUserInteractionEventTimeout();
- const initiallyFocusedField = this.mostRecentlyFocusedField;
- await this.updateMostRecentlyFocusedField(formFieldElement);
- const formElementHasValue = Boolean((formFieldElement as HTMLInputElement).value);
-
- if (
- this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick ||
- (formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField)
- ) {
- this.removeAutofillOverlayList();
- }
-
- if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("openAutofillOverlay");
- return;
- }
-
- this.updateOverlayButtonPosition();
- }
-
- /**
- * Validates whether the user is currently authenticated.
- */
- private isUserAuthed() {
- return this.authStatus === AuthenticationStatus.Unlocked;
- }
-
- /**
- * Validates that the most recently focused field is currently
- * focused within the root node relative to the field.
- */
- private recentlyFocusedFieldIsCurrentlyFocused() {
- return (
- this.getRootNodeActiveElement(this.mostRecentlyFocusedField) === this.mostRecentlyFocusedField
- );
- }
-
- /**
- * Updates the position of both the overlay button and overlay list.
- */
- private updateOverlayElementsPosition() {
- this.updateOverlayButtonPosition();
- this.updateOverlayListPosition();
- }
-
- /**
- * Updates the position of the overlay button.
- */
- private updateOverlayButtonPosition() {
- if (!this.overlayButtonElement) {
- this.createAutofillOverlayButton();
- this.updateCustomElementDefaultStyles(this.overlayButtonElement);
- }
-
- if (!this.isOverlayButtonVisible) {
- this.appendOverlayElementToBody(this.overlayButtonElement);
- this.isOverlayButtonVisible = true;
- this.setOverlayRepositionEventListeners();
- }
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.Button,
- });
- }
-
- /**
- * Updates the position of the overlay list.
- */
- private updateOverlayListPosition() {
- if (!this.overlayListElement) {
- this.createAutofillOverlayList();
- this.updateCustomElementDefaultStyles(this.overlayListElement);
- }
-
- if (!this.isOverlayListVisible) {
- this.appendOverlayElementToBody(this.overlayListElement);
- this.isOverlayListVisible = true;
- }
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("updateAutofillOverlayPosition", {
- overlayElement: AutofillOverlayElement.List,
- });
- }
-
- /**
- * Appends the overlay element to the body element. This method will also
- * observe the body element to ensure that the overlay element is not
- * interfered with by any DOM changes.
- *
- * @param element - The overlay element to append to the body element.
- */
- private appendOverlayElementToBody(element: HTMLElement) {
- this.observeBodyElement();
- globalThis.document.body.appendChild(element);
- }
-
- /**
- * Sends a message that facilitates hiding the overlay elements.
- *
- * @param isHidden - Indicates if the overlay elements should be hidden.
- */
- private toggleOverlayHidden(isHidden: boolean) {
- const displayValue = isHidden ? "none" : "block";
- void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue });
-
- this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden;
- this.isOverlayListVisible = !!this.overlayListElement && !isHidden;
- }
-
- /**
- * Updates the data used to position the overlay elements in relation
- * to the most recently focused form field.
- *
- * @param formFieldElement - The form field element that triggered the focus event.
- */
- private async updateMostRecentlyFocusedField(
- formFieldElement: ElementWithOpId,
- ) {
- this.mostRecentlyFocusedField = formFieldElement;
- const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement);
- const { width, height, top, left } =
- await this.getMostRecentlyFocusedFieldRects(formFieldElement);
- this.focusedFieldData = {
- focusedFieldStyles: { paddingRight, paddingLeft },
- focusedFieldRects: { width, height, top, left },
- };
-
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.sendExtensionMessage("updateFocusedFieldData", {
- focusedFieldData: this.focusedFieldData,
- });
- }
-
- /**
- * Gets the bounding client rects for the most recently focused field. This method will
- * attempt to use an intersection observer to get the most recently focused field's
- * bounding client rects. If the intersection observer is not supported, or the
- * intersection observer does not return a valid bounding client rect, the form
- * field element's bounding client rect will be used.
- *
- * @param formFieldElement - The form field element that triggered the focus event.
- */
- private async getMostRecentlyFocusedFieldRects(
- formFieldElement: ElementWithOpId,
- ) {
- const focusedFieldRects =
- await this.getBoundingClientRectFromIntersectionObserver(formFieldElement);
- if (focusedFieldRects) {
- return focusedFieldRects;
- }
-
- return formFieldElement.getBoundingClientRect();
- }
-
- /**
- * Gets the bounds of the form field element from the IntersectionObserver API.
- *
- * @param formFieldElement - The form field element that triggered the focus event.
- */
- private async getBoundingClientRectFromIntersectionObserver(
- formFieldElement: ElementWithOpId,
- ): Promise {
- if (!("IntersectionObserver" in globalThis) && !("IntersectionObserverEntry" in globalThis)) {
- return null;
- }
-
- return new Promise((resolve) => {
- const intersectionObserver = new IntersectionObserver(
- (entries) => {
- let fieldBoundingClientRects = entries[0]?.boundingClientRect;
- if (!fieldBoundingClientRects?.width || !fieldBoundingClientRects.height) {
- fieldBoundingClientRects = null;
- }
-
- intersectionObserver.disconnect();
- resolve(fieldBoundingClientRects);
- },
- {
- root: globalThis.document.body,
- rootMargin: "0px",
- threshold: 0.9999, // Safari doesn't seem to function properly with a threshold of 1
- },
- );
- intersectionObserver.observe(formFieldElement);
- });
- }
-
- /**
- * Identifies if the field should have the autofill overlay setup on it. Currently, this is mainly
- * determined by whether the field correlates with a login cipher. This method will need to be
- * updated in the future to support other types of forms.
- *
- * @param autofillFieldData - Autofill field data captured from the form field element.
- * @param pageDetails - The collected page details from the tab.
- */
- private isIgnoredField(
- autofillFieldData: AutofillField,
- pageDetails: AutofillPageDetails,
- ): boolean {
- if (
- autofillFieldData.readonly ||
- autofillFieldData.disabled ||
- !autofillFieldData.viewable ||
- this.ignoredFieldTypes.has(autofillFieldData.type)
- ) {
- return true;
- }
-
- return !this.inlineMenuFieldQualificationService.isFieldForLoginForm(
- autofillFieldData,
- pageDetails,
- );
- }
-
- /**
- * Creates the autofill overlay button element. Will not attempt
- * to create the element if it already exists in the DOM.
- */
- private createAutofillOverlayButton() {
- if (this.overlayButtonElement) {
- return;
- }
-
- if (this.isFirefoxBrowser) {
- this.overlayButtonElement = globalThis.document.createElement("div");
- new AutofillOverlayButtonIframe(this.overlayButtonElement);
-
- return;
- }
-
- const customElementName = this.generateRandomCustomElementName();
- globalThis.customElements?.define(
- customElementName,
- class extends HTMLElement {
- constructor() {
- super();
- new AutofillOverlayButtonIframe(this);
- }
- },
- );
- this.overlayButtonElement = globalThis.document.createElement(customElementName);
- }
-
- /**
- * Creates the autofill overlay list element. Will not attempt
- * to create the element if it already exists in the DOM.
- */
- private createAutofillOverlayList() {
- if (this.overlayListElement) {
- return;
- }
-
- if (this.isFirefoxBrowser) {
- this.overlayListElement = globalThis.document.createElement("div");
- new AutofillOverlayListIframe(this.overlayListElement);
-
- return;
- }
-
- const customElementName = this.generateRandomCustomElementName();
- globalThis.customElements?.define(
- customElementName,
- class extends HTMLElement {
- constructor() {
- super();
- new AutofillOverlayListIframe(this);
- }
- },
- );
- this.overlayListElement = globalThis.document.createElement(customElementName);
- }
-
- /**
- * Updates the default styles for the custom element. This method will
- * remove any styles that are added to the custom element by other methods.
- *
- * @param element - The custom element to update the default styles for.
- */
- private updateCustomElementDefaultStyles(element: HTMLElement) {
- this.unobserveCustomElements();
-
- setElementStyles(element, this.customElementDefaultStyles, true);
-
- this.observeCustomElements();
- }
-
- /**
- * Queries the background script for the autofill overlay visibility setting.
- * If the setting is not found, a default value of OnFieldFocus will be used
- * @private
- */
- private async getAutofillOverlayVisibility() {
- const overlayVisibility = await this.sendExtensionMessage("getAutofillOverlayVisibility");
- this.autofillOverlayVisibility = overlayVisibility || AutofillOverlayVisibility.OnFieldFocus;
- }
-
- /**
- * Sets up event listeners that facilitate repositioning
- * the autofill overlay on scroll or resize.
- */
- private setOverlayRepositionEventListeners() {
- globalThis.addEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, {
- capture: true,
- });
- globalThis.addEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent);
- }
-
- /**
- * Removes the listeners that facilitate repositioning
- * the autofill overlay on scroll or resize.
- */
- private removeOverlayRepositionEventListeners() {
- globalThis.removeEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, {
- capture: true,
- });
- globalThis.removeEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent);
- }
-
- /**
- * Handles the resize or scroll events that enact
- * repositioning of the overlay.
- */
- private handleOverlayRepositionEvent = () => {
- if (!this.isOverlayButtonVisible && !this.isOverlayListVisible) {
- return;
- }
-
- this.toggleOverlayHidden(true);
- this.clearUserInteractionEventTimeout();
- this.userInteractionEventTimeout = setTimeout(
- this.triggerOverlayRepositionUpdates,
- 750,
- ) as unknown as number;
- };
-
- /**
- * Triggers the overlay reposition updates. This method ensures that the overlay elements
- * are correctly positioned when the viewport scrolls or repositions.
- */
- private triggerOverlayRepositionUpdates = async () => {
- if (!this.recentlyFocusedFieldIsCurrentlyFocused()) {
- this.toggleOverlayHidden(false);
- this.removeAutofillOverlay();
- return;
- }
-
- await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
- this.updateOverlayElementsPosition();
- this.toggleOverlayHidden(false);
- this.clearUserInteractionEventTimeout();
-
- if (
- this.focusedFieldData.focusedFieldRects?.top > 0 &&
- this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight
- ) {
- return;
- }
-
- this.removeAutofillOverlay();
- };
-
- /**
- * Clears the user interaction event timeout. This is used to ensure that
- * the overlay is not repositioned while the user is interacting with it.
- */
- private clearUserInteractionEventTimeout() {
- if (this.userInteractionEventTimeout) {
- clearTimeout(this.userInteractionEventTimeout);
- }
- }
-
- /**
- * Sets up global event listeners and the mutation
- * observer to facilitate required changes to the
- * overlay elements.
- */
- private setupGlobalEventListeners = () => {
- globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
- globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
- this.setupMutationObserver();
- };
-
- /**
- * Handles the visibility change event. This method will remove the
- * autofill overlay if the document is not visible.
- */
- private handleVisibilityChangeEvent = () => {
- if (document.visibilityState === "visible") {
- return;
- }
-
- this.mostRecentlyFocusedField = null;
- this.removeAutofillOverlay();
- };
-
- /**
- * Sets up mutation observers for the overlay elements, the body element, and the
- * document element. The mutation observers are used to remove any styles that are
- * added to the overlay elements by the website. They are also used to ensure that
- * the overlay elements are always present at the bottom of the body element.
- */
- private setupMutationObserver = () => {
- this.overlayElementsMutationObserver = new MutationObserver(
- this.handleOverlayElementMutationObserverUpdate,
- );
-
- this.bodyElementMutationObserver = new MutationObserver(
- this.handleBodyElementMutationObserverUpdate,
- );
- };
-
- /**
- * Sets up mutation observers to verify that the overlay
- * elements are not modified by the website.
- */
- private observeCustomElements() {
- if (this.overlayButtonElement) {
- this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, {
- attributes: true,
- });
- }
-
- if (this.overlayListElement) {
- this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true });
- }
- }
-
- /**
- * Disconnects the mutation observers that are used to verify that the overlay
- * elements are not modified by the website.
- */
- private unobserveCustomElements() {
- this.overlayElementsMutationObserver?.disconnect();
- }
-
- /**
- * Sets up a mutation observer for the body element. The mutation observer is used
- * to ensure that the overlay elements are always present at the bottom of the body
- * element.
- */
- private observeBodyElement() {
- this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
- }
-
- /**
- * Disconnects the mutation observer for the body element.
- */
- private removeBodyElementObserver() {
- this.bodyElementMutationObserver?.disconnect();
- }
-
- /**
- * Handles the mutation observer update for the overlay elements. This method will
- * remove any attributes or styles that might be added to the overlay elements by
- * a separate process within the website where this script is injected.
- *
- * @param mutationRecord - The mutation record that triggered the update.
- */
- private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
- if (this.isTriggeringExcessiveMutationObserverIterations()) {
- return;
- }
-
- for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
- const record = mutationRecord[recordIndex];
- if (record.type !== "attributes") {
- continue;
- }
-
- const element = record.target as HTMLElement;
- if (record.attributeName !== "style") {
- this.removeModifiedElementAttributes(element);
-
- continue;
- }
-
- element.removeAttribute("style");
- this.updateCustomElementDefaultStyles(element);
- }
- };
-
- /**
- * Removes all elements from a passed overlay
- * element except for the style attribute.
- *
- * @param element - The element to remove the attributes from.
- */
- private removeModifiedElementAttributes(element: HTMLElement) {
- const attributes = Array.from(element.attributes);
- for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
- const attribute = attributes[attributeIndex];
- if (attribute.name === "style") {
- continue;
- }
-
- element.removeAttribute(attribute.name);
- }
- }
-
- /**
- * Handles the mutation observer update for the body element. This method will
- * ensure that the overlay elements are always present at the bottom of the body
- * element.
- */
- private handleBodyElementMutationObserverUpdate = () => {
- if (
- (!this.overlayButtonElement && !this.overlayListElement) ||
- this.isTriggeringExcessiveMutationObserverIterations()
- ) {
- return;
- }
-
- const lastChild = globalThis.document.body.lastElementChild;
- const secondToLastChild = lastChild?.previousElementSibling;
- const lastChildIsOverlayList = lastChild === this.overlayListElement;
- const lastChildIsOverlayButton = lastChild === this.overlayButtonElement;
- const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement;
-
- if (
- (lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
- (lastChildIsOverlayButton && !this.isOverlayListVisible)
- ) {
- return;
- }
-
- if (
- (lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
- (lastChildIsOverlayButton && this.isOverlayListVisible)
- ) {
- globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement);
- return;
- }
-
- globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement);
- };
-
- /**
- * Identifies if the mutation observer is triggering excessive iterations.
- * Will trigger a blur of the most recently focused field and remove the
- * autofill overlay if any set mutation observer is triggering
- * excessive iterations.
- */
- private isTriggeringExcessiveMutationObserverIterations() {
- if (this.mutationObserverIterationsResetTimeout) {
- clearTimeout(this.mutationObserverIterationsResetTimeout);
- }
-
- this.mutationObserverIterations++;
- this.mutationObserverIterationsResetTimeout = setTimeout(
- () => (this.mutationObserverIterations = 0),
- 2000,
- );
-
- if (this.mutationObserverIterations > 100) {
- clearTimeout(this.mutationObserverIterationsResetTimeout);
- this.mutationObserverIterations = 0;
- this.blurMostRecentOverlayField();
- this.removeAutofillOverlay();
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Gets the root node of the passed element and returns the active element within that root node.
- *
- * @param element - The element to get the root node active element for.
- */
- private getRootNodeActiveElement(element: Element): Element {
- const documentRoot = element.getRootNode() as ShadowRoot | Document;
- return documentRoot?.activeElement;
- }
-
- /**
- * Destroys the autofill overlay content service. This method will
- * disconnect the mutation observers and remove all event listeners.
- */
- destroy() {
- this.documentElementMutationObserver?.disconnect();
- this.clearUserInteractionEventTimeout();
- this.formFieldElements.forEach((formFieldElement) => {
- this.removeCachedFormFieldEventListeners(formFieldElement);
- formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
- formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
- this.formFieldElements.delete(formFieldElement);
- });
- globalThis.document.removeEventListener(
- EVENTS.VISIBILITYCHANGE,
- this.handleVisibilityChangeEvent,
- );
- globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
- this.removeAutofillOverlay();
- this.removeOverlayRepositionEventListeners();
- }
-}
-
-export default LegacyAutofillOverlayContentService;
diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html
index 90a536ad25d..4fd85ddce33 100644
--- a/apps/browser/src/autofill/popup/settings/autofill.component.html
+++ b/apps/browser/src/autofill/popup/settings/autofill.component.html
@@ -43,10 +43,7 @@
-
+
-
+
this.generatePassword(),
+ (password) => this.addPasswordToHistory(password),
);
- if (!inlineMenuPositioningImprovementsEnabled) {
- this.overlayBackground = new LegacyOverlayBackground(
- this.cipherService,
- this.autofillService,
- this.authService,
- this.environmentService,
- this.domainSettingsService,
- this.autofillSettingsService,
- this.i18nService,
- this.platformUtilsService,
- this.themeStateService,
- this.accountService,
- );
- } else {
- this.overlayBackground = new OverlayBackground(
- this.logService,
- this.cipherService,
- this.autofillService,
- this.authService,
- this.environmentService,
- this.domainSettingsService,
- this.autofillSettingsService,
- this.i18nService,
- this.platformUtilsService,
- this.vaultSettingsService,
- this.fido2ActiveRequestManager,
- this.inlineMenuFieldQualificationService,
- this.themeStateService,
- this.totpService,
- this.accountService,
- () => this.generatePassword(),
- (password) => this.addPasswordToHistory(password),
- );
- }
-
this.tabsBackground = new TabsBackground(
this,
this.notificationBackground,
diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json
index 07aa3d2e4a9..35578fb8321 100644
--- a/apps/browser/src/manifest.json
+++ b/apps/browser/src/manifest.json
@@ -79,12 +79,7 @@
"__safari__optional_permissions": null,
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
"sandbox": {
- "pages": [
- "overlay/menu-button.html",
- "overlay/menu-list.html",
- "overlay/button.html",
- "overlay/list.html"
- ],
+ "pages": ["overlay/menu-button.html", "overlay/menu-list.html"],
"content_security_policy": "sandbox allow-scripts; script-src 'self'"
},
"__firefox__sandbox": null,
@@ -140,8 +135,6 @@
"overlay/menu-button.html",
"overlay/menu-list.html",
"overlay/menu.html",
- "overlay/button.html",
- "overlay/list.html",
"popup/fonts/*"
],
"__firefox__browser_specific_settings": {
diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json
index be1a3f17827..556a73ac15b 100644
--- a/apps/browser/src/manifest.v3.json
+++ b/apps/browser/src/manifest.v3.json
@@ -105,12 +105,7 @@
"__chrome__sandbox": "sandbox allow-scripts; script-src 'self'"
},
"sandbox": {
- "pages": [
- "overlay/menu-button.html",
- "overlay/menu-list.html",
- "overlay/button.html",
- "overlay/list.html"
- ]
+ "pages": ["overlay/menu-button.html", "overlay/menu-list.html"]
},
"__firefox__sandbox": null,
"commands": {
@@ -167,8 +162,6 @@
"overlay/menu-button.html",
"overlay/menu-list.html",
"overlay/menu.html",
- "overlay/button.html",
- "overlay/list.html",
"popup/fonts/*"
],
"matches": [""]
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index 8ebf6eb6110..21ac4c19700 100644
--- a/apps/browser/src/popup/app-routing.module.ts
+++ b/apps/browser/src/popup/app-routing.module.ts
@@ -593,6 +593,7 @@ const routes: Routes = [
path: "intro-carousel",
component: ExtensionAnonLayoutWrapperComponent,
canActivate: [],
+ data: { elevation: 0, doNotSaveUrl: true } satisfies RouteDataProperties,
children: [
{
path: "",
diff --git a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html
index 3c061109945..ff7bf25b86b 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html
@@ -43,7 +43,7 @@
bitButton
buttonType="secondary"
(click)="navigateToLogin()"
- class="tw-w-full tw-mt-4"
+ class="tw-w-full tw-mt-2"
>
{{ "logIn" | i18n }}
diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js
index 09d1133a4df..4b66ed7d70a 100644
--- a/apps/browser/webpack.config.js
+++ b/apps/browser/webpack.config.js
@@ -135,16 +135,6 @@ const plugins = [
filename: "overlay/menu.html",
chunks: ["overlay/menu"],
}),
- new HtmlWebpackPlugin({
- template: "./src/autofill/deprecated/overlay/pages/button/legacy-button.html",
- filename: "overlay/button.html",
- chunks: ["overlay/button"],
- }),
- new HtmlWebpackPlugin({
- template: "./src/autofill/deprecated/overlay/pages/list/legacy-list.html",
- filename: "overlay/list.html",
- chunks: ["overlay/list"],
- }),
new CopyWebpackPlugin({
patterns: [
{
@@ -197,8 +187,6 @@ const mainConfig = {
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
"content/bootstrap-autofill-overlay-notifications":
"./src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
- "content/bootstrap-legacy-autofill-overlay":
- "./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts",
"content/autofiller": "./src/autofill/content/autofiller.ts",
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
@@ -213,10 +201,6 @@ const mainConfig = {
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
"overlay/menu":
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
- "overlay/button":
- "./src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts",
- "overlay/list":
- "./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts",
"encrypt-worker": "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts",
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html
index ede6eb7ed82..8477a588fef 100644
--- a/apps/desktop/src/vault/app/vault/view.component.html
+++ b/apps/desktop/src/vault/app/vault/view.component.html
@@ -656,7 +656,11 @@
class="primary"
(click)="restore()"
appA11yTitle="{{ 'restore' | i18n }}"
- *ngIf="(limitItemDeletion$ | async) ? (canRestoreCipher$ | async) : cipher.isDeleted"
+ *ngIf="
+ (limitItemDeletion$ | async)
+ ? (canRestoreCipher$ | async) && cipher.isDeleted
+ : cipher.isDeleted
+ "
>
diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html
index 47846c77571..e50c55e83d2 100644
--- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html
+++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html
@@ -19,12 +19,26 @@
*ngIf="canShowVaultTab(organization)"
>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
;
protected isBreadcrumbEventLogsEnabled$: Observable;
+ protected showSponsoredFamiliesDropdown$: Observable;
constructor(
private route: ActivatedRoute,
@@ -76,6 +78,7 @@ export class OrganizationLayoutComponent implements OnInit {
private providerService: ProviderService,
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
+ private freeFamiliesPolicyService: FreeFamiliesPolicyService,
) {}
async ngOnInit() {
@@ -92,6 +95,8 @@ export class OrganizationLayoutComponent implements OnInit {
),
filter((org) => org != null),
);
+ this.showSponsoredFamiliesDropdown$ =
+ this.freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(this.organization$);
this.showAccountDeprovisioningBanner$ = combineLatest([
this.bannerService.showBanner$,
diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html
index 80d22467123..2079d592a28 100644
--- a/apps/web/src/app/admin-console/organizations/manage/events.component.html
+++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html
@@ -1,6 +1,12 @@
@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
-
+
{{ "upgrade" | i18n }}
diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts
index 5220ea1ef39..9666630fc08 100644
--- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts
+++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts
@@ -3,6 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component";
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
import { MembersComponent } from "./members.component";
@@ -16,6 +17,14 @@ const routes: Routes = [
titleId: "members",
},
},
+ {
+ path: "sponsored-families",
+ component: SponsoredFamiliesComponent,
+ canActivate: [organizationPermissionsGuard(canAccessMembersTab)],
+ data: {
+ titleId: "sponsoredFamilies",
+ },
+ },
];
@NgModule({
diff --git a/apps/web/src/app/billing/services/free-families-policy.service.spec.ts b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts
new file mode 100644
index 00000000000..10ccc448986
--- /dev/null
+++ b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts
@@ -0,0 +1,193 @@
+import { mock, MockProxy } from "jest-mock-extended";
+import { firstValueFrom, of } from "rxjs";
+
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
+
+import { FreeFamiliesPolicyService } from "./free-families-policy.service";
+
+describe("FreeFamiliesPolicyService", () => {
+ let service: FreeFamiliesPolicyService;
+ let organizationService: MockProxy;
+ let policyService: MockProxy;
+ let configService: MockProxy;
+ let accountService: FakeAccountService;
+ const userId = Utils.newGuid() as UserId;
+
+ beforeEach(() => {
+ organizationService = mock();
+ policyService = mock();
+ configService = mock();
+ accountService = mockAccountServiceWith(userId);
+
+ service = new FreeFamiliesPolicyService(
+ policyService,
+ organizationService,
+ accountService,
+ configService,
+ );
+ });
+
+ describe("showSponsoredFamiliesDropdown$", () => {
+ it("should return true when all conditions are met", async () => {
+ // Configure mocks
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization that meets all criteria
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: true,
+ isAdmin: true,
+ isOwner: false,
+ canManageUsers: false,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(true);
+ });
+
+ it("should return false when organization is not Enterprise", async () => {
+ // Configure mocks
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization that is not Enterprise tier
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Teams,
+ useAdminSponsoredFamilies: true,
+ isAdmin: true,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(false);
+ });
+
+ it("should return false when feature flag is disabled", async () => {
+ // Configure mocks to disable feature flag
+ configService.getFeatureFlag$.mockReturnValue(of(false));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization that meets other criteria
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: true,
+ isAdmin: true,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(false);
+ });
+
+ it("should return false when families feature is disabled by policy", async () => {
+ // Configure mocks with a policy that disables the feature
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(
+ of([{ organizationId: "org-id", enabled: true } as Policy]),
+ );
+
+ // Create a test organization
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: true,
+ isAdmin: true,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(false);
+ });
+
+ it("should return false when useAdminSponsoredFamilies is false", async () => {
+ // Configure mocks
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization with useAdminSponsoredFamilies set to false
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: false,
+ isAdmin: true,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(false);
+ });
+
+ it("should return true when user is an owner but not admin", async () => {
+ // Configure mocks
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization where user is owner but not admin
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: true,
+ isAdmin: false,
+ isOwner: true,
+ canManageUsers: false,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(true);
+ });
+
+ it("should return true when user can manage users but is not admin or owner", async () => {
+ // Configure mocks
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization where user can manage users but is not admin or owner
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: true,
+ isAdmin: false,
+ isOwner: false,
+ canManageUsers: true,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(true);
+ });
+
+ it("should return false when user has no admin permissions", async () => {
+ // Configure mocks
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ policyService.policiesByType$.mockReturnValue(of([]));
+
+ // Create a test organization where user has no admin permissions
+ const organization = {
+ id: "org-id",
+ productTierType: ProductTierType.Enterprise,
+ useAdminSponsoredFamilies: true,
+ isAdmin: false,
+ isOwner: false,
+ canManageUsers: false,
+ } as Organization;
+
+ // Test the method
+ const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts
index 81cb970cdbe..7a8e3804b2c 100644
--- a/apps/web/src/app/billing/services/free-families-policy.service.ts
+++ b/apps/web/src/app/billing/services/free-families-policy.service.ts
@@ -7,6 +7,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
interface EnterpriseOrgStatus {
isFreeFamilyPolicyEnabled: boolean;
@@ -26,6 +29,7 @@ export class FreeFamiliesPolicyService {
private policyService: PolicyService,
private organizationService: OrganizationService,
private accountService: AccountService,
+ private configService: ConfigService,
) {}
organizations$ = this.accountService.activeAccount$.pipe(
@@ -42,6 +46,48 @@ export class FreeFamiliesPolicyService {
return this.getFreeFamiliesVisibility$();
}
+ /**
+ * Determines whether to show the sponsored families dropdown in the organization layout
+ * @param organization The organization to check
+ * @returns Observable indicating whether to show the dropdown
+ */
+ showSponsoredFamiliesDropdown$(organization: Observable): Observable {
+ const enterpriseOrganization$ = organization.pipe(
+ map((org) => org.productTierType === ProductTierType.Enterprise),
+ );
+
+ return this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) => {
+ const policies$ = this.policyService.policiesByType$(
+ PolicyType.FreeFamiliesSponsorshipPolicy,
+ userId,
+ );
+
+ return combineLatest([
+ enterpriseOrganization$,
+ this.configService.getFeatureFlag$(FeatureFlag.PM17772_AdminInitiatedSponsorships),
+ organization,
+ policies$,
+ ]).pipe(
+ map(([isEnterprise, featureFlagEnabled, org, policies]) => {
+ const familiesFeatureDisabled = policies.some(
+ (policy) => policy.organizationId === org.id && policy.enabled,
+ );
+
+ return (
+ isEnterprise &&
+ featureFlagEnabled &&
+ !familiesFeatureDisabled &&
+ org.useAdminSponsoredFamilies &&
+ (org.isAdmin || org.isOwner || org.canManageUsers)
+ );
+ }),
+ );
+ }),
+ );
+ }
+
private getFreeFamiliesVisibility$(): Observable {
return combineLatest([
this.checkEnterpriseOrganizationsAndFetchPolicy(),
diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts
index da50a25886e..1ae58d3eef3 100644
--- a/libs/admin-console/src/common/collections/services/default-collection.service.ts
+++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts
@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
+import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs";
import { Jsonify } from "type-fest";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
@@ -8,10 +8,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
ActiveUserState,
- StateProvider,
COLLECTION_DATA,
DeriveDefinition,
DerivedState,
+ StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
@@ -84,6 +84,7 @@ export class DefaultCollectionService implements CollectionService {
switchMap(([userId, collectionData]) =>
combineLatest([of(collectionData), this.keyService.orgKeys$(userId)]),
),
+ shareReplay({ refCount: false, bufferSize: 1 }),
);
this.decryptedCollectionDataState = this.stateProvider.getDerived(
diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts
index 8a198663e06..4ca18b4985e 100644
--- a/libs/auth/src/angular/login/login.component.ts
+++ b/libs/auth/src/angular/login/login.component.ts
@@ -536,6 +536,10 @@ export class LoginComponent implements OnInit, OnDestroy {
if (storedEmail) {
this.formGroup.controls.email.setValue(storedEmail);
this.formGroup.controls.rememberEmail.setValue(true);
+ // If we load an email into the form, we need to initialize it for the login process as well
+ // so that other login components can use it.
+ // We do this here as it's possible that a user doesn't edit the email field before submitting.
+ this.loginEmailService.setLoginEmail(storedEmail);
} else {
this.formGroup.controls.rememberEmail.setValue(false);
}
diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts
index 5f487e1f898..fae24133502 100644
--- a/libs/common/src/admin-console/models/data/organization.data.spec.ts
+++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts
@@ -58,6 +58,7 @@ describe("ORGANIZATIONS state", () => {
familySponsorshipLastSyncDate: new Date(),
userIsManagedByOrganization: false,
useRiskInsights: false,
+ useAdminSponsoredFamilies: false,
},
};
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts
index b81d06e6367..799d062aefa 100644
--- a/libs/common/src/admin-console/models/data/organization.data.ts
+++ b/libs/common/src/admin-console/models/data/organization.data.ts
@@ -60,6 +60,7 @@ export class OrganizationData {
allowAdminAccessToAllCollectionItems: boolean;
userIsManagedByOrganization: boolean;
useRiskInsights: boolean;
+ useAdminSponsoredFamilies: boolean;
constructor(
response?: ProfileOrganizationResponse,
@@ -122,6 +123,7 @@ export class OrganizationData {
this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems;
this.userIsManagedByOrganization = response.userIsManagedByOrganization;
this.useRiskInsights = response.useRiskInsights;
+ this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies;
this.isMember = options.isMember;
this.isProviderUser = options.isProviderUser;
diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts
index c5c5b53cce7..2e51c54b0ad 100644
--- a/libs/common/src/admin-console/models/domain/organization.ts
+++ b/libs/common/src/admin-console/models/domain/organization.ts
@@ -90,6 +90,7 @@ export class Organization {
*/
userIsManagedByOrganization: boolean;
useRiskInsights: boolean;
+ useAdminSponsoredFamilies: boolean;
constructor(obj?: OrganizationData) {
if (obj == null) {
@@ -148,6 +149,7 @@ export class Organization {
this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems;
this.userIsManagedByOrganization = obj.userIsManagedByOrganization;
this.useRiskInsights = obj.useRiskInsights;
+ this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies;
}
get canAccess() {
diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts
index 5e37cfc4c5c..da97a1034b1 100644
--- a/libs/common/src/admin-console/models/response/profile-organization.response.ts
+++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts
@@ -55,6 +55,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
allowAdminAccessToAllCollectionItems: boolean;
userIsManagedByOrganization: boolean;
useRiskInsights: boolean;
+ useAdminSponsoredFamilies: boolean;
constructor(response: any) {
super(response);
@@ -121,5 +122,6 @@ export class ProfileOrganizationResponse extends BaseResponse {
);
this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization");
this.useRiskInsights = this.getResponseProperty("UseRiskInsights");
+ this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies");
}
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index fa776285ead..f9e2d9757bc 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -24,7 +24,6 @@ export enum FeatureFlag {
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
IdpAutoSubmitLogin = "idp-auto-submit-login",
- InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
NotificationRefresh = "notification-refresh",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
MacOsNativeCredentialSync = "macos-native-credential-sync",
@@ -34,6 +33,7 @@ export enum FeatureFlag {
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features",
PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method",
+ PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
@@ -88,7 +88,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
- [FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.NotificationRefresh]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
@@ -117,6 +116,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
[FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE,
[FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE,
+ [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
/* Key Management */
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts
index 52d70e8652a..50577472120 100644
--- a/libs/vault/src/cipher-form/cipher-form.stories.ts
+++ b/libs/vault/src/cipher-form/cipher-form.stories.ts
@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { importProvidersFrom, signal } from "@angular/core";
+import { ActivatedRoute } from "@angular/router";
import { action } from "@storybook/addon-actions";
import {
applicationConfig,
@@ -225,6 +226,14 @@ export default {
getFeatureFlag: () => Promise.resolve(false),
},
},
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: {
+ queryParams: {},
+ },
+ },
+ },
],
}),
componentWrapperDecorator(
diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts
index 58a9b8f3965..af39ea96c16 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts
@@ -83,4 +83,24 @@ describe("AddEditCustomFieldDialogComponent", () => {
expect.objectContaining({ value: FieldType.Linked }),
);
});
+
+ it("does not filter out 'Hidden' field type when 'disallowHiddenField' is false", () => {
+ dialogData.disallowHiddenField = false;
+ fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent);
+ component = fixture.componentInstance;
+
+ expect(component.fieldTypeOptions).toContainEqual(
+ expect.objectContaining({ value: FieldType.Hidden }),
+ );
+ });
+
+ it("filers out 'Hidden' field type when 'disallowHiddenField' is true", () => {
+ dialogData.disallowHiddenField = true;
+ fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent);
+ component = fixture.componentInstance;
+
+ expect(component.fieldTypeOptions).not.toContainEqual(
+ expect.objectContaining({ value: FieldType.Hidden }),
+ );
+ });
});
diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts
index bdf5345672d..72bdf5dca1a 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts
@@ -25,6 +25,7 @@ export type AddEditCustomFieldDialogData = {
cipherType: CipherType;
/** When provided, dialog will display edit label variants */
editLabelConfig?: { index: number; label: string };
+ disallowHiddenField?: boolean;
};
@Component({
@@ -68,6 +69,9 @@ export class AddEditCustomFieldDialogComponent {
this.variant = data.editLabelConfig ? "edit" : "add";
this.fieldTypeOptions = this.fieldTypeOptions.filter((option) => {
+ if (this.data.disallowHiddenField && option.value === FieldType.Hidden) {
+ return false;
+ }
// Filter out the Linked field type for Secure Notes
if (this.data.cipherType === CipherType.SecureNote) {
return option.value !== FieldType.Linked;
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html
index 3bce3c5f385..1305bcdae05 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html
+++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html
@@ -89,7 +89,7 @@
bitIconButton="bwi-pencil-square"
class="tw-self-center tw-mt-2"
data-testid="edit-custom-field-button"
- *ngIf="!isPartialEdit"
+ *ngIf="canEdit(field.value.type)"
>
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts
index fb9664594ed..ced8763f895 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts
@@ -45,7 +45,9 @@ describe("CustomFieldsComponent", () => {
announce = jest.fn().mockResolvedValue(null);
patchCipher = jest.fn();
originalCipherView = new CipherView();
- config = {} as CipherFormConfig;
+ config = {
+ collections: [],
+ } as CipherFormConfig;
await TestBed.configureTestingModule({
imports: [CustomFieldsComponent],
@@ -463,5 +465,91 @@ describe("CustomFieldsComponent", () => {
// "reorder boolean label to position 4 of 4"
expect(announce).toHaveBeenCalledWith("reorderFieldDown boolean label 4 4", "assertive");
});
+
+ it("hides reorder buttons when in partial edit mode", () => {
+ originalCipherView.fields = mockFieldViews;
+ config.mode = "partial-edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ toggleItems = fixture.debugElement.queryAll(
+ By.css('button[data-testid="reorder-toggle-button"]'),
+ );
+
+ expect(toggleItems).toHaveLength(0);
+ });
+ });
+
+ it("shows all reorders button when in edit mode and viewPassword is true", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = true;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const toggleItems = fixture.debugElement.queryAll(
+ By.css('button[data-testid="reorder-toggle-button"]'),
+ );
+ expect(toggleItems).toHaveLength(4);
+ });
+
+ it("shows all reorder buttons except for hidden fields when in edit mode and viewPassword is false", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = false;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const toggleItems = fixture.debugElement.queryAll(
+ By.css('button[data-testid="reorder-toggle-button"]'),
+ );
+
+ expect(toggleItems).toHaveLength(3);
+ });
+
+ describe("edit button", () => {
+ it("hides the edit button when in partial-edit mode", () => {
+ originalCipherView.fields = mockFieldViews;
+ config.mode = "partial-edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const editButtons = fixture.debugElement.queryAll(
+ By.css('button[data-testid="edit-custom-field-button"]'),
+ );
+ expect(editButtons).toHaveLength(0);
+ });
+
+ it("shows all the edit buttons when in edit mode and viewPassword is true", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = true;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const editButtons = fixture.debugElement.queryAll(
+ By.css('button[data-testid="edit-custom-field-button"]'),
+ );
+ expect(editButtons).toHaveLength(4);
+ });
+
+ it("shows all the edit buttons except for hidden fields when in edit mode and viewPassword is false", () => {
+ originalCipherView.fields = mockFieldViews;
+ originalCipherView.viewPassword = false;
+ config.mode = "edit";
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const editButtons = fixture.debugElement.queryAll(
+ By.css('button[data-testid="edit-custom-field-button"]'),
+ );
+ expect(editButtons).toHaveLength(3);
+ });
});
});
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts
index dd3fd8c24a8..49e9e109b74 100644
--- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts
+++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts
@@ -116,6 +116,8 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
/** Emits when a new custom field should be focused */
private focusOnNewInput$ = new Subject();
+ disallowHiddenField?: boolean;
+
destroyed$: DestroyRef;
FieldType = FieldType;
@@ -141,6 +143,13 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
return this.customFieldsForm.controls.fields as FormArray;
}
+ canEdit(type: FieldType): boolean {
+ return (
+ !this.isPartialEdit &&
+ (type !== FieldType.Hidden || this.cipherFormContainer.originalCipherView?.viewPassword)
+ );
+ }
+
ngOnInit() {
const linkedFieldsOptionsForCipher = this.getLinkedFieldsOptionsForCipher();
const optionsArray = Array.from(linkedFieldsOptionsForCipher?.entries() ?? []);
@@ -210,6 +219,7 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
/** Opens the add/edit custom field dialog */
openAddEditCustomFieldDialog(editLabelConfig?: AddEditCustomFieldDialogData["editLabelConfig"]) {
+ const { cipherType, mode, originalCipher } = this.cipherFormContainer.config;
this.dialogRef = this.dialogService.open(
AddEditCustomFieldDialogComponent,
{
@@ -217,8 +227,9 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
addField: this.addField.bind(this),
updateLabel: this.updateLabel.bind(this),
removeField: this.removeField.bind(this),
- cipherType: this.cipherFormContainer.config.cipherType,
+ cipherType,
editLabelConfig,
+ disallowHiddenField: mode === "edit" && !originalCipher.viewPassword,
},
},
);
diff --git a/libs/vault/src/icons/login-cards.ts b/libs/vault/src/icons/login-cards.ts
index 01baf308412..6e066a0d924 100644
--- a/libs/vault/src/icons/login-cards.ts
+++ b/libs/vault/src/icons/login-cards.ts
@@ -2,10 +2,10 @@ import { svgIcon } from "@bitwarden/components";
export const LoginCards = svgIcon`
`;
diff --git a/libs/vault/src/icons/secure-devices.ts b/libs/vault/src/icons/secure-devices.ts
index ee3a6ea6b90..4e123afad40 100644
--- a/libs/vault/src/icons/secure-devices.ts
+++ b/libs/vault/src/icons/secure-devices.ts
@@ -3,14 +3,14 @@ import { svgIcon } from "@bitwarden/components";
export const SecureDevices = svgIcon`
`;
diff --git a/libs/vault/src/icons/secure-user.ts b/libs/vault/src/icons/secure-user.ts
index f8f126adbac..39d9957030c 100644
--- a/libs/vault/src/icons/secure-user.ts
+++ b/libs/vault/src/icons/secure-user.ts
@@ -2,8 +2,8 @@ import { svgIcon } from "@bitwarden/components";
export const SecureUser = svgIcon`
diff --git a/libs/vault/src/icons/security-handshake.ts b/libs/vault/src/icons/security-handshake.ts
index 5a598fd180d..d68f8a948d3 100644
--- a/libs/vault/src/icons/security-handshake.ts
+++ b/libs/vault/src/icons/security-handshake.ts
@@ -2,11 +2,11 @@ import { svgIcon } from "@bitwarden/components";
export const SecurityHandshake = svgIcon`
`;