mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53: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 MainBackground from "../../background/main.background";
|
||||||
|
|
||||||
import { OverlayBackground } from "./abstractions/overlay.background";
|
import { OverlayBackground } from "./abstractions/overlay.background";
|
||||||
@@ -14,7 +10,7 @@ export default class TabsBackground {
|
|||||||
private overlayBackground: OverlayBackground,
|
private overlayBackground: OverlayBackground,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private focusedWindowId: number;
|
private focusedWindowId: number = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the window and tab listeners.
|
* Initializes the window and tab listeners.
|
||||||
@@ -90,14 +86,6 @@ export default class TabsBackground {
|
|||||||
changeInfo: chrome.tabs.TabChangeInfo,
|
changeInfo: chrome.tabs.TabChangeInfo,
|
||||||
tab: chrome.tabs.Tab,
|
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) {
|
if (this.focusedWindowId > 0 && tab.windowId !== this.focusedWindowId) {
|
||||||
return;
|
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>
|
</a>
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control
|
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
|
||||||
*ngIf="inlineMenuPositioningImprovementsEnabled && enableInlineMenu"
|
|
||||||
class="tw-ml-5"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
id="show-inline-menu-identities"
|
id="show-inline-menu-identities"
|
||||||
@@ -58,10 +55,7 @@
|
|||||||
{{ "showInlineMenuIdentitiesLabel" | i18n }}
|
{{ "showInlineMenuIdentitiesLabel" | i18n }}
|
||||||
</bit-label>
|
</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control
|
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
|
||||||
*ngIf="inlineMenuPositioningImprovementsEnabled && enableInlineMenu"
|
|
||||||
class="tw-ml-5"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
id="show-inline-menu-cards"
|
id="show-inline-menu-cards"
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export class AutofillComponent implements OnInit {
|
|||||||
protected defaultBrowserAutofillDisabled: boolean = false;
|
protected defaultBrowserAutofillDisabled: boolean = false;
|
||||||
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
|
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
|
||||||
AutofillOverlayVisibility.OnFieldFocus;
|
AutofillOverlayVisibility.OnFieldFocus;
|
||||||
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
|
|
||||||
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
|
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
|
||||||
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
|
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
|
||||||
protected disablePasswordManagerURI: DisablePasswordManagerUri =
|
protected disablePasswordManagerURI: DisablePasswordManagerUri =
|
||||||
@@ -180,21 +179,17 @@ export class AutofillComponent implements OnInit {
|
|||||||
this.autofillSettingsService.inlineMenuVisibility$,
|
this.autofillSettingsService.inlineMenuVisibility$,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
|
|
||||||
FeatureFlag.InlineMenuPositioningImprovements,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
|
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
|
||||||
FeatureFlag.BlockBrowserInjectionsByDomain,
|
FeatureFlag.BlockBrowserInjectionsByDomain,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.showInlineMenuIdentities =
|
this.showInlineMenuIdentities = await firstValueFrom(
|
||||||
this.inlineMenuPositioningImprovementsEnabled &&
|
this.autofillSettingsService.showInlineMenuIdentities$,
|
||||||
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
|
);
|
||||||
|
|
||||||
this.showInlineMenuCards =
|
this.showInlineMenuCards = await firstValueFrom(
|
||||||
this.inlineMenuPositioningImprovementsEnabled &&
|
this.autofillSettingsService.showInlineMenuCards$,
|
||||||
(await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$));
|
);
|
||||||
|
|
||||||
this.enableInlineMenuOnIconSelect =
|
this.enableInlineMenuOnIconSelect =
|
||||||
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick;
|
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick;
|
||||||
|
|||||||
@@ -284,15 +284,6 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
inlineMenuVisibility = await this.getInlineMenuVisibility();
|
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(
|
const enableChangedPasswordPrompt = await firstValueFrom(
|
||||||
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
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 { CipherContextMenuHandler } from "../autofill/browser/cipher-context-menu-handler";
|
||||||
import { ContextMenuClickedHandler } from "../autofill/browser/context-menu-clicked-handler";
|
import { ContextMenuClickedHandler } from "../autofill/browser/context-menu-clicked-handler";
|
||||||
import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-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 as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background";
|
||||||
import { Fido2Background } from "../autofill/fido2/background/fido2.background";
|
import { Fido2Background } from "../autofill/fido2/background/fido2.background";
|
||||||
import {
|
import {
|
||||||
@@ -1716,45 +1715,26 @@ export default class MainBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
|
this.overlayBackground = new OverlayBackground(
|
||||||
FeatureFlag.InlineMenuPositioningImprovements,
|
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.tabsBackground = new TabsBackground(
|
||||||
this,
|
this,
|
||||||
this.notificationBackground,
|
this.notificationBackground,
|
||||||
|
|||||||
@@ -79,12 +79,7 @@
|
|||||||
"__safari__optional_permissions": null,
|
"__safari__optional_permissions": null,
|
||||||
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"pages": [
|
"pages": ["overlay/menu-button.html", "overlay/menu-list.html"],
|
||||||
"overlay/menu-button.html",
|
|
||||||
"overlay/menu-list.html",
|
|
||||||
"overlay/button.html",
|
|
||||||
"overlay/list.html"
|
|
||||||
],
|
|
||||||
"content_security_policy": "sandbox allow-scripts; script-src 'self'"
|
"content_security_policy": "sandbox allow-scripts; script-src 'self'"
|
||||||
},
|
},
|
||||||
"__firefox__sandbox": null,
|
"__firefox__sandbox": null,
|
||||||
@@ -140,8 +135,6 @@
|
|||||||
"overlay/menu-button.html",
|
"overlay/menu-button.html",
|
||||||
"overlay/menu-list.html",
|
"overlay/menu-list.html",
|
||||||
"overlay/menu.html",
|
"overlay/menu.html",
|
||||||
"overlay/button.html",
|
|
||||||
"overlay/list.html",
|
|
||||||
"popup/fonts/*"
|
"popup/fonts/*"
|
||||||
],
|
],
|
||||||
"__firefox__browser_specific_settings": {
|
"__firefox__browser_specific_settings": {
|
||||||
|
|||||||
@@ -105,12 +105,7 @@
|
|||||||
"__chrome__sandbox": "sandbox allow-scripts; script-src 'self'"
|
"__chrome__sandbox": "sandbox allow-scripts; script-src 'self'"
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"pages": [
|
"pages": ["overlay/menu-button.html", "overlay/menu-list.html"]
|
||||||
"overlay/menu-button.html",
|
|
||||||
"overlay/menu-list.html",
|
|
||||||
"overlay/button.html",
|
|
||||||
"overlay/list.html"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"__firefox__sandbox": null,
|
"__firefox__sandbox": null,
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -167,8 +162,6 @@
|
|||||||
"overlay/menu-button.html",
|
"overlay/menu-button.html",
|
||||||
"overlay/menu-list.html",
|
"overlay/menu-list.html",
|
||||||
"overlay/menu.html",
|
"overlay/menu.html",
|
||||||
"overlay/button.html",
|
|
||||||
"overlay/list.html",
|
|
||||||
"popup/fonts/*"
|
"popup/fonts/*"
|
||||||
],
|
],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
|
|||||||
@@ -135,16 +135,6 @@ const plugins = [
|
|||||||
filename: "overlay/menu.html",
|
filename: "overlay/menu.html",
|
||||||
chunks: ["overlay/menu"],
|
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({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
@@ -197,8 +187,6 @@ const mainConfig = {
|
|||||||
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
||||||
"content/bootstrap-autofill-overlay-notifications":
|
"content/bootstrap-autofill-overlay-notifications":
|
||||||
"./src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
|
"./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/autofiller": "./src/autofill/content/autofiller.ts",
|
||||||
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
||||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.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",
|
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
|
||||||
"overlay/menu":
|
"overlay/menu":
|
||||||
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
|
"./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",
|
"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-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||||
"content/send-popup-open-message": "./src/vault/content/send-popup-open-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",
|
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
||||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
|
||||||
NotificationRefresh = "notification-refresh",
|
NotificationRefresh = "notification-refresh",
|
||||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||||
@@ -89,7 +88,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
|
||||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||||
|
|||||||
Reference in New Issue
Block a user