mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-27569] Typing cleanup (#17087)
* typing cleanup * additional cleanup * more typing fixes * revert notification background changes * fix DOM query service breakage * do not run a fill_by_opid action if there is a nullish or empty value attribute * type cleanup * cleanup per review suggestions * remove unused flag check * add non-null assertion signposts * additional cleanup
This commit is contained in:
@@ -147,7 +147,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
||||
bgGetActiveUserServerConfig: () => Promise<ServerConfig>;
|
||||
bgGetActiveUserServerConfig: () => Promise<ServerConfig | null>;
|
||||
getWebVaultUrlForNotification: () => Promise<string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { InlineMenuFillType } from "../../enums/autofill-overlay.enum";
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
import { PageDetail } from "../../services/abstractions/autofill.service";
|
||||
|
||||
import { LockedVaultPendingNotificationsData } from "./notification.background";
|
||||
|
||||
export type PageDetailsForTab = Record<
|
||||
chrome.runtime.MessageSender["tab"]["id"],
|
||||
Map<chrome.runtime.MessageSender["frameId"], PageDetail>
|
||||
>;
|
||||
export type TabId = NonNullable<chrome.tabs.Tab["id"]>;
|
||||
|
||||
export type FrameId = NonNullable<chrome.runtime.MessageSender["frameId"]>;
|
||||
|
||||
type PageDetailsByFrame = Map<FrameId, PageDetail>;
|
||||
|
||||
export type PageDetailsForTab = Record<TabId, PageDetailsByFrame>;
|
||||
|
||||
export type SubFrameOffsetData = {
|
||||
top: number;
|
||||
@@ -21,19 +24,14 @@ export type SubFrameOffsetData = {
|
||||
url?: string;
|
||||
frameId?: number;
|
||||
parentFrameIds?: number[];
|
||||
isCrossOriginSubframe?: boolean;
|
||||
isMainFrame?: boolean;
|
||||
hasParentFrame?: boolean;
|
||||
} | null;
|
||||
|
||||
export type SubFrameOffsetsForTab = Record<
|
||||
chrome.runtime.MessageSender["tab"]["id"],
|
||||
Map<chrome.runtime.MessageSender["frameId"], SubFrameOffsetData>
|
||||
>;
|
||||
type SubFrameOffsetsByFrame = Map<FrameId, SubFrameOffsetData>;
|
||||
|
||||
export type WebsiteIconData = {
|
||||
imageEnabled: boolean;
|
||||
image: string;
|
||||
fallbackImage: string;
|
||||
icon: string;
|
||||
};
|
||||
export type SubFrameOffsetsForTab = Record<TabId, SubFrameOffsetsByFrame>;
|
||||
|
||||
export type UpdateOverlayCiphersParams = {
|
||||
updateAllCipherTypes: boolean;
|
||||
@@ -146,7 +144,7 @@ export type OverlayBackgroundExtensionMessage = {
|
||||
isFieldCurrentlyFilling?: boolean;
|
||||
subFrameData?: SubFrameOffsetData;
|
||||
focusedFieldData?: FocusedFieldData;
|
||||
allFieldsRect?: any;
|
||||
allFieldsRect?: AutofillField[];
|
||||
isOpeningFullInlineMenu?: boolean;
|
||||
styles?: Partial<CSSStyleDeclaration>;
|
||||
data?: LockedVaultPendingNotificationsData;
|
||||
@@ -155,13 +153,30 @@ export type OverlayBackgroundExtensionMessage = {
|
||||
ToggleInlineMenuHiddenMessage &
|
||||
UpdateInlineMenuVisibilityMessage;
|
||||
|
||||
export type OverlayPortCommand =
|
||||
| "fillCipher"
|
||||
| "addNewVaultItem"
|
||||
| "viewCipher"
|
||||
| "redirectFocus"
|
||||
| "updateHeight"
|
||||
| "buttonClicked"
|
||||
| "blurred"
|
||||
| "updateColorScheme"
|
||||
| "unlockVault"
|
||||
| "refreshGeneratedPassword"
|
||||
| "fillGeneratedPassword";
|
||||
|
||||
export type OverlayPortMessage = {
|
||||
[key: string]: any;
|
||||
command: string;
|
||||
direction?: string;
|
||||
command: OverlayPortCommand;
|
||||
direction?: "up" | "down" | "left" | "right";
|
||||
inlineMenuCipherId?: string;
|
||||
addNewCipherType?: CipherType;
|
||||
usePasskey?: boolean;
|
||||
height?: number;
|
||||
backgroundColorScheme?: "light" | "dark";
|
||||
viewsCipherData?: InlineMenuCipherData;
|
||||
loginUrl?: string;
|
||||
fillGeneratedPassword?: boolean;
|
||||
};
|
||||
|
||||
export type InlineMenuCipherData = {
|
||||
@@ -170,7 +185,7 @@ export type InlineMenuCipherData = {
|
||||
type: CipherType;
|
||||
reprompt: CipherRepromptType;
|
||||
favorite: boolean;
|
||||
icon: WebsiteIconData;
|
||||
icon: CipherIconDetails;
|
||||
accountCreationFieldType?: string;
|
||||
login?: {
|
||||
totp?: string;
|
||||
@@ -201,9 +216,14 @@ export type BuildCipherDataParams = {
|
||||
export type BackgroundMessageParam = {
|
||||
message: OverlayBackgroundExtensionMessage;
|
||||
};
|
||||
|
||||
export type BackgroundSenderParam = {
|
||||
sender: chrome.runtime.MessageSender;
|
||||
sender: chrome.runtime.MessageSender & {
|
||||
tab: NonNullable<chrome.runtime.MessageSender["tab"]>;
|
||||
frameId: FrameId;
|
||||
};
|
||||
};
|
||||
|
||||
export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
|
||||
|
||||
export type OverlayBackgroundExtensionMessageHandlers = {
|
||||
@@ -253,9 +273,13 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
||||
export type PortMessageParam = {
|
||||
message: OverlayPortMessage;
|
||||
};
|
||||
|
||||
export type PortConnectionParam = {
|
||||
port: chrome.runtime.Port;
|
||||
port: chrome.runtime.Port & {
|
||||
sender: NonNullable<chrome.runtime.Port["sender"]>;
|
||||
};
|
||||
};
|
||||
|
||||
export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
|
||||
|
||||
export type InlineMenuButtonPortMessageHandlers = {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
|
||||
|
||||
@@ -17,9 +15,11 @@ export default class ContextMenusBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
this.contextMenus.onClicked.addListener((info, tab) =>
|
||||
this.contextMenuClickedHandler.run(info, tab),
|
||||
);
|
||||
this.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (tab) {
|
||||
return this.contextMenuClickedHandler.run(info, tab);
|
||||
}
|
||||
});
|
||||
|
||||
BrowserApi.messageListener(
|
||||
"contextmenus.background",
|
||||
@@ -28,18 +28,16 @@ export default class ContextMenusBackground {
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) => {
|
||||
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
|
||||
// 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.contextMenuClickedHandler
|
||||
.cipherAction(
|
||||
msg.data.commandToRetry.message.contextMenuOnClickData,
|
||||
msg.data.commandToRetry.sender.tab,
|
||||
)
|
||||
.then(() => {
|
||||
// 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, "closeNotificationBar");
|
||||
const onClickData = msg.data.commandToRetry.message.contextMenuOnClickData;
|
||||
const senderTab = msg.data.commandToRetry.sender.tab;
|
||||
|
||||
if (onClickData && senderTab) {
|
||||
void this.contextMenuClickedHandler.cipherAction(onClickData, senderTab).then(() => {
|
||||
if (sender.tab) {
|
||||
void BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -39,9 +39,7 @@ describe("TabsBackground", () => {
|
||||
"handleWindowOnFocusChanged",
|
||||
);
|
||||
|
||||
// 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
|
||||
tabsBackground.init();
|
||||
void tabsBackground.init();
|
||||
|
||||
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
|
||||
handleWindowOnFocusChangedSpy,
|
||||
|
||||
@@ -191,9 +191,11 @@ export class ContextMenuClickedHandler {
|
||||
});
|
||||
} else {
|
||||
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
||||
// 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.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
|
||||
|
||||
void this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientCopiedPassword,
|
||||
cipher.id,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 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";
|
||||
@@ -179,9 +177,11 @@ export class MainContextMenuHandler {
|
||||
|
||||
try {
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const hasPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
);
|
||||
const hasPremium =
|
||||
!!account?.id &&
|
||||
(await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
));
|
||||
|
||||
const isCardRestricted = (
|
||||
await firstValueFrom(this.restrictedItemTypesService.restricted$)
|
||||
@@ -198,14 +198,16 @@ export class MainContextMenuHandler {
|
||||
if (requiresPremiumAccess && !hasPremium) {
|
||||
continue;
|
||||
}
|
||||
if (menuItem.id.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) {
|
||||
if (menuItem.id?.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
if (error instanceof Error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
} finally {
|
||||
this.initRunning = false;
|
||||
}
|
||||
@@ -318,9 +320,11 @@ export class MainContextMenuHandler {
|
||||
}
|
||||
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
);
|
||||
const canAccessPremium =
|
||||
!!account?.id &&
|
||||
(await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
));
|
||||
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
|
||||
await createChildItem(COPY_VERIFICATION_CODE_ID);
|
||||
}
|
||||
@@ -333,7 +337,9 @@ export class MainContextMenuHandler {
|
||||
await createChildItem(AUTOFILL_IDENTITY_ID);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
if (error instanceof Error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +357,11 @@ export class MainContextMenuHandler {
|
||||
this.loadOptions(
|
||||
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
).catch((error) => this.logService.warning(error.message));
|
||||
).catch((error) => {
|
||||
if (error instanceof Error) {
|
||||
return this.logService.warning(error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +373,9 @@ export class MainContextMenuHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
if (error instanceof Error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +385,9 @@ export class MainContextMenuHandler {
|
||||
await MainContextMenuHandler.create(menuItem);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
if (error instanceof Error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +397,9 @@ export class MainContextMenuHandler {
|
||||
await MainContextMenuHandler.create(menuItem);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
if (error instanceof Error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +411,9 @@ export class MainContextMenuHandler {
|
||||
|
||||
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
||||
} catch (error) {
|
||||
this.logService.warning(error.message);
|
||||
if (error instanceof Error) {
|
||||
this.logService.warning(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 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 AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -123,9 +121,9 @@ import {
|
||||
* @param fillScript - The autofill script to use
|
||||
*/
|
||||
function triggerAutoSubmitOnForm(fillScript: AutofillScript) {
|
||||
const formOpid = fillScript.autosubmit[0];
|
||||
const formOpid = fillScript.autosubmit?.[0];
|
||||
|
||||
if (formOpid === null) {
|
||||
if (!formOpid) {
|
||||
triggerAutoSubmitOnFormlessFields(fillScript);
|
||||
return;
|
||||
}
|
||||
@@ -159,8 +157,11 @@ import {
|
||||
fillScript.script[fillScript.script.length - 1][1],
|
||||
);
|
||||
|
||||
const lastFieldIsPasswordInput =
|
||||
elementIsInputElement(currentElement) && currentElement.type === "password";
|
||||
const lastFieldIsPasswordInput = !!(
|
||||
currentElement &&
|
||||
elementIsInputElement(currentElement) &&
|
||||
currentElement.type === "password"
|
||||
);
|
||||
|
||||
while (currentElement && currentElement.tagName !== "HTML") {
|
||||
if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
|
||||
export const CipherTypes = {
|
||||
Login: 1,
|
||||
SecureNote: 2,
|
||||
@@ -22,20 +24,13 @@ export const OrganizationCategories = {
|
||||
family: "family",
|
||||
} as const;
|
||||
|
||||
export type WebsiteIconData = {
|
||||
imageEnabled: boolean;
|
||||
image: string;
|
||||
fallbackImage: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
type BaseCipherData<CipherTypeValue> = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: CipherTypeValue;
|
||||
reprompt: CipherRepromptType;
|
||||
favorite: boolean;
|
||||
icon: WebsiteIconData;
|
||||
icon: CipherIconDetails;
|
||||
};
|
||||
|
||||
export type CipherData = BaseCipherData<CipherType> & {
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
const inputTags = ["input", "textarea", "select"];
|
||||
const labelTags = ["label", "span"];
|
||||
const attributes = ["id", "name", "label-aria", "placeholder"];
|
||||
const attributeKeys = ["id", "name", "label-aria", "placeholder"];
|
||||
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
|
||||
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
|
||||
|
||||
let clickedEl: HTMLElement = null;
|
||||
let clickedElement: HTMLElement | null = null;
|
||||
|
||||
// Find the best attribute to be used as the Name for an element in a custom field.
|
||||
function getClickedElementIdentifier() {
|
||||
if (clickedEl == null) {
|
||||
if (clickedElement == null) {
|
||||
return invalidElement;
|
||||
}
|
||||
|
||||
const clickedTag = clickedEl.nodeName.toLowerCase();
|
||||
let inputEl = null;
|
||||
const clickedTag = clickedElement.nodeName.toLowerCase();
|
||||
let inputElement = null;
|
||||
|
||||
// Try to identify the input element (which may not be the clicked element)
|
||||
if (labelTags.includes(clickedTag)) {
|
||||
let inputId = null;
|
||||
let inputId;
|
||||
if (clickedTag === "label") {
|
||||
inputId = clickedEl.getAttribute("for");
|
||||
inputId = clickedElement.getAttribute("for");
|
||||
} else {
|
||||
inputId = clickedEl.closest("label")?.getAttribute("for");
|
||||
inputId = clickedElement.closest("label")?.getAttribute("for");
|
||||
}
|
||||
|
||||
inputEl = document.getElementById(inputId);
|
||||
if (inputId) {
|
||||
inputElement = document.getElementById(inputId);
|
||||
}
|
||||
} else {
|
||||
inputEl = clickedEl;
|
||||
inputElement = clickedElement;
|
||||
}
|
||||
|
||||
if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) {
|
||||
if (inputElement == null || !inputTags.includes(inputElement.nodeName.toLowerCase())) {
|
||||
return invalidElement;
|
||||
}
|
||||
|
||||
for (const attr of attributes) {
|
||||
const attributeValue = inputEl.getAttribute(attr);
|
||||
const selector = "[" + attr + '="' + attributeValue + '"]';
|
||||
for (const attributeKey of attributeKeys) {
|
||||
const attributeValue = inputElement.getAttribute(attributeKey);
|
||||
const selector = "[" + attributeKey + '="' + attributeValue + '"]';
|
||||
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
|
||||
return attributeValue;
|
||||
}
|
||||
@@ -45,14 +45,14 @@ function getClickedElementIdentifier() {
|
||||
return noUniqueIdentifier;
|
||||
}
|
||||
|
||||
function isNullOrEmpty(s: string) {
|
||||
function isNullOrEmpty(s: string | null) {
|
||||
return s == null || s === "";
|
||||
}
|
||||
|
||||
// We only have access to the element that's been clicked when the context menu is first opened.
|
||||
// Remember it for use later.
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
clickedEl = event.target as HTMLElement;
|
||||
clickedElement = event.target as HTMLElement;
|
||||
});
|
||||
|
||||
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
|
||||
@@ -62,9 +62,8 @@ chrome.runtime.onMessage.addListener((event, _sender, sendResponse) => {
|
||||
if (sendResponse) {
|
||||
sendResponse(identifier);
|
||||
}
|
||||
// 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
|
||||
chrome.runtime.sendMessage({
|
||||
|
||||
void chrome.runtime.sendMessage({
|
||||
command: "getClickedElementResponse",
|
||||
sender: "contextMenuHandler",
|
||||
identifier: identifier,
|
||||
|
||||
@@ -267,9 +267,7 @@ import { Messenger } from "./messaging/messenger";
|
||||
|
||||
clearWaitForFocus();
|
||||
void messenger.destroy();
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
} catch {
|
||||
/** empty */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,8 @@ describe("Messenger", () => {
|
||||
|
||||
it("should deliver message to B when sending request from A", () => {
|
||||
const request = createRequest();
|
||||
// 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
|
||||
messengerA.request(request);
|
||||
|
||||
void messengerA.request(request);
|
||||
|
||||
const received = handlerB.receive();
|
||||
|
||||
@@ -66,14 +65,13 @@ describe("Messenger", () => {
|
||||
|
||||
it("should deliver abort signal to B when requesting abort", () => {
|
||||
const abortController = new AbortController();
|
||||
// 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
|
||||
messengerA.request(createRequest(), abortController.signal);
|
||||
|
||||
void messengerA.request(createRequest(), abortController.signal);
|
||||
abortController.abort();
|
||||
|
||||
const received = handlerB.receive();
|
||||
|
||||
expect(received[0].abortController.signal.aborted).toBe(true);
|
||||
expect(received[0].abortController?.signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
@@ -103,29 +101,25 @@ describe("Messenger", () => {
|
||||
|
||||
it("should dispatch the destroy event on messenger destruction", async () => {
|
||||
const request = createRequest();
|
||||
// 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
|
||||
messengerA.request(request);
|
||||
|
||||
void messengerA.request(request);
|
||||
|
||||
const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent");
|
||||
// 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
|
||||
messengerA.destroy();
|
||||
|
||||
void messengerA.destroy();
|
||||
|
||||
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event));
|
||||
});
|
||||
|
||||
it("should trigger onDestroyListener when the destroy event is dispatched", async () => {
|
||||
const request = createRequest();
|
||||
// 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
|
||||
messengerA.request(request);
|
||||
|
||||
void messengerA.request(request);
|
||||
|
||||
const onDestroyListener = jest.fn();
|
||||
(messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener);
|
||||
// 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
|
||||
messengerA.destroy();
|
||||
|
||||
void messengerA.destroy();
|
||||
|
||||
expect(onDestroyListener).toHaveBeenCalled();
|
||||
const eventArg = onDestroyListener.mock.calls[0][0];
|
||||
@@ -213,7 +207,7 @@ class MockMessagePort<T> {
|
||||
remotePort: MockMessagePort<T>;
|
||||
|
||||
postMessage(message: T, port?: MessagePort) {
|
||||
this.remotePort.onmessage(
|
||||
this.remotePort.onmessage?.(
|
||||
new MessageEvent("message", {
|
||||
data: message,
|
||||
ports: port ? [port] : [],
|
||||
|
||||
@@ -155,9 +155,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
}
|
||||
|
||||
static sendMessage(msg: BrowserFido2Message) {
|
||||
// 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.sendMessage(BrowserFido2MessageName, msg);
|
||||
void BrowserApi.sendMessage(BrowserFido2MessageName, msg);
|
||||
}
|
||||
|
||||
static abortPopout(sessionId: string, fallbackRequested = false) {
|
||||
@@ -206,9 +204,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
fromEvent(abortController.signal, "abort")
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
// 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.close();
|
||||
void this.close();
|
||||
BrowserFido2UserInterfaceSession.sendMessage({
|
||||
type: BrowserFido2MessageTypes.AbortRequest,
|
||||
sessionId: this.sessionId,
|
||||
@@ -224,12 +220,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
)
|
||||
.subscribe((msg) => {
|
||||
if (msg.type === BrowserFido2MessageTypes.AbortResponse) {
|
||||
// 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.close();
|
||||
// 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.abort(msg.fallbackRequested);
|
||||
void this.close();
|
||||
void this.abort(msg.fallbackRequested);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -388,12 +380,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(() => {
|
||||
// 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.close();
|
||||
// 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.abort(true);
|
||||
void this.close();
|
||||
void this.abort(true);
|
||||
});
|
||||
|
||||
await connectPromise;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { FieldRect } from "../background/abstractions/overlay.background";
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
||||
import {
|
||||
InlineMenuAccountCreationFieldTypes,
|
||||
@@ -13,34 +11,36 @@ import {
|
||||
export default class AutofillField {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* The unique identifier assigned to this field during collection of the page details
|
||||
* Non-null asserted. The unique identifier assigned to this field during collection of the page details
|
||||
*/
|
||||
opid: string;
|
||||
opid!: string;
|
||||
/**
|
||||
* Sequential number assigned to each element collected, based on its position in the DOM.
|
||||
* Non-null asserted. Sequential number assigned to each element collected, based on its position in the DOM.
|
||||
* Used to do perform proximal checks for username and password fields on the DOM.
|
||||
*/
|
||||
elementNumber: number;
|
||||
elementNumber!: number;
|
||||
/**
|
||||
* Designates whether the field is viewable on the current part of the DOM that the user can see
|
||||
* Non-null asserted. Designates whether the field is viewable on the current part of the DOM that the user can see
|
||||
*/
|
||||
viewable: boolean;
|
||||
viewable!: boolean;
|
||||
/**
|
||||
* The HTML `id` attribute of the field
|
||||
* Non-null asserted. The HTML `id` attribute of the field
|
||||
*/
|
||||
htmlID: string | null;
|
||||
htmlID!: string | null;
|
||||
/**
|
||||
* The HTML `name` attribute of the field
|
||||
* Non-null asserted. The HTML `name` attribute of the field
|
||||
*/
|
||||
htmlName: string | null;
|
||||
htmlName!: string | null;
|
||||
/**
|
||||
* The HTML `class` attribute of the field
|
||||
* Non-null asserted. The HTML `class` attribute of the field
|
||||
*/
|
||||
htmlClass: string | null;
|
||||
htmlClass!: string | null;
|
||||
|
||||
tabindex: string | null;
|
||||
/** Non-null asserted. */
|
||||
tabindex!: string | null;
|
||||
|
||||
title: string | null;
|
||||
/** Non-null asserted. */
|
||||
title!: string | null;
|
||||
/**
|
||||
* The `tagName` for the field
|
||||
*/
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
/**
|
||||
* Represents an HTML form whose elements can be autofilled
|
||||
*/
|
||||
export default class AutofillForm {
|
||||
[key: string]: any;
|
||||
|
||||
/**
|
||||
* The unique identifier assigned to this field during collection of the page details
|
||||
* Non-null asserted. The unique identifier assigned to this field during collection of the page details
|
||||
*/
|
||||
opid: string;
|
||||
opid!: string;
|
||||
|
||||
/**
|
||||
* The HTML `name` attribute of the form field
|
||||
* Non-null asserted. The HTML `name` attribute of the form field
|
||||
*/
|
||||
htmlName: string;
|
||||
htmlName!: string;
|
||||
|
||||
/**
|
||||
* The HTML `id` attribute of the form field
|
||||
* Non-null asserted. The HTML `id` attribute of the form field
|
||||
*/
|
||||
htmlID: string;
|
||||
htmlID!: string;
|
||||
|
||||
/**
|
||||
* The HTML `action` attribute of the form field
|
||||
* Non-null asserted. The HTML `action` attribute of the form field
|
||||
*/
|
||||
htmlAction: string;
|
||||
htmlAction!: string;
|
||||
|
||||
/**
|
||||
* The HTML `method` attribute of the form field
|
||||
* Non-null asserted. The HTML `method` attribute of the form field.
|
||||
*/
|
||||
htmlMethod: string;
|
||||
htmlMethod!: "get" | "post" | string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import AutofillField from "./autofill-field";
|
||||
import AutofillForm from "./autofill-form";
|
||||
|
||||
@@ -7,16 +5,20 @@ import AutofillForm from "./autofill-form";
|
||||
* The details of a page that have been collected and can be used for autofill
|
||||
*/
|
||||
export default class AutofillPageDetails {
|
||||
title: string;
|
||||
url: string;
|
||||
documentUrl: string;
|
||||
/** Non-null asserted. */
|
||||
title!: string;
|
||||
/** Non-null asserted. */
|
||||
url!: string;
|
||||
/** Non-null asserted. */
|
||||
documentUrl!: string;
|
||||
/**
|
||||
* A collection of all of the forms in the page DOM, keyed by their `opid`
|
||||
* Non-null asserted. A collection of all of the forms in the page DOM, keyed by their `opid`
|
||||
*/
|
||||
forms: { [id: string]: AutofillForm };
|
||||
forms!: { [id: string]: AutofillForm };
|
||||
/**
|
||||
* A collection of all the fields in the page DOM, keyed by their `opid`
|
||||
* Non-null asserted. A collection of all the fields in the page DOM, keyed by their `opid`
|
||||
*/
|
||||
fields: AutofillField[];
|
||||
collectedTimestamp: number;
|
||||
fields!: AutofillField[];
|
||||
/** Non-null asserted. */
|
||||
collectedTimestamp!: number;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// String values affect code flow in autofill.ts and must not be changed
|
||||
export type FillScriptActions = "click_on_opid" | "focus_by_opid" | "fill_by_opid";
|
||||
|
||||
export type FillScript = [action: FillScriptActions, opid: string, value?: string];
|
||||
|
||||
export type AutofillScriptProperties = {
|
||||
delay_between_operations?: number;
|
||||
};
|
||||
|
||||
export const FillScriptActionTypes = {
|
||||
fill_by_opid: "fill_by_opid",
|
||||
click_on_opid: "click_on_opid",
|
||||
focus_by_opid: "focus_by_opid",
|
||||
} as const;
|
||||
|
||||
// String values affect code flow in autofill.ts and must not be changed
|
||||
export type FillScriptActions = keyof typeof FillScriptActionTypes;
|
||||
|
||||
export type AutofillInsertActions = {
|
||||
fill_by_opid: ({ opid, value }: { opid: string; value: string }) => void;
|
||||
click_on_opid: ({ opid }: { opid: string }) => void;
|
||||
focus_by_opid: ({ opid }: { opid: string }) => void;
|
||||
[FillScriptActionTypes.fill_by_opid]: ({ opid, value }: { opid: string; value: string }) => void;
|
||||
[FillScriptActionTypes.click_on_opid]: ({ opid }: { opid: string }) => void;
|
||||
[FillScriptActionTypes.focus_by_opid]: ({ opid }: { opid: string }) => void;
|
||||
};
|
||||
|
||||
export default class AutofillScript {
|
||||
script: FillScript[] = [];
|
||||
properties: AutofillScriptProperties = {};
|
||||
metadata: any = {}; // Unused, not written or read
|
||||
autosubmit: string[]; // Appears to be unused, read but not written
|
||||
savedUrls: string[];
|
||||
untrustedIframe: boolean;
|
||||
itemType: string; // Appears to be unused, read but not written
|
||||
/** Non-null asserted. */
|
||||
autosubmit!: string[] | null; // Appears to be unused, read but not written
|
||||
/** Non-null asserted. */
|
||||
savedUrls!: string[];
|
||||
/** Non-null asserted. */
|
||||
untrustedIframe!: boolean;
|
||||
/** Non-null asserted. */
|
||||
itemType!: string; // Appears to be unused, read but not written
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 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";
|
||||
@@ -103,7 +101,10 @@ export class AutofillInlineMenuButton extends AutofillInlineMenuPageElement {
|
||||
*/
|
||||
private updatePageColorScheme({ colorScheme }: AutofillInlineMenuButtonMessage) {
|
||||
const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']");
|
||||
colorSchemeMetaTag?.setAttribute("content", colorScheme);
|
||||
|
||||
if (colorSchemeMetaTag && colorScheme) {
|
||||
colorSchemeMetaTag.setAttribute("content", colorScheme);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 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 { setElementStyles } from "../../../../utils";
|
||||
@@ -14,8 +12,10 @@ export class AutofillInlineMenuContainer {
|
||||
private readonly setElementStyles = setElementStyles;
|
||||
private readonly extensionOriginsSet: Set<string>;
|
||||
private port: chrome.runtime.Port | null = null;
|
||||
private portName: string;
|
||||
private inlineMenuPageIframe: HTMLIFrameElement;
|
||||
/** Non-null asserted. */
|
||||
private portName!: string;
|
||||
/** Non-null asserted. */
|
||||
private inlineMenuPageIframe!: HTMLIFrameElement;
|
||||
private readonly iframeStyles: Partial<CSSStyleDeclaration> = {
|
||||
all: "initial",
|
||||
position: "fixed",
|
||||
@@ -42,8 +42,10 @@ export class AutofillInlineMenuContainer {
|
||||
tabIndex: "-1",
|
||||
};
|
||||
private readonly windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = {
|
||||
initAutofillInlineMenuButton: (message) => this.handleInitInlineMenuIframe(message),
|
||||
initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message),
|
||||
initAutofillInlineMenuButton: (message: InitAutofillInlineMenuElementMessage) =>
|
||||
this.handleInitInlineMenuIframe(message),
|
||||
initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) =>
|
||||
this.handleInitInlineMenuIframe(message),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@@ -116,14 +118,20 @@ export class AutofillInlineMenuContainer {
|
||||
*
|
||||
* @param event - The message event.
|
||||
*/
|
||||
private handleWindowMessage = (event: MessageEvent) => {
|
||||
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
||||
const message = event.data;
|
||||
if (this.isForeignWindowMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.windowMessageHandlers[message.command]) {
|
||||
this.windowMessageHandlers[message.command](message);
|
||||
if (
|
||||
this.windowMessageHandlers[
|
||||
message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers
|
||||
]
|
||||
) {
|
||||
this.windowMessageHandlers[
|
||||
message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers
|
||||
](message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,8 +150,8 @@ export class AutofillInlineMenuContainer {
|
||||
*
|
||||
* @param event - The message event.
|
||||
*/
|
||||
private isForeignWindowMessage(event: MessageEvent) {
|
||||
if (!event.data.portKey) {
|
||||
private isForeignWindowMessage(event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) {
|
||||
if (!event.data?.portKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -159,7 +167,9 @@ export class AutofillInlineMenuContainer {
|
||||
*
|
||||
* @param event - The message event.
|
||||
*/
|
||||
private isMessageFromParentWindow(event: MessageEvent): boolean {
|
||||
private isMessageFromParentWindow(
|
||||
event: MessageEvent<AutofillInlineMenuContainerWindowMessage>,
|
||||
): boolean {
|
||||
return globalThis.parent === event.source;
|
||||
}
|
||||
|
||||
@@ -168,7 +178,9 @@ export class AutofillInlineMenuContainer {
|
||||
*
|
||||
* @param event - The message event.
|
||||
*/
|
||||
private isMessageFromInlineMenuPageIframe(event: MessageEvent): boolean {
|
||||
private isMessageFromInlineMenuPageIframe(
|
||||
event: MessageEvent<AutofillInlineMenuContainerWindowMessage>,
|
||||
): boolean {
|
||||
if (!this.inlineMenuPageIframe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 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";
|
||||
@@ -10,10 +8,14 @@ import {
|
||||
|
||||
export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||
protected shadowDom: ShadowRoot;
|
||||
protected messageOrigin: string;
|
||||
protected translations: Record<string, string>;
|
||||
private portKey: string;
|
||||
protected windowMessageHandlers: AutofillInlineMenuPageElementWindowMessageHandlers;
|
||||
/** Non-null asserted. */
|
||||
protected messageOrigin!: string;
|
||||
/** Non-null asserted. */
|
||||
protected translations!: Record<string, string>;
|
||||
/** Non-null asserted. */
|
||||
private portKey!: string;
|
||||
/** Non-null asserted. */
|
||||
protected windowMessageHandlers!: AutofillInlineMenuPageElementWindowMessageHandlers;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn());
|
||||
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(async () => null);
|
||||
jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
|
||||
postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
|
||||
domQueryService = mock<DomQueryService>();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
@@ -69,7 +67,7 @@ export class Fido2UseBrowserLinkComponent {
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
"",
|
||||
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,13 +155,15 @@ export class AutofillComponent implements OnInit {
|
||||
autofillOnPageLoadOptions: { name: string; value: boolean }[];
|
||||
enableContextMenuItem: boolean = false;
|
||||
enableAutoTotpCopy: boolean = false;
|
||||
clearClipboard: ClearClipboardDelaySetting;
|
||||
/** Non-null asserted. */
|
||||
clearClipboard!: ClearClipboardDelaySetting;
|
||||
clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[];
|
||||
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||
uriMatchOptions: { name: string; value: UriMatchStrategySetting; disabled?: boolean }[];
|
||||
showCardsCurrentTab: boolean = true;
|
||||
showIdentitiesCurrentTab: boolean = true;
|
||||
autofillKeyboardHelperText: string;
|
||||
/** Non-null asserted. */
|
||||
autofillKeyboardHelperText!: string;
|
||||
accountSwitcherEnabled: boolean = false;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -26,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||
destroyAutofillInlineMenuListeners: () => void;
|
||||
getInlineMenuFormFieldData: ({
|
||||
message,
|
||||
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
|
||||
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData | void>;
|
||||
};
|
||||
|
||||
export interface AutofillOverlayContentService {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
|
||||
@@ -64,29 +62,39 @@ export const COLLECT_PAGE_DETAILS_RESPONSE_COMMAND =
|
||||
);
|
||||
|
||||
export abstract class AutofillService {
|
||||
collectPageDetailsFromTab$: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
|
||||
loadAutofillScriptsOnInstall: () => Promise<void>;
|
||||
reloadAutofillScripts: () => Promise<void>;
|
||||
injectAutofillScripts: (
|
||||
/** Non-null asserted. */
|
||||
collectPageDetailsFromTab$!: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
|
||||
/** Non-null asserted. */
|
||||
loadAutofillScriptsOnInstall!: () => Promise<void>;
|
||||
/** Non-null asserted. */
|
||||
reloadAutofillScripts!: () => Promise<void>;
|
||||
/** Non-null asserted. */
|
||||
injectAutofillScripts!: (
|
||||
tab: chrome.tabs.Tab,
|
||||
frameId?: number,
|
||||
triggeringOnPageLoad?: boolean,
|
||||
) => Promise<void>;
|
||||
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
|
||||
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
|
||||
doAutoFillOnTab: (
|
||||
/** Non-null asserted. */
|
||||
getFormsWithPasswordFields!: (pageDetails: AutofillPageDetails) => FormData[];
|
||||
/** Non-null asserted. */
|
||||
doAutoFill!: (options: AutoFillOptions) => Promise<string | null>;
|
||||
/** Non-null asserted. */
|
||||
doAutoFillOnTab!: (
|
||||
pageDetails: PageDetail[],
|
||||
tab: chrome.tabs.Tab,
|
||||
fromCommand: boolean,
|
||||
autoSubmitLogin?: boolean,
|
||||
) => Promise<string | null>;
|
||||
doAutoFillActiveTab: (
|
||||
/** Non-null asserted. */
|
||||
doAutoFillActiveTab!: (
|
||||
pageDetails: PageDetail[],
|
||||
fromCommand: boolean,
|
||||
cipherType?: CipherType,
|
||||
) => Promise<string | null>;
|
||||
setAutoFillOnPageLoadOrgPolicy: () => Promise<void>;
|
||||
isPasswordRepromptRequired: (
|
||||
/** Non-null asserted. */
|
||||
setAutoFillOnPageLoadOrgPolicy!: () => Promise<void>;
|
||||
/** Non-null asserted. */
|
||||
isPasswordRepromptRequired!: (
|
||||
cipher: CipherView,
|
||||
tab: chrome.tabs.Tab,
|
||||
action?: string,
|
||||
|
||||
@@ -369,9 +369,7 @@ describe("AutofillService", () => {
|
||||
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
autofillService.reloadAutofillScripts();
|
||||
void autofillService.reloadAutofillScripts();
|
||||
|
||||
expect(port1.disconnect).toHaveBeenCalled();
|
||||
expect(port2.disconnect).toHaveBeenCalled();
|
||||
@@ -680,7 +678,9 @@ describe("AutofillService", () => {
|
||||
await autofillService.doAutoFill(autofillOptions);
|
||||
triggerTestFailure();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -691,7 +691,9 @@ describe("AutofillService", () => {
|
||||
await autofillService.doAutoFill(autofillOptions);
|
||||
triggerTestFailure();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -702,7 +704,9 @@ describe("AutofillService", () => {
|
||||
await autofillService.doAutoFill(autofillOptions);
|
||||
triggerTestFailure();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -713,7 +717,9 @@ describe("AutofillService", () => {
|
||||
await autofillService.doAutoFill(autofillOptions);
|
||||
triggerTestFailure();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(nothingToAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -727,7 +733,9 @@ describe("AutofillService", () => {
|
||||
await autofillService.doAutoFill(autofillOptions);
|
||||
triggerTestFailure();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(didNotAutofillError);
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(didNotAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -766,7 +774,6 @@ describe("AutofillService", () => {
|
||||
{
|
||||
command: "fillForm",
|
||||
fillScript: {
|
||||
metadata: {},
|
||||
properties: {
|
||||
delay_between_operations: 20,
|
||||
},
|
||||
@@ -863,7 +870,9 @@ describe("AutofillService", () => {
|
||||
expect(logService.info).toHaveBeenCalledWith(
|
||||
"Autofill on page load was blocked due to an untrusted iframe.",
|
||||
);
|
||||
expect(error.message).toBe(didNotAutofillError);
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(didNotAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -898,7 +907,10 @@ describe("AutofillService", () => {
|
||||
} catch (error) {
|
||||
expect(autofillService["generateFillScript"]).toHaveBeenCalled();
|
||||
expect(BrowserApi.tabSendMessage).not.toHaveBeenCalled();
|
||||
expect(error.message).toBe(didNotAutofillError);
|
||||
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe(didNotAutofillError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1370,7 +1382,10 @@ describe("AutofillService", () => {
|
||||
triggerTestFailure();
|
||||
} catch (error) {
|
||||
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
|
||||
expect(error.message).toBe("No tab found.");
|
||||
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).toBe("No tab found.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1610,7 +1625,6 @@ describe("AutofillService", () => {
|
||||
|
||||
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith(
|
||||
{
|
||||
metadata: {},
|
||||
properties: {},
|
||||
script: [
|
||||
["click_on_opid", "username-field"],
|
||||
@@ -1648,7 +1662,6 @@ describe("AutofillService", () => {
|
||||
|
||||
expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith(
|
||||
{
|
||||
metadata: {},
|
||||
properties: {},
|
||||
script: [
|
||||
["click_on_opid", "username-field"],
|
||||
@@ -1686,7 +1699,6 @@ describe("AutofillService", () => {
|
||||
|
||||
expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith(
|
||||
{
|
||||
metadata: {},
|
||||
properties: {},
|
||||
script: [
|
||||
["click_on_opid", "username-field"],
|
||||
@@ -2279,7 +2291,7 @@ describe("AutofillService", () => {
|
||||
);
|
||||
expect(value).toStrictEqual({
|
||||
autosubmit: null,
|
||||
metadata: {},
|
||||
itemType: "",
|
||||
properties: { delay_between_operations: 20 },
|
||||
savedUrls: ["https://www.example.com"],
|
||||
script: [
|
||||
@@ -2294,7 +2306,6 @@ describe("AutofillService", () => {
|
||||
["fill_by_opid", "password", "password"],
|
||||
["focus_by_opid", "password"],
|
||||
],
|
||||
itemType: "",
|
||||
untrustedIframe: false,
|
||||
});
|
||||
});
|
||||
@@ -2364,11 +2375,10 @@ describe("AutofillService", () => {
|
||||
describe("given an invalid autofill field", () => {
|
||||
const unmodifiedFillScriptValues: AutofillScript = {
|
||||
autosubmit: null,
|
||||
metadata: {},
|
||||
itemType: "",
|
||||
properties: { delay_between_operations: 20 },
|
||||
savedUrls: [],
|
||||
script: [],
|
||||
itemType: "",
|
||||
untrustedIframe: false,
|
||||
};
|
||||
|
||||
@@ -2555,7 +2565,6 @@ describe("AutofillService", () => {
|
||||
expect(value).toStrictEqual({
|
||||
autosubmit: null,
|
||||
itemType: "",
|
||||
metadata: {},
|
||||
properties: {
|
||||
delay_between_operations: 20,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
|
||||
import { FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
|
||||
@@ -202,7 +200,7 @@ class DomElementVisibilityService implements DomElementVisibilityServiceInterfac
|
||||
|
||||
const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label");
|
||||
|
||||
return targetElementLabelsSet.has(closestParentLabel);
|
||||
return closestParentLabel ? targetElementLabelsSet.has(closestParentLabel) : false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS, MAX_DEEP_QUERY_RECURSION_DEPTH } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { nodeIsElement } from "../utils";
|
||||
@@ -7,7 +5,8 @@ import { nodeIsElement } from "../utils";
|
||||
import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service";
|
||||
|
||||
export class DomQueryService implements DomQueryServiceInterface {
|
||||
private pageContainsShadowDom: boolean;
|
||||
/** Non-null asserted. */
|
||||
private pageContainsShadowDom!: boolean;
|
||||
private ignoredTreeWalkerNodes = new Set([
|
||||
"svg",
|
||||
"script",
|
||||
@@ -217,13 +216,12 @@ export class DomQueryService implements DomQueryServiceInterface {
|
||||
if ((chrome as any).dom?.openOrClosedShadowRoot) {
|
||||
try {
|
||||
return (chrome as any).dom.openOrClosedShadowRoot(node);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox-specific equivalent of `openOrClosedShadowRoot`
|
||||
return (node as any).openOrClosedShadowRoot;
|
||||
}
|
||||
|
||||
@@ -276,7 +274,7 @@ export class DomQueryService implements DomQueryServiceInterface {
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT,
|
||||
);
|
||||
let currentNode = treeWalker?.currentNode;
|
||||
let currentNode: Node | null = treeWalker?.currentNode;
|
||||
|
||||
while (currentNode) {
|
||||
if (filterCallback(currentNode)) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils";
|
||||
@@ -162,12 +160,14 @@ export class InlineMenuFieldQualificationService
|
||||
private isExplicitIdentityEmailField(field: AutofillField): boolean {
|
||||
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
||||
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
||||
if (!matchFieldAttributeValues[attrIndex]) {
|
||||
const attributeValueToMatch = matchFieldAttributeValues[attrIndex];
|
||||
|
||||
if (!attributeValueToMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
|
||||
if (this.newEmailFieldKeywords.has(matchFieldAttributeValues[attrIndex])) {
|
||||
if (this.newEmailFieldKeywords.has(attributeValueToMatch)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -210,10 +210,7 @@ export class InlineMenuFieldQualificationService
|
||||
}
|
||||
|
||||
constructor() {
|
||||
void Promise.all([
|
||||
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
|
||||
sendExtensionMessage("getUserPremiumStatus"),
|
||||
]).then(([fieldQualificationFlag, premiumStatus]) => {
|
||||
void sendExtensionMessage("getUserPremiumStatus").then((premiumStatus) => {
|
||||
this.premiumEnabled = !!premiumStatus?.result;
|
||||
});
|
||||
}
|
||||
@@ -263,7 +260,13 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
let parentForm;
|
||||
|
||||
const fieldForm = field.form;
|
||||
|
||||
if (fieldForm) {
|
||||
parentForm = pageDetails.forms[fieldForm];
|
||||
}
|
||||
|
||||
// If the field does not have a parent form
|
||||
if (!parentForm) {
|
||||
@@ -321,7 +324,13 @@ export class InlineMenuFieldQualificationService
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
let parentForm;
|
||||
|
||||
const fieldForm = field.form;
|
||||
|
||||
if (fieldForm) {
|
||||
parentForm = pageDetails.forms[fieldForm];
|
||||
}
|
||||
|
||||
if (!parentForm) {
|
||||
// If the field does not have a parent form, but we can identify that the page contains at least
|
||||
@@ -374,7 +383,13 @@ export class InlineMenuFieldQualificationService
|
||||
field: AutofillField,
|
||||
pageDetails: AutofillPageDetails,
|
||||
): boolean {
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
let parentForm;
|
||||
|
||||
const fieldForm = field.form;
|
||||
|
||||
if (fieldForm) {
|
||||
parentForm = pageDetails.forms[fieldForm];
|
||||
}
|
||||
|
||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||
@@ -476,7 +491,13 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
// If the field is not explicitly set as a username field, we need to qualify
|
||||
// the field based on the other fields that are present on the page.
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
let parentForm;
|
||||
|
||||
const fieldForm = field.form;
|
||||
|
||||
if (fieldForm) {
|
||||
parentForm = pageDetails.forms[fieldForm];
|
||||
}
|
||||
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
|
||||
|
||||
if (this.isNewsletterForm(parentForm)) {
|
||||
@@ -919,8 +940,10 @@ export class InlineMenuFieldQualificationService
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isUsernameField = (field: AutofillField): boolean => {
|
||||
const fieldType = field.type;
|
||||
if (
|
||||
!this.usernameFieldTypes.has(field.type) ||
|
||||
!fieldType ||
|
||||
!this.usernameFieldTypes.has(fieldType) ||
|
||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
|
||||
this.fieldHasDisqualifyingAttributeValue(field)
|
||||
) {
|
||||
@@ -1026,7 +1049,13 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
const testedValues = [field.htmlID, field.htmlName, field.placeholder];
|
||||
for (let i = 0; i < testedValues.length; i++) {
|
||||
if (this.valueIsLikePassword(testedValues[i])) {
|
||||
const attributeValueToMatch = testedValues[i];
|
||||
|
||||
if (!attributeValueToMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.valueIsLikePassword(attributeValueToMatch)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1101,7 +1130,9 @@ export class InlineMenuFieldQualificationService
|
||||
* @param excludedTypes - The set of excluded types
|
||||
*/
|
||||
private isExcludedFieldType(field: AutofillField, excludedTypes: Set<string>): boolean {
|
||||
if (excludedTypes.has(field.type)) {
|
||||
const fieldType = field.type;
|
||||
|
||||
if (fieldType && excludedTypes.has(fieldType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1116,12 +1147,14 @@ export class InlineMenuFieldQualificationService
|
||||
private isSearchField(field: AutofillField): boolean {
|
||||
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
||||
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
||||
if (!matchFieldAttributeValues[attrIndex]) {
|
||||
const attributeValueToMatch = matchFieldAttributeValues[attrIndex];
|
||||
|
||||
if (!attributeValueToMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Separate camel case words and case them to lower case values
|
||||
const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex]
|
||||
const camelCaseSeparatedFieldAttribute = attributeValueToMatch
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.toLowerCase();
|
||||
// Split the attribute by non-alphabetical characters to get the keywords
|
||||
@@ -1168,7 +1201,7 @@ export class InlineMenuFieldQualificationService
|
||||
this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(","));
|
||||
}
|
||||
|
||||
return this.submitButtonKeywordsMap.get(element);
|
||||
return this.submitButtonKeywordsMap.get(element) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1222,8 +1255,9 @@ export class InlineMenuFieldQualificationService
|
||||
];
|
||||
const keywordsSet = new Set<string>();
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
if (keywords[i] && typeof keywords[i] === "string") {
|
||||
let keywordEl = keywords[i].toLowerCase();
|
||||
const attributeValue = keywords[i];
|
||||
if (attributeValue && typeof attributeValue === "string") {
|
||||
let keywordEl = attributeValue.toLowerCase();
|
||||
keywordsSet.add(keywordEl);
|
||||
|
||||
// Remove hyphens from all potential keywords, we want to treat these as a single word.
|
||||
@@ -1253,7 +1287,7 @@ export class InlineMenuFieldQualificationService
|
||||
}
|
||||
|
||||
const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData);
|
||||
return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet;
|
||||
return mapValues ? (returnStringValue ? mapValues.stringValue : mapValues.keywordsSet) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script";
|
||||
import AutofillScript, { FillScript, FillScriptActionTypes } from "../models/autofill-script";
|
||||
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
|
||||
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
|
||||
|
||||
@@ -94,14 +94,13 @@ describe("InsertAutofillContentService", () => {
|
||||
);
|
||||
fillScript = {
|
||||
script: [
|
||||
["click_on_opid", "username"],
|
||||
["focus_by_opid", "username"],
|
||||
["fill_by_opid", "username", "test"],
|
||||
[FillScriptActionTypes.click_on_opid, "username"],
|
||||
[FillScriptActionTypes.focus_by_opid, "username"],
|
||||
[FillScriptActionTypes.fill_by_opid, "username", "test"],
|
||||
],
|
||||
properties: {
|
||||
delay_between_operations: 20,
|
||||
},
|
||||
metadata: {},
|
||||
autosubmit: [],
|
||||
savedUrls: ["https://bitwarden.com"],
|
||||
untrustedIframe: false,
|
||||
@@ -221,17 +220,14 @@ describe("InsertAutofillContentService", () => {
|
||||
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
fillScript.script[0],
|
||||
0,
|
||||
);
|
||||
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
fillScript.script[1],
|
||||
1,
|
||||
);
|
||||
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
fillScript.script[2],
|
||||
2,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -376,42 +372,62 @@ describe("InsertAutofillContentService", () => {
|
||||
});
|
||||
|
||||
it("returns early if no opid is provided", async () => {
|
||||
const action = "fill_by_opid";
|
||||
const action = FillScriptActionTypes.fill_by_opid;
|
||||
const opid = "";
|
||||
const value = "value";
|
||||
const scriptAction: FillScript = [action, opid, value];
|
||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||
|
||||
await insertAutofillContentService["runFillScriptAction"](scriptAction, 0);
|
||||
await insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||
jest.advanceTimersByTime(20);
|
||||
|
||||
expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("given a valid fill script action and opid", () => {
|
||||
const fillScriptActions: FillScriptActions[] = [
|
||||
"fill_by_opid",
|
||||
"click_on_opid",
|
||||
"focus_by_opid",
|
||||
];
|
||||
fillScriptActions.forEach((action) => {
|
||||
it(`triggers a ${action} action`, () => {
|
||||
const opid = "opid";
|
||||
const value = "value";
|
||||
const scriptAction: FillScript = [action, opid, value];
|
||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||
it(`triggers a fill_by_opid action`, () => {
|
||||
const action = FillScriptActionTypes.fill_by_opid;
|
||||
const opid = "opid";
|
||||
const value = "value";
|
||||
const scriptAction: FillScript = [action, opid, value];
|
||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||
|
||||
// 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
|
||||
insertAutofillContentService["runFillScriptAction"](scriptAction, 0);
|
||||
jest.advanceTimersByTime(20);
|
||||
void insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||
jest.advanceTimersByTime(20);
|
||||
|
||||
expect(
|
||||
insertAutofillContentService["autofillInsertActions"][action],
|
||||
).toHaveBeenCalledWith({
|
||||
opid,
|
||||
value,
|
||||
});
|
||||
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
|
||||
opid,
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
it(`triggers a click_on_opid action`, () => {
|
||||
const action = FillScriptActionTypes.click_on_opid;
|
||||
const opid = "opid";
|
||||
const value = "value";
|
||||
const scriptAction: FillScript = [action, opid, value];
|
||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||
|
||||
void insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||
jest.advanceTimersByTime(20);
|
||||
|
||||
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
|
||||
opid,
|
||||
});
|
||||
});
|
||||
|
||||
it(`triggers a focus_by_opid action`, () => {
|
||||
const action = FillScriptActionTypes.focus_by_opid;
|
||||
const opid = "opid";
|
||||
const value = "value";
|
||||
const scriptAction: FillScript = [action, opid, value];
|
||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||
|
||||
void insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||
jest.advanceTimersByTime(20);
|
||||
|
||||
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
|
||||
opid,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script";
|
||||
import AutofillScript, {
|
||||
AutofillInsertActions,
|
||||
FillScript,
|
||||
FillScriptActionTypes,
|
||||
} from "../models/autofill-script";
|
||||
import { FormFieldElement } from "../types";
|
||||
import {
|
||||
currentlyInSandboxedIframe,
|
||||
@@ -50,7 +52,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
}
|
||||
|
||||
for (let index = 0; index < fillScript.script.length; index++) {
|
||||
await this.runFillScriptAction(fillScript.script[index], index);
|
||||
await this.runFillScriptAction(fillScript.script[index]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,25 +118,26 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
/**
|
||||
* Runs the autofill action based on the action type and the opid.
|
||||
* Each action is subsequently delayed by 20 milliseconds.
|
||||
* @param {"click_on_opid" | "focus_by_opid" | "fill_by_opid"} action
|
||||
* @param {string} opid
|
||||
* @param {string} value
|
||||
* @param {number} actionIndex
|
||||
* @param {FillScript} [action, opid, value]
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private runFillScriptAction = (
|
||||
[action, opid, value]: FillScript,
|
||||
actionIndex: number,
|
||||
): Promise<void> => {
|
||||
private runFillScriptAction = ([action, opid, value]: FillScript): Promise<void> => {
|
||||
if (!opid || !this.autofillInsertActions[action]) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const delayActionsInMilliseconds = 20;
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
this.autofillInsertActions[action]({ opid, value });
|
||||
if (action === FillScriptActionTypes.fill_by_opid && !!value?.length) {
|
||||
this.autofillInsertActions.fill_by_opid({ opid, value });
|
||||
} else if (action === FillScriptActionTypes.click_on_opid) {
|
||||
this.autofillInsertActions.click_on_opid({ opid });
|
||||
} else if (action === FillScriptActionTypes.focus_by_opid) {
|
||||
this.autofillInsertActions.focus_by_opid({ opid });
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, delayActionsInMilliseconds),
|
||||
);
|
||||
@@ -158,7 +161,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
*/
|
||||
private handleClickOnFieldByOpidAction(opid: string) {
|
||||
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
|
||||
this.triggerClickOnElement(element);
|
||||
|
||||
if (element) {
|
||||
this.triggerClickOnElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,6 +177,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
private handleFocusOnFieldByOpidAction(opid: string) {
|
||||
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement === element) {
|
||||
element.blur();
|
||||
}
|
||||
@@ -187,6 +197,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
* @private
|
||||
*/
|
||||
private insertValueIntoField(element: FormFieldElement | null, value: string) {
|
||||
if (!element || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementCanBeReadonly =
|
||||
elementIsInputElement(element) || elementIsTextAreaElement(element);
|
||||
const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element);
|
||||
@@ -195,8 +209,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value);
|
||||
|
||||
if (
|
||||
!element ||
|
||||
!value ||
|
||||
elementAlreadyHasTheValue ||
|
||||
(elementCanBeReadonly && element.readOnly) ||
|
||||
(elementCanBeFilled && element.disabled)
|
||||
@@ -298,7 +310,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
* @private
|
||||
*/
|
||||
private triggerClickOnElement(element?: HTMLElement): void {
|
||||
if (typeof element?.click !== TYPE_CHECK.FUNCTION) {
|
||||
if (!element || typeof element.click !== TYPE_CHECK.FUNCTION) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,7 +325,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
* @private
|
||||
*/
|
||||
private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void {
|
||||
if (typeof element?.focus !== TYPE_CHECK.FUNCTION) {
|
||||
if (!element || typeof element.focus !== TYPE_CHECK.FUNCTION) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -144,7 +142,6 @@ export function createAutofillScriptMock(
|
||||
|
||||
return {
|
||||
autosubmit: null,
|
||||
metadata: {},
|
||||
properties: {
|
||||
delay_between_operations: 20,
|
||||
},
|
||||
@@ -299,7 +296,7 @@ export function createMutationRecordMock(customFields = {}): MutationRecord {
|
||||
oldValue: "default-oldValue",
|
||||
previousSibling: null,
|
||||
removedNodes: mock<NodeList>(),
|
||||
target: null,
|
||||
target: mock<Node>(),
|
||||
type: "attributes",
|
||||
...customFields,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user