mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<CSSStyleDeclaration>;
|
||||
focusedFieldRects: Partial<DOMRect>;
|
||||
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,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<string, CipherView> = new Map();
|
||||
private pageDetailsForTab: Record<
|
||||
chrome.runtime.MessageSender["tab"]["id"],
|
||||
Map<chrome.runtime.MessageSender["frameId"], PageDetail>
|
||||
> = {};
|
||||
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<string, string>;
|
||||
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<OverlayCipherData[]> {
|
||||
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<InlineMenuVisibilitySetting> {
|
||||
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;
|
||||
@@ -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 };
|
||||
@@ -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<AutofillOverlayContentServiceDeprecated>();
|
||||
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<IntersectionObserver>());
|
||||
});
|
||||
|
||||
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<chrome.tabs.Tab>(),
|
||||
sender: "sender",
|
||||
};
|
||||
sender = mock<chrome.runtime.MessageSender>();
|
||||
});
|
||||
|
||||
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<AutofillScript>();
|
||||
|
||||
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<chrome.tabs.Tab>(),
|
||||
};
|
||||
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<AutofillScript>();
|
||||
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<AutofillScript>();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<AutofillPageDetails | void> {
|
||||
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;
|
||||
@@ -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);
|
||||
@@ -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<string, string>;
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
type AutofillOverlayIframeExtensionMessage = {
|
||||
command: string;
|
||||
styles?: Partial<CSSStyleDeclaration>;
|
||||
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<CSSStyleDeclaration>, ariaAlert?: string): void;
|
||||
}
|
||||
|
||||
export {
|
||||
AutofillOverlayIframeExtensionMessage,
|
||||
AutofillOverlayIframeWindowMessageHandlers,
|
||||
BackgroundPortMessageHandlers,
|
||||
AutofillOverlayIframeService,
|
||||
};
|
||||
@@ -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<string, string>;
|
||||
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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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`] = `
|
||||
<div
|
||||
aria-atomic="true"
|
||||
aria-live="polite"
|
||||
role="status"
|
||||
style="position: absolute !important; top: -9999px !important; left: -9999px !important; width: 1px !important; height: 1px !important; overflow: hidden !important; opacity: 0 !important; pointer-events: none;"
|
||||
>
|
||||
aria alert
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's attributes 1`] = `
|
||||
<iframe
|
||||
allowtransparency="true"
|
||||
sandbox="allow-scripts"
|
||||
src="chrome-extension://id/overlay/legacy-list.html"
|
||||
style="all: initial !important; position: fixed !important; display: block !important; z-index: 2147483647 !important; line-height: 0 !important; overflow: hidden !important; transition: opacity 125ms ease-out 0s !important; visibility: visible !important; clip-path: none !important; pointer-events: auto !important; margin: 0px !important; padding: 0px !important; color-scheme: normal !important; opacity: 0 !important; height: 0px;"
|
||||
tabindex="-1"
|
||||
title="title"
|
||||
/>
|
||||
`;
|
||||
@@ -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 = "<autofill-overlay-button-iframe></autofill-overlay-button-iframe>";
|
||||
|
||||
const iframe = document.querySelector("autofill-overlay-button-iframe");
|
||||
|
||||
expect(iframe).toBeInstanceOf(HTMLElement);
|
||||
expect(iframe.shadowRoot).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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 = "<autofill-overlay-iframe></autofill-overlay-iframe>";
|
||||
|
||||
const iframe = document.querySelector("autofill-overlay-iframe");
|
||||
|
||||
expect(iframe).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("attaches a closed shadow DOM", () => {
|
||||
document.body.innerHTML = "<autofill-overlay-iframe></autofill-overlay-iframe>";
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import AutofillOverlayIframeService from "./autofill-overlay-iframe.service.deprecated";
|
||||
|
||||
class AutofillOverlayIframeElement {
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
iframePath: string,
|
||||
portName: string,
|
||||
initStyles: Partial<CSSStyleDeclaration>,
|
||||
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;
|
||||
@@ -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<MediaQueryList>({ 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<MediaQueryList>({ 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string>;
|
||||
private iframeMutationObserver: MutationObserver;
|
||||
private iframe: HTMLIFrameElement;
|
||||
private ariaAlertElement: HTMLDivElement;
|
||||
private ariaAlertTimeout: number | NodeJS.Timeout;
|
||||
private iframeStyles: Partial<CSSStyleDeclaration> = {
|
||||
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<string, string> = {
|
||||
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<CSSStyleDeclaration>,
|
||||
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<CSSStyleDeclaration>) {
|
||||
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<CSSStyleDeclaration>) {
|
||||
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;
|
||||
@@ -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 = "<autofill-overlay-list-iframe></autofill-overlay-list-iframe>";
|
||||
|
||||
const iframe = document.querySelector("autofill-overlay-list-iframe");
|
||||
|
||||
expect(iframe).toBeInstanceOf(HTMLElement);
|
||||
expect(iframe.shadowRoot).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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`] = `
|
||||
<button
|
||||
aria-label="toggleBitwardenVaultOverlay"
|
||||
class="overlay-button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="overlay-button-svg-icon logo-locked-icon"
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
<path
|
||||
d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
cx="12.889"
|
||||
cy="12.889"
|
||||
fill="#F8F9FA"
|
||||
r="4.889"
|
||||
/>
|
||||
<path
|
||||
d="M11.26 11.717h2.37v-.848c0-.313-.116-.58-.348-.8a1.17 1.17 0 0 0-.838-.332c-.327 0-.606.11-.838.332a1.066 1.066 0 0 0-.347.8v.848Zm3.851.424v2.546a.4.4 0 0 1-.13.3.44.44 0 0 1-.314.124h-4.445a.44.44 0 0 1-.315-.124.4.4 0 0 1-.13-.3V12.14a.4.4 0 0 1 .13-.3.44.44 0 0 1 .315-.124h.148v-.848c0-.542.204-1.008.612-1.397a2.044 2.044 0 0 1 1.462-.583c.568 0 1.056.194 1.463.583.408.39.611.855.611 1.397v.848h.149a.44.44 0 0 1 .315.124.4.4 0 0 1 .13.3Z"
|
||||
fill="#555"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<rect
|
||||
fill="#fff"
|
||||
height="16"
|
||||
rx="2"
|
||||
width="16"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`AutofillOverlayButton initAutofillOverlayButton creates the button element with the normal icon when the user's auth status is Unlocked 1`] = `
|
||||
<button
|
||||
aria-label="toggleBitwardenVaultOverlay"
|
||||
class="overlay-button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="overlay-button-svg-icon logo-icon"
|
||||
fill="none"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
width="14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
<path
|
||||
d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
@@ -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 = `<autofill-overlay-button></autofill-overlay-button>`;
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Bitwarden overlay button</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="color-scheme" content="normal" />
|
||||
</head>
|
||||
<body>
|
||||
<autofill-inline-menu-button></autofill-inline-menu-button>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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`] = `
|
||||
<div
|
||||
class="overlay-list-container theme_light"
|
||||
>
|
||||
<ul
|
||||
class="overlay-actions-list"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
class="overlay-actions-list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="partialUsername, username1"
|
||||
aria-label="fillCredentialsFor website login 1"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 1"
|
||||
>
|
||||
website login 1
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
title="username1"
|
||||
>
|
||||
username1
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 1, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="overlay-actions-list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="partialUsername, username2"
|
||||
aria-label="fillCredentialsFor website login 2"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon bwi bw-icon"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 2"
|
||||
>
|
||||
website login 2
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
title="username2"
|
||||
>
|
||||
username2
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 2, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="overlay-actions-list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="partialUsername, "
|
||||
aria-label="fillCredentialsFor "
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon bwi bw-icon"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view , opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="overlay-actions-list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="partialUsername, username4"
|
||||
aria-label="fillCredentialsFor website login 4"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M18.026 17.842c-1.418 1.739-3.517 2.84-5.86 2.84a7.364 7.364 0 0 1-3.431-.848l.062-.15.062-.151.063-.157c.081-.203.17-.426.275-.646.133-.28.275-.522.426-.68.026-.028.101-.075.275-.115.165-.037.376-.059.629-.073.138-.008.288-.014.447-.02.399-.016.847-.034 1.266-.092.314-.044.566-.131.755-.271a.884.884 0 0 0 .352-.555c.037-.2.008-.392-.03-.543-.035-.135-.084-.264-.12-.355l-.01-.03a4.26 4.26 0 0 0-.145-.33c-.126-.264-.237-.497-.288-1.085-.03-.344.09-.73.251-1.138l.089-.22c.05-.123.1-.247.14-.355.064-.171.129-.375.129-.566a1.51 1.51 0 0 0-.134-.569 2.573 2.573 0 0 0-.319-.547c-.246-.323-.635-.669-1.093-.669-.44 0-1.006.169-1.487.368-.246.102-.48.216-.68.33-.192.111-.372.235-.492.359-.93.96-1.48 1.239-1.81 1.258-.277.017-.478-.15-.736-.525a9.738 9.738 0 0 1-.19-.29l-.006-.01a11.568 11.568 0 0 0-.198-.305 2.76 2.76 0 0 0-.521-.6 1.39 1.39 0 0 0-1.088-.314 8.302 8.302 0 0 1 1.987-3.936c.055.342.146.626.272.856.23.42.561.64.926.716.406.086.857-.061 1.26-.216.125-.047.248-.097.372-.147.309-.125.618-.25.947-.341.26-.072.581-.057.959.012.264.049.529.118.8.19l.36.091c.379.094.782.178 1.135.148.374-.032.733-.197.934-.623a.874.874 0 0 0 .024-.752c-.087-.197-.24-.355-.35-.47-.26-.267-.412-.427-.412-.685 0-.125.037-.2.09-.263a.982.982 0 0 1 .303-.211c.059-.03.119-.058.183-.089l.036-.016a3.79 3.79 0 0 0 .236-.118c.047-.026.098-.056.148-.093 1.936.747 3.51 2.287 4.368 4.249a7.739 7.739 0 0 0-.031-.004c-.38-.047-.738-.056-1.063.061-.34.123-.603.368-.817.74-.122.211-.284.43-.463.67l-.095.129c-.207.281-.431.595-.58.92-.15.326-.245.705-.142 1.103.104.397.387.738.837 1.036.099.065.225.112.314.145l.02.008c.108.04.195.074.268.117.07.042.106.08.124.114.017.03.037.087.022.206-.047.376-.069.73-.052 1.034.017.292.071.59.218.809.118.174.12.421.108.786v.01a2.46 2.46 0 0 0 .021.518.809.809 0 0 0 .15.35Zm1.357.059a9.654 9.654 0 0 0 1.62-5.386c0-5.155-3.957-9.334-8.836-9.334-4.88 0-8.836 4.179-8.836 9.334 0 3.495 1.82 6.543 4.513 8.142v.093h.161a8.426 8.426 0 0 0 4.162 1.098c2.953 0 5.568-1.53 7.172-3.882a.569.569 0 0 0 .048-.062l-.004-.003ZM8.152 19.495a43.345 43.345 0 0 1 .098-.238l.057-.142c.082-.205.182-.455.297-.698.143-.301.323-.624.552-.864.163-.172.392-.254.602-.302.219-.05.473-.073.732-.088.162-.01.328-.016.495-.023.386-.015.782-.03 1.168-.084.255-.036.392-.099.461-.15.06-.045.076-.084.083-.12a.534.534 0 0 0-.02-.223 2.552 2.552 0 0 0-.095-.278l-.01-.027a3.128 3.128 0 0 0-.104-.232c-.134-.282-.31-.65-.374-1.381-.046-.533.138-1.063.3-1.472.035-.09.069-.172.1-.249.046-.11.086-.21.123-.31.062-.169.083-.264.083-.312a.812.812 0 0 0-.076-.283 1.867 1.867 0 0 0-.23-.394c-.21-.274-.428-.408-.577-.408-.315 0-.788.13-1.246.32a5.292 5.292 0 0 0-.603.293 1.727 1.727 0 0 0-.347.244c-.936.968-1.641 1.421-2.235 1.457-.646.04-1.036-.413-1.31-.813-.07-.103-.139-.21-.203-.311l-.005-.007c-.064-.101-.125-.197-.188-.29a2.098 2.098 0 0 0-.387-.453.748.748 0 0 0-.436-.18c-.1-.006-.22.005-.365.046a8.707 8.707 0 0 0-.056.992c0 2.957 1.488 5.547 3.716 6.98Zm10.362-2.316.003-.192.002-.046c.01-.305.026-.786-.232-1.169-.036-.054-.082-.189-.096-.444-.014-.243.003-.55.047-.9a1.051 1.051 0 0 0-.105-.649.987.987 0 0 0-.374-.374 2.285 2.285 0 0 0-.367-.166h-.003a1.243 1.243 0 0 1-.205-.088c-.369-.244-.505-.46-.549-.629-.044-.168-.015-.364.099-.61.115-.25.297-.511.507-.796l.089-.12c.178-.239.368-.495.512-.745.152-.263.302-.382.466-.441.18-.065.416-.073.77-.03.142.018.275.04.397.063.274.837.423 1.736.423 2.671a8.45 8.45 0 0 1-1.384 4.665Zm-4.632-12.63a7.362 7.362 0 0 0-1.715-.201c-1.89 0-3.621.716-4.965 1.905.025.54.12.887.24 1.105.13.238.295.34.482.38.2.042.484-.027.905-.188l.328-.13c.32-.13.681-.275 1.048-.377.398-.111.833-.075 1.24 0 .289.053.59.132.871.205l.326.084c.383.094.694.151.932.13.216-.017.326-.092.395-.237.039-.083.027-.114.014-.144-.027-.062-.088-.136-.212-.264l-.043-.044c-.218-.222-.567-.578-.567-1.142 0-.305.101-.547.262-.734.137-.159.308-.267.46-.347Z"
|
||||
fill="#777"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 4"
|
||||
>
|
||||
website login 4
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
title="username4"
|
||||
>
|
||||
username4
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 4, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="overlay-actions-list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="partialUsername, username5"
|
||||
aria-label="fillCredentialsFor website login 5"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 5"
|
||||
>
|
||||
website login 5
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
title="username5"
|
||||
>
|
||||
username5
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 5, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="overlay-actions-list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="partialUsername, username6"
|
||||
aria-label="fillCredentialsFor website login 6"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 6"
|
||||
>
|
||||
website login 6
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
title="username6"
|
||||
>
|
||||
username6
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 6, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an unauthenticated user creates the views for the locked overlay 1`] = `
|
||||
<div
|
||||
class="overlay-list-container theme_light"
|
||||
>
|
||||
<div
|
||||
class="locked-overlay overlay-list-message"
|
||||
id="locked-overlay-description"
|
||||
>
|
||||
unlockYourAccount
|
||||
</div>
|
||||
<div
|
||||
class="overlay-list-button-container"
|
||||
>
|
||||
<button
|
||||
aria-label="unlockAccount, opensInANewWindow"
|
||||
class="unlock-button overlay-list-button"
|
||||
id="unlock-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="17"
|
||||
viewBox="0 0 17 17"
|
||||
width="17"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M8.799 11.633a.68.68 0 0 0-.639.422.695.695 0 0 0-.054.264.682.682 0 0 0 .374.6v1.13a.345.345 0 1 0 .693 0v-1.17a.68.68 0 0 0 .315-.56.695.695 0 0 0-.204-.486.682.682 0 0 0-.485-.2Zm4.554-4.657h-7.11a.438.438 0 0 1-.406-.26A3.81 3.81 0 0 1 5.584 4.3c.112-.435.312-.842.588-1.195A3.196 3.196 0 0 1 7.19 2.25a3.468 3.468 0 0 1 3.225-.059A3.62 3.62 0 0 1 11.94 3.71l.327.59a.502.502 0 1 0 .885-.483l-.307-.552a4.689 4.689 0 0 0-2.209-2.078 4.466 4.466 0 0 0-3.936.185A4.197 4.197 0 0 0 5.37 2.49a4.234 4.234 0 0 0-.768 1.565 4.714 4.714 0 0 0 .162 2.682.182.182 0 0 1-.085.22.173.173 0 0 1-.082.02h-.353a1.368 1.368 0 0 0-1.277.842c-.07.168-.107.348-.109.53v7.1a1.392 1.392 0 0 0 .412.974 1.352 1.352 0 0 0 .974.394h9.117c.363.001.711-.142.97-.4a1.39 1.39 0 0 0 .407-.972v-7.1a1.397 1.397 0 0 0-.414-.973 1.368 1.368 0 0 0-.972-.396Zm.37 8.469a.373.373 0 0 1-.11.26.364.364 0 0 1-.26.107H4.246a.366.366 0 0 1-.26-.107.374.374 0 0 1-.11-.261V8.349a.374.374 0 0 1 .11-.26.366.366 0 0 1 .26-.108h9.116a.366.366 0 0 1 .37.367l-.008 7.097Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M.798.817h16v16h-16z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
unlockAccount
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty list of ciphers creates the views for the no results overlay 1`] = `
|
||||
<div
|
||||
class="overlay-list-container theme_light"
|
||||
>
|
||||
<div
|
||||
class="no-items overlay-list-message"
|
||||
>
|
||||
noItemsToShow
|
||||
</div>
|
||||
<div
|
||||
class="overlay-list-button-container"
|
||||
>
|
||||
<button
|
||||
aria-label="addNewVaultItem, opensInANewWindow"
|
||||
class="add-new-item-button overlay-list-button"
|
||||
id="new-item-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="17"
|
||||
width="17"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
|
||||
fill="#175DDC"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M.421.421h16v16h-16z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
newItem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -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 = `<autofill-overlay-list></autofill-overlay-list>`;
|
||||
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<HTMLElement>(),
|
||||
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/",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Bitwarden vault</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="color-scheme" content="normal" />
|
||||
</head>
|
||||
<body>
|
||||
<autofill-inline-menu-list></autofill-inline-menu-list>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "<autofill-overlay-page-element></autofill-overlay-page-element>";
|
||||
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<OverlayButtonWindowMessageHandlers>(),
|
||||
);
|
||||
|
||||
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<OverlayButtonWindowMessageHandlers>({
|
||||
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<OverlayButtonWindowMessageHandlers>({
|
||||
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<OverlayButtonWindowMessageHandlers>({
|
||||
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<OverlayButtonWindowMessageHandlers>(),
|
||||
);
|
||||
|
||||
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<OverlayButtonWindowMessageHandlers>(),
|
||||
);
|
||||
|
||||
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<OverlayButtonWindowMessageHandlers>(),
|
||||
);
|
||||
|
||||
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<OverlayButtonWindowMessageHandlers>(),
|
||||
);
|
||||
|
||||
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<OverlayButtonWindowMessageHandlers>(),
|
||||
);
|
||||
|
||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "redirectOverlayFocusOut", direction: "current" },
|
||||
"https://jest-testing-website.com",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, string>;
|
||||
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<string, string>,
|
||||
): 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;
|
||||
@@ -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<FormFieldElement>,
|
||||
autofillFieldData: AutofillField,
|
||||
pageDetails: AutofillPageDetails,
|
||||
): Promise<void>;
|
||||
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 };
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -43,10 +43,7 @@
|
||||
</a>
|
||||
</bit-hint>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
*ngIf="inlineMenuPositioningImprovementsEnabled && enableInlineMenu"
|
||||
class="tw-ml-5"
|
||||
>
|
||||
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
|
||||
<input
|
||||
bitCheckbox
|
||||
id="show-inline-menu-identities"
|
||||
@@ -58,10 +55,7 @@
|
||||
{{ "showInlineMenuIdentitiesLabel" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
*ngIf="inlineMenuPositioningImprovementsEnabled && enableInlineMenu"
|
||||
class="tw-ml-5"
|
||||
>
|
||||
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
|
||||
<input
|
||||
bitCheckbox
|
||||
id="show-inline-menu-cards"
|
||||
|
||||
@@ -92,7 +92,6 @@ export class AutofillComponent implements OnInit {
|
||||
protected defaultBrowserAutofillDisabled: boolean = false;
|
||||
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
|
||||
AutofillOverlayVisibility.OnFieldFocus;
|
||||
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
|
||||
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
|
||||
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
|
||||
protected disablePasswordManagerURI: DisablePasswordManagerUri =
|
||||
@@ -180,21 +179,17 @@ export class AutofillComponent implements OnInit {
|
||||
this.autofillSettingsService.inlineMenuVisibility$,
|
||||
);
|
||||
|
||||
this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.InlineMenuPositioningImprovements,
|
||||
);
|
||||
|
||||
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.BlockBrowserInjectionsByDomain,
|
||||
);
|
||||
|
||||
this.showInlineMenuIdentities =
|
||||
this.inlineMenuPositioningImprovementsEnabled &&
|
||||
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
|
||||
this.showInlineMenuIdentities = await firstValueFrom(
|
||||
this.autofillSettingsService.showInlineMenuIdentities$,
|
||||
);
|
||||
|
||||
this.showInlineMenuCards =
|
||||
this.inlineMenuPositioningImprovementsEnabled &&
|
||||
(await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$));
|
||||
this.showInlineMenuCards = await firstValueFrom(
|
||||
this.autofillSettingsService.showInlineMenuCards$,
|
||||
);
|
||||
|
||||
this.enableInlineMenuOnIconSelect =
|
||||
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick;
|
||||
|
||||
@@ -284,15 +284,6 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
inlineMenuVisibility = await this.getInlineMenuVisibility();
|
||||
}
|
||||
|
||||
const inlineMenuPositioningImprovements = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.InlineMenuPositioningImprovements,
|
||||
);
|
||||
if (!inlineMenuPositioningImprovements) {
|
||||
return !inlineMenuVisibility
|
||||
? "bootstrap-autofill.js"
|
||||
: "bootstrap-legacy-autofill-overlay.js";
|
||||
}
|
||||
|
||||
const enableChangedPasswordPrompt = await firstValueFrom(
|
||||
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
||||
);
|
||||
|
||||
@@ -245,7 +245,6 @@ import WebRequestBackground from "../autofill/background/web-request.background"
|
||||
import { CipherContextMenuHandler } from "../autofill/browser/cipher-context-menu-handler";
|
||||
import { ContextMenuClickedHandler } from "../autofill/browser/context-menu-clicked-handler";
|
||||
import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-handler";
|
||||
import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated";
|
||||
import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background";
|
||||
import { Fido2Background } from "../autofill/fido2/background/fido2.background";
|
||||
import {
|
||||
@@ -1716,45 +1715,26 @@ export default class MainBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.InlineMenuPositioningImprovements,
|
||||
this.overlayBackground = new OverlayBackground(
|
||||
this.logService,
|
||||
this.cipherService,
|
||||
this.autofillService,
|
||||
this.authService,
|
||||
this.environmentService,
|
||||
this.domainSettingsService,
|
||||
this.autofillSettingsService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.vaultSettingsService,
|
||||
this.fido2ActiveRequestManager,
|
||||
this.inlineMenuFieldQualificationService,
|
||||
this.themeStateService,
|
||||
this.totpService,
|
||||
this.accountService,
|
||||
() => this.generatePassword(),
|
||||
(password) => this.addPasswordToHistory(password),
|
||||
);
|
||||
|
||||
if (!inlineMenuPositioningImprovementsEnabled) {
|
||||
this.overlayBackground = new LegacyOverlayBackground(
|
||||
this.cipherService,
|
||||
this.autofillService,
|
||||
this.authService,
|
||||
this.environmentService,
|
||||
this.domainSettingsService,
|
||||
this.autofillSettingsService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.themeStateService,
|
||||
this.accountService,
|
||||
);
|
||||
} else {
|
||||
this.overlayBackground = new OverlayBackground(
|
||||
this.logService,
|
||||
this.cipherService,
|
||||
this.autofillService,
|
||||
this.authService,
|
||||
this.environmentService,
|
||||
this.domainSettingsService,
|
||||
this.autofillSettingsService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.vaultSettingsService,
|
||||
this.fido2ActiveRequestManager,
|
||||
this.inlineMenuFieldQualificationService,
|
||||
this.themeStateService,
|
||||
this.totpService,
|
||||
this.accountService,
|
||||
() => this.generatePassword(),
|
||||
(password) => this.addPasswordToHistory(password),
|
||||
);
|
||||
}
|
||||
|
||||
this.tabsBackground = new TabsBackground(
|
||||
this,
|
||||
this.notificationBackground,
|
||||
|
||||
@@ -79,12 +79,7 @@
|
||||
"__safari__optional_permissions": null,
|
||||
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
||||
"sandbox": {
|
||||
"pages": [
|
||||
"overlay/menu-button.html",
|
||||
"overlay/menu-list.html",
|
||||
"overlay/button.html",
|
||||
"overlay/list.html"
|
||||
],
|
||||
"pages": ["overlay/menu-button.html", "overlay/menu-list.html"],
|
||||
"content_security_policy": "sandbox allow-scripts; script-src 'self'"
|
||||
},
|
||||
"__firefox__sandbox": null,
|
||||
@@ -140,8 +135,6 @@
|
||||
"overlay/menu-button.html",
|
||||
"overlay/menu-list.html",
|
||||
"overlay/menu.html",
|
||||
"overlay/button.html",
|
||||
"overlay/list.html",
|
||||
"popup/fonts/*"
|
||||
],
|
||||
"__firefox__browser_specific_settings": {
|
||||
|
||||
@@ -105,12 +105,7 @@
|
||||
"__chrome__sandbox": "sandbox allow-scripts; script-src 'self'"
|
||||
},
|
||||
"sandbox": {
|
||||
"pages": [
|
||||
"overlay/menu-button.html",
|
||||
"overlay/menu-list.html",
|
||||
"overlay/button.html",
|
||||
"overlay/list.html"
|
||||
]
|
||||
"pages": ["overlay/menu-button.html", "overlay/menu-list.html"]
|
||||
},
|
||||
"__firefox__sandbox": null,
|
||||
"commands": {
|
||||
@@ -167,8 +162,6 @@
|
||||
"overlay/menu-button.html",
|
||||
"overlay/menu-list.html",
|
||||
"overlay/menu.html",
|
||||
"overlay/button.html",
|
||||
"overlay/list.html",
|
||||
"popup/fonts/*"
|
||||
],
|
||||
"matches": ["<all_urls>"]
|
||||
|
||||
@@ -135,16 +135,6 @@ const plugins = [
|
||||
filename: "overlay/menu.html",
|
||||
chunks: ["overlay/menu"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/deprecated/overlay/pages/button/legacy-button.html",
|
||||
filename: "overlay/button.html",
|
||||
chunks: ["overlay/button"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/deprecated/overlay/pages/list/legacy-list.html",
|
||||
filename: "overlay/list.html",
|
||||
chunks: ["overlay/list"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
@@ -197,8 +187,6 @@ const mainConfig = {
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
||||
"content/bootstrap-autofill-overlay-notifications":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
|
||||
"content/bootstrap-legacy-autofill-overlay":
|
||||
"./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts",
|
||||
"content/autofiller": "./src/autofill/content/autofiller.ts",
|
||||
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||
@@ -213,10 +201,6 @@ const mainConfig = {
|
||||
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
|
||||
"overlay/menu":
|
||||
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
|
||||
"overlay/button":
|
||||
"./src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts",
|
||||
"overlay/list":
|
||||
"./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts",
|
||||
"encrypt-worker": "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
|
||||
|
||||
@@ -24,7 +24,6 @@ export enum FeatureFlag {
|
||||
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
NotificationRefresh = "notification-refresh",
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||
@@ -89,7 +88,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
|
||||
Reference in New Issue
Block a user