Date: Tue, 15 Apr 2025 17:19:58 -0400
Subject: [PATCH 27/47] PM-20106 Pass indicator data to notification bar cipher
items (#14246)
* PM-20106 initial approach whihc preserves exisiting indicator file style
* refactored approach to be able to pass any icon when or if needed in the future
* address feedback
---
.../background/notification.background.ts | 31 ++++++++++++++--
.../cipher/cipher-indicator-icons.ts | 35 +++++++++++--------
.../content/components/cipher/cipher-info.ts | 16 ++++++---
.../content/components/cipher/types.ts | 9 +++++
4 files changed, 70 insertions(+), 21 deletions(-)
diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts
index 1f0cc469e2c..6589252d94b 100644
--- a/apps/browser/src/autofill/background/notification.background.ts
+++ b/apps/browser/src/autofill/background/notification.background.ts
@@ -16,6 +16,7 @@ import {
} from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
+import { ProductTierType } from "@bitwarden/common/billing/enums/product-tier-type.enum";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -41,7 +42,11 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { BrowserApi } from "../../platform/browser/browser-api";
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
-import { NotificationCipherData } from "../content/components/cipher/types";
+import {
+ OrganizationCategory,
+ OrganizationCategories,
+ NotificationCipherData,
+} from "../content/components/cipher/types";
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
@@ -174,8 +179,29 @@ export default class NotificationBackground {
activeUserId,
);
+ const organizations = await firstValueFrom(
+ this.organizationService.organizations$(activeUserId),
+ );
+
return decryptedCiphers.map((view) => {
- const { id, name, reprompt, favorite, login } = view;
+ const { id, name, reprompt, favorite, login, organizationId } = view;
+
+ const organizationType = organizationId
+ ? organizations.find((org) => org.id === organizationId)?.productTierType
+ : null;
+
+ const organizationCategories: OrganizationCategory[] = [];
+
+ if (
+ [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes(
+ organizationType,
+ )
+ ) {
+ organizationCategories.push(OrganizationCategories.business);
+ }
+ if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) {
+ organizationCategories.push(OrganizationCategories.family);
+ }
return {
id,
@@ -183,6 +209,7 @@ export default class NotificationBackground {
type: CipherType.Login,
reprompt,
favorite,
+ ...(organizationCategories.length ? { organizationCategories } : {}),
icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
login: login && {
username: login.username,
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 9096149f510..e4fe012a678 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
@@ -1,30 +1,35 @@
import { css } from "@emotion/css";
-import { html } from "lit";
+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";
-// @TODO connect data source to icon checks
-// @TODO support other indicator types (attachments, etc)
+import { OrganizationCategories, OrganizationCategory } from "./types";
+
+const cipherIndicatorIconsMap: Record<
+ OrganizationCategory,
+ (args: { color: string; theme: Theme }) => TemplateResult
+> = {
+ [OrganizationCategories.business]: Business,
+ [OrganizationCategories.family]: Users,
+};
+
export function CipherInfoIndicatorIcons({
- showBusinessIcon,
- showFamilyIcon,
+ organizationCategories = [],
theme,
}: {
- showBusinessIcon?: boolean;
- showFamilyIcon?: boolean;
+ organizationCategories?: OrganizationCategory[];
theme: Theme;
}) {
- const indicatorIcons = [
- ...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []),
- ...(showFamilyIcon ? [Users({ color: themes[theme].text.muted, theme })] : []),
- ];
-
- return indicatorIcons.length
- ? html` ${indicatorIcons} `
- : null; // @TODO null case should be handled by parent
+ return html`
+
+ ${organizationCategories.map((name) =>
+ cipherIndicatorIconsMap[name]?.({ color: themes[theme].text.muted, theme }),
+ )}
+
+ `;
}
const cipherInfoIndicatorIconsStyles = css`
diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts
index 6ff32353938..e3d237b9bc6 100644
--- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts
+++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts
@@ -1,5 +1,5 @@
import { css } from "@emotion/css";
-import { html } from "lit";
+import { html, nothing } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
@@ -8,14 +8,22 @@ import { themes, typography } from "../../../content/components/constants/styles
import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons";
import { NotificationCipherData } from "./types";
-// @TODO support other cipher types (card, identity, notes, etc)
export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) {
- const { name, login } = cipher;
+ const { name, login, organizationCategories } = cipher;
+ const hasIndicatorIcons = organizationCategories?.length;
return html`
- ${[name, CipherInfoIndicatorIcons({ theme })]}
+ ${[
+ name,
+ hasIndicatorIcons
+ ? CipherInfoIndicatorIcons({
+ theme,
+ organizationCategories,
+ })
+ : nothing,
+ ]}
${login?.username
diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts
index ff29f9b559f..590311682bf 100644
--- a/apps/browser/src/autofill/content/components/cipher/types.ts
+++ b/apps/browser/src/autofill/content/components/cipher/types.ts
@@ -14,6 +14,14 @@ export const CipherRepromptTypes = {
type CipherRepromptType = (typeof CipherRepromptTypes)[keyof typeof CipherRepromptTypes];
+export type OrganizationCategory =
+ (typeof OrganizationCategories)[keyof typeof OrganizationCategories];
+
+export const OrganizationCategories = {
+ business: "business",
+ family: "family",
+} as const;
+
export type WebsiteIconData = {
imageEnabled: boolean;
image: string;
@@ -50,4 +58,5 @@ export type NotificationCipherData = BaseCipherData &
login?: {
username: string;
};
+ organizationCategories?: OrganizationCategory[];
};
From cb869484239c01075e6145681c240c8c43f21a4c Mon Sep 17 00:00:00 2001
From: Miles Blackwood
Date: Tue, 15 Apr 2025 20:00:08 -0400
Subject: [PATCH 28/47] [PM-15436] Standalone password entry should trigger
save to bitwarden prompt. (#14110)
* Modify behavior so standalone password entry (with or without generator) should trigger save to bitwarden prompt.
* Rename intent to action, extend button/action styles.
* Ensure font weight is returned to normal.
* Make save login message a button to handle accessibility, adds helper function.
* Fix failing snapshot by reintigrating erroneously removed line.
* Update snapshot to match new saveLoginButton.
* Add add'l open in new window message to aria label.
* Update snapshot with open in new window message.
---
apps/browser/src/_locales/en/messages.json | 4 +-
.../autofill/background/overlay.background.ts | 6 +--
.../autofill-inline-menu-list.spec.ts.snap | 39 ++-------------
.../list/autofill-inline-menu-list.spec.ts | 10 ++--
.../pages/list/autofill-inline-menu-list.ts | 50 +++++++++++++++----
.../overlay/inline-menu/pages/list/list.scss | 8 +++
6 files changed, 61 insertions(+), 56 deletions(-)
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index f3b85496b75..87b94650b51 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -4928,8 +4928,8 @@
"message": "Password regenerated",
"description": "Notification message for when a password has been regenerated"
},
- "saveLoginToBitwarden": {
- "message": "Save login to Bitwarden?",
+ "saveToBitwarden": {
+ "message": "Save to Bitwarden",
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index a2088f50a11..4e2e773a0c7 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -1852,7 +1852,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Verifies whether the save login inline menu view should be shown. This requires that
- * the login data on the page contains a username and either a current or new password.
+ * the login data on the page contains either a current or new password.
*
* @param tab - The tab to check for login data
*/
@@ -1869,7 +1869,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return (
(this.shouldShowInlineMenuAccountCreation() ||
this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration)) &&
- !!(loginData.username && (loginData.password || loginData.newPassword))
+ !!(loginData.password || loginData.newPassword)
);
}
@@ -2157,7 +2157,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
"passwordRegenerated",
"passwords",
"regeneratePassword",
- "saveLoginToBitwarden",
+ "saveToBitwarden",
"toggleBitwardenVaultOverlay",
"totpCodeAria",
"totpSecondsSpanAria",
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap
index acd06fb8c65..b6e41c448d6 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap
@@ -4,47 +4,14 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList creates the build sav
`;
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts
index b1eebd2bc39..ed28375e4fe 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts
@@ -1089,12 +1089,12 @@ describe("AutofillInlineMenuList", () => {
});
describe("displaying the save login view", () => {
- let buildSaveLoginInlineMenuListSpy: jest.SpyInstance;
+ let buildSaveLoginInlineMenuSpy: jest.SpyInstance;
beforeEach(() => {
- buildSaveLoginInlineMenuListSpy = jest.spyOn(
+ buildSaveLoginInlineMenuSpy = jest.spyOn(
autofillInlineMenuList as any,
- "buildSaveLoginInlineMenuList",
+ "buildSaveLoginInlineMenu",
);
});
@@ -1108,7 +1108,7 @@ describe("AutofillInlineMenuList", () => {
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
- expect(buildSaveLoginInlineMenuListSpy).not.toHaveBeenCalled();
+ expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled();
});
it("builds the save login item view", async () => {
@@ -1117,7 +1117,7 @@ describe("AutofillInlineMenuList", () => {
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
- expect(buildSaveLoginInlineMenuListSpy).toHaveBeenCalled();
+ expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled();
});
});
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts
index acb01594cc6..e0db93b6b4a 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts
@@ -3,6 +3,8 @@
import "@webcomponents/custom-elements";
import "lit/polyfill-support.js";
+import { FocusableElement } from "tabbable";
+
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
@@ -117,7 +119,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}
if (showSaveLoginMenu) {
- this.buildSaveLoginInlineMenuList();
+ this.buildSaveLoginInlineMenu();
return;
}
@@ -165,24 +167,52 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
/**
* Builds the inline menu list as a prompt that asks the user if they'd like to save the login data.
*/
- private buildSaveLoginInlineMenuList() {
- const saveLoginMessage = globalThis.document.createElement("div");
- saveLoginMessage.classList.add("save-login", "inline-menu-list-message");
- saveLoginMessage.textContent = this.getTranslation("saveLoginToBitwarden");
+ private buildSaveLoginInlineMenu() {
+ const saveLoginButton = globalThis.document.createElement("button");
+ saveLoginButton.classList.add(
+ "save-login",
+ "inline-menu-list-button",
+ "inline-menu-list-action",
+ );
+
+ saveLoginButton.tabIndex = -1;
+ saveLoginButton.setAttribute(
+ "aria-label",
+ `${this.getTranslation("saveToBitwarden")}, ${this.getTranslation("opensInANewWindow")}`,
+ );
+ saveLoginButton.textContent = this.getTranslation("saveToBitwarden");
+
+ saveLoginButton.addEventListener(EVENTS.CLICK, this.handleNewLoginVaultItemAction);
+ saveLoginButton.addEventListener(EVENTS.KEYUP, this.handleSaveLoginInlineMenuKeyUp);
+
+ const inlineMenuListButtonContainer = this.buildButtonContainer(saveLoginButton);
- const newItemButton = this.buildNewItemButton(true);
this.showInlineMenuAccountCreation = true;
- this.inlineMenuListContainer.append(saveLoginMessage, newItemButton);
+ this.inlineMenuListContainer.append(inlineMenuListButtonContainer);
}
+ private handleSaveLoginInlineMenuKeyUp = (event: KeyboardEvent) => {
+ const listenedForKeys = new Set(["ArrowDown"]);
+ if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
+ return;
+ }
+
+ event.preventDefault();
+
+ if (event.code === "ArrowDown") {
+ (event.target as FocusableElement).focus();
+ return;
+ }
+ };
+
/**
* Handles the show save login inline menu list message that is triggered from the background script.
*/
private handleShowSaveLoginInlineMenuList() {
if (this.authStatus === AuthenticationStatus.Unlocked) {
this.resetInlineMenuContainer();
- this.buildSaveLoginInlineMenuList();
+ this.buildSaveLoginInlineMenu();
}
}
@@ -521,7 +551,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.newItemButtonElement.textContent = this.getNewItemButtonText(showLogin);
this.newItemButtonElement.setAttribute("aria-label", this.getNewItemAriaLabel(showLogin));
this.newItemButtonElement.prepend(buildSvgDomElement(plusIcon));
- this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
+ this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handleNewLoginVaultItemAction);
return this.buildButtonContainer(this.newItemButtonElement);
}
@@ -581,7 +611,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* Handles the click event for the new item button.
* Sends a message to the parent window to add a new vault item.
*/
- private handeNewItemButtonClick = () => {
+ private handleNewLoginVaultItemAction = () => {
let addNewCipherType = this.inlineMenuFillType;
if (this.showInlineMenuAccountCreation) {
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss
index d0875cfe427..93f5f647ffe 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss
@@ -45,6 +45,14 @@ body * {
&.no-items,
&.save-login {
font-size: 1.6rem;
+ &:has(:focus-visible) {
+ outline-width: 0.2rem;
+ outline-style: solid;
+
+ @include themify($themes) {
+ outline-color: themed("focusOutlineColor");
+ }
+ }
}
}
From 9da15601be3b8a7e2883db55b7ab0a0a8150ccb8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?=
Date: Wed, 16 Apr 2025 15:06:41 +0200
Subject: [PATCH 29/47] Add workflow to trigger self-host unified build in
publish web (#14268)
---
.github/workflows/publish-web.yml | 33 +++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml
index 09f5ddc6318..69b29086d36 100644
--- a/.github/workflows/publish-web.yml
+++ b/.github/workflows/publish-web.yml
@@ -141,3 +141,36 @@ jobs:
- name: Log out of Docker
run: docker logout
+
+ self-host-unified-build:
+ name: Trigger self-host unified build
+ runs-on: ubuntu-22.04
+ needs:
+ - setup
+ steps:
+ - name: Log in to Azure - CI subscription
+ uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ with:
+ creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+
+ - name: Retrieve GitHub PAT secrets
+ id: retrieve-secret-pat
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: "bitwarden-ci"
+ secrets: "github-pat-bitwarden-devops-bot-repo-scope"
+
+ - name: Trigger self-host build
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
+ script: |
+ await github.rest.actions.createWorkflowDispatch({
+ owner: 'bitwarden',
+ repo: 'self-host',
+ workflow_id: 'build-unified.yml',
+ ref: 'main',
+ inputs: {
+ use_latest_core_version: true
+ }
+ });
From 9cffc3b4f4c61aa57dcc62b2298f4840e3906a18 Mon Sep 17 00:00:00 2001
From: Vijay Oommen
Date: Wed, 16 Apr 2025 08:16:40 -0500
Subject: [PATCH 30/47] [PM-20118] Capitalize risk insights (#14291)
---
apps/web/src/locales/en/messages.json | 4 ++--
.../access-intelligence/risk-insights-loading.component.html | 2 +-
.../tools/access-intelligence/risk-insights.component.html | 1 +
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 0193fc4862b..85a7b8cb927 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -4073,8 +4073,8 @@
"updateBrowser": {
"message": "Update browser"
},
- "generatingRiskInsights": {
- "message": "Generating your risk insights..."
+ "generatingYourRiskInsights": {
+ "message": "Generating your Risk Insights..."
},
"updateBrowserDesc": {
"message": "You are using an unsupported web browser. The web vault may not function properly."
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html
index 4e77838229e..0c5b74eead2 100644
--- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html
+++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html
@@ -4,5 +4,5 @@
title="{{ 'loading' | i18n }}"
aria-hidden="true"
>
- {{ "generatingRiskInsights" | i18n }}
+ {{ "generatingYourRiskInsights" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html
index 397e2a630de..2d5693dad54 100644
--- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html
+++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html
@@ -8,6 +8,7 @@
{{ "reviewAtRiskPasswords" | i18n }}
Date: Wed, 16 Apr 2025 11:04:31 -0400
Subject: [PATCH 31/47] fix restore button (#14244)
---
apps/desktop/src/vault/app/vault/view.component.html | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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
+ "
>
From b413272bd5adc63b2c77bdeceddf23d7f0603208 Mon Sep 17 00:00:00 2001
From: Jonathan Prusik
Date: Wed, 16 Apr 2025 11:08:51 -0400
Subject: [PATCH 32/47] [PM-20325] - Misc design fixes/tweaks (#14309)
* fix icon sizing in option selection
* fix close button vertical centering
* fix cipher item update text
* fix missing header background color
* fix brand logo positioning in notification header
---
.../autofill/content/components/buttons/close-button.ts | 1 +
.../components/buttons/option-selection-button.ts | 5 +++--
.../autofill/content/components/cipher/cipher-action.ts | 4 ++--
.../content/components/icons/brand-icon-container.ts | 9 +++++++--
.../autofill/content/components/notification/header.ts | 2 +-
.../content/components/option-selection/option-item.ts | 7 ++++---
6 files changed, 18 insertions(+), 10 deletions(-)
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/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/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;
}
`;
From f293c15f4d8ae9bdae07e1f8a26232da2c456243 Mon Sep 17 00:00:00 2001
From: Shane Melton
Date: Wed, 16 Apr 2025 08:24:30 -0700
Subject: [PATCH 33/47] [PM-19538] Add shareReplay to internal orgKeys
subscription (#14034)
---
.../collections/services/default-collection.service.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
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(
From db16c98a1d93114b613398f0758c15bdbc7faf20 Mon Sep 17 00:00:00 2001
From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Date: Wed, 16 Apr 2025 11:58:54 -0400
Subject: [PATCH 34/47] [PM-17773] Added "Sponsored Families" dropdown nav item
in the admin console (#14029)
* Added nav item for f4e in org admin console
* shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table
* Resolved issue with members nav item also being selected when f4e is selected
* Separated out billing's logic from the org layout component
* Removed unused observable
* Moved logic to existing f4e policy service and added unit tests
* Resolved script typescript error
* Resolved goofy switchMap
---------
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
---
.../organization-layout.component.html | 26 ++-
.../layouts/organization-layout.component.ts | 5 +
.../members/members-routing.module.ts | 9 +
.../free-families-policy.service.spec.ts | 193 ++++++++++++++++++
.../services/free-families-policy.service.ts | 46 +++++
.../models/data/organization.data.spec.ts | 1 +
.../models/data/organization.data.ts | 2 +
.../models/domain/organization.ts | 2 +
.../response/profile-organization.response.ts | 2 +
libs/common/src/enums/feature-flag.enum.ts | 2 +
10 files changed, 282 insertions(+), 6 deletions(-)
create mode 100644 apps/web/src/app/billing/services/free-families-policy.service.spec.ts
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/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/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..9ee1ef919f5 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -34,6 +34,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",
@@ -117,6 +118,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,
From 6bd3fceaa1eac26fb8116fde0f308bbc40e77922 Mon Sep 17 00:00:00 2001
From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Date: Wed, 16 Apr 2025 17:27:48 +0100
Subject: [PATCH 35/47] fix: align upgrade badge with header text in Event Logs
(#14213)
---
.../organizations/manage/events.component.html | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
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 }}
From 1efdcacd16d5d1fd9e0c853976c73491d40e839d Mon Sep 17 00:00:00 2001
From: Jonathan Prusik
Date: Wed, 16 Apr 2025 13:15:43 -0400
Subject: [PATCH 36/47] [PM-16641] Remove
"inline-menu-positioning-improvements" feature flag (#14225)
* remove inline-menu-positioning-improvements flag
* remove unused LegacyOverlayBackground
* remove unused deprecated files
* appease ts error TS2564
* remove deleted resources from the manifest files
---
.../autofill/background/tabs.background.ts | 14 +-
.../overlay.background.deprecated.ts | 124 --
.../overlay.background.deprecated.spec.ts | 1464 --------------
.../overlay.background.deprecated.ts | 811 --------
.../abstractions/autofill-init.deprecated.ts | 41 -
.../content/autofill-init.deprecated.spec.ts | 604 ------
.../content/autofill-init.deprecated.ts | 315 ---
.../bootstrap-legacy-autofill-overlay.ts | 14 -
.../autofill-overlay-button.deprecated.ts | 29 -
...ofill-overlay-iframe.service.deprecated.ts | 33 -
.../autofill-overlay-list.deprecated.ts | 31 -
...utofill-overlay-page-element.deprecated.ts | 13 -
...lay-iframe.service.deprecated.spec.ts.snap | 23 -
...l-overlay-button-iframe.deprecated.spec.ts | 26 -
...tofill-overlay-button-iframe.deprecated.ts | 21 -
...-overlay-iframe-element.deprecated.spec.ts | 46 -
...ofill-overlay-iframe-element.deprecated.ts | 22 -
...-overlay-iframe.service.deprecated.spec.ts | 521 -----
...ofill-overlay-iframe.service.deprecated.ts | 429 ----
...ill-overlay-list-iframe.deprecated.spec.ts | 26 -
...autofill-overlay-list-iframe.deprecated.ts | 26 -
...ill-overlay-button.deprecated.spec.ts.snap | 83 -
...autofill-overlay-button.deprecated.spec.ts | 135 --
.../autofill-overlay-button.deprecated.ts | 124 --
...trap-autofill-overlay-button.deprecated.ts | 11 -
.../overlay/pages/button/legacy-button.html | 12 -
.../overlay/pages/button/legacy-button.scss | 36 -
...ofill-overlay-list.deprecated.spec.ts.snap | 537 -----
.../autofill-overlay-list.deprecated.spec.ts | 467 -----
.../list/autofill-overlay-list.deprecated.ts | 621 ------
...tstrap-autofill-overlay-list.deprecated.ts | 11 -
.../overlay/pages/list/legacy-list.html | 12 -
.../overlay/pages/list/legacy-list.scss | 292 ---
...ll-overlay-page-element.deprecated.spec.ts | 222 ---
...utofill-overlay-page-element.deprecated.ts | 157 --
.../autofill-overlay-content.service.ts | 37 -
...overlay-content.service.deprecated.spec.ts | 1743 -----------------
...fill-overlay-content.service.deprecated.ts | 1139 -----------
.../popup/settings/autofill.component.html | 10 +-
.../popup/settings/autofill.component.ts | 17 +-
.../src/autofill/services/autofill.service.ts | 9 -
.../browser/src/background/main.background.ts | 56 +-
apps/browser/src/manifest.json | 9 +-
apps/browser/src/manifest.v3.json | 9 +-
apps/browser/webpack.config.js | 16 -
libs/common/src/enums/feature-flag.enum.ts | 2 -
46 files changed, 29 insertions(+), 10371 deletions(-)
delete mode 100644 apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.html
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts
delete mode 100644 apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts
delete mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts
delete mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts
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/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`] = `
-
-`;
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 = `
-