mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-17 20:31:50 +01:00
[PM-8833] Implement on page autofill menu for password generation (#11114)
This commit is contained in:
parent
9264e6775c
commit
da1e508c25
@ -4599,5 +4599,158 @@
|
|||||||
},
|
},
|
||||||
"authenticating": {
|
"authenticating": {
|
||||||
"message": "Authenticating"
|
"message": "Authenticating"
|
||||||
|
},
|
||||||
|
"fillGeneratedPassword": {
|
||||||
|
"message": "Fill generated password",
|
||||||
|
"description": "Heading for the password generator within the inline menu"
|
||||||
|
},
|
||||||
|
"passwordRegenerated": {
|
||||||
|
"message": "Password regenerated",
|
||||||
|
"description": "Notification message for when a password has been regenerated"
|
||||||
|
},
|
||||||
|
"saveLoginToBitwarden": {
|
||||||
|
"message": "Save login to Bitwarden?",
|
||||||
|
"description": "Confirmation message for saving a login to Bitwarden"
|
||||||
|
},
|
||||||
|
"spaceCharacterDescriptor": {
|
||||||
|
"message": "Space",
|
||||||
|
"description": "Represents the space key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"tildeCharacterDescriptor": {
|
||||||
|
"message": "Tilde",
|
||||||
|
"description": "Represents the ~ key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"backtickCharacterDescriptor": {
|
||||||
|
"message": "Backtick",
|
||||||
|
"description": "Represents the ` key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"exclamationCharacterDescriptor": {
|
||||||
|
"message": "Exclamation mark",
|
||||||
|
"description": "Represents the ! key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"atSignCharacterDescriptor": {
|
||||||
|
"message": "At sign",
|
||||||
|
"description": "Represents the @ key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"hashSignCharacterDescriptor": {
|
||||||
|
"message": "Hash sign",
|
||||||
|
"description": "Represents the # key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"dollarSignCharacterDescriptor": {
|
||||||
|
"message": "Dollar sign",
|
||||||
|
"description": "Represents the $ key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"percentSignCharacterDescriptor": {
|
||||||
|
"message": "Percent sign",
|
||||||
|
"description": "Represents the % key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"caretCharacterDescriptor": {
|
||||||
|
"message": "Caret",
|
||||||
|
"description": "Represents the ^ key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"ampersandCharacterDescriptor": {
|
||||||
|
"message": "Ampersand",
|
||||||
|
"description": "Represents the & key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"asteriskCharacterDescriptor": {
|
||||||
|
"message": "Asterisk",
|
||||||
|
"description": "Represents the * key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"parenLeftCharacterDescriptor": {
|
||||||
|
"message": "Left parenthesis",
|
||||||
|
"description": "Represents the ( key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"parenRightCharacterDescriptor": {
|
||||||
|
"message": "Right parenthesis",
|
||||||
|
"description": "Represents the ) key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"hyphenCharacterDescriptor": {
|
||||||
|
"message": "Underscore",
|
||||||
|
"description": "Represents the _ key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"underscoreCharacterDescriptor": {
|
||||||
|
"message": "Hyphen",
|
||||||
|
"description": "Represents the - key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"plusCharacterDescriptor": {
|
||||||
|
"message": "Plus",
|
||||||
|
"description": "Represents the + key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"equalsCharacterDescriptor": {
|
||||||
|
"message": "Equals",
|
||||||
|
"description": "Represents the = key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"braceLeftCharacterDescriptor": {
|
||||||
|
"message": "Left brace",
|
||||||
|
"description": "Represents the { key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"braceRightCharacterDescriptor": {
|
||||||
|
"message": "Right brace",
|
||||||
|
"description": "Represents the } key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"bracketLeftCharacterDescriptor": {
|
||||||
|
"message": "Left bracket",
|
||||||
|
"description": "Represents the [ key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"bracketRightCharacterDescriptor": {
|
||||||
|
"message": "Right bracket",
|
||||||
|
"description": "Represents the ] key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"pipeCharacterDescriptor": {
|
||||||
|
"message": "Pipe",
|
||||||
|
"description": "Represents the | key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"backSlashCharacterDescriptor": {
|
||||||
|
"message": "Back slash",
|
||||||
|
"description": "Represents the back slash key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"colonCharacterDescriptor": {
|
||||||
|
"message": "Colon",
|
||||||
|
"description": "Represents the : key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"semicolonCharacterDescriptor": {
|
||||||
|
"message": "Semicolon",
|
||||||
|
"description": "Represents the ; key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"doubleQuoteCharacterDescriptor": {
|
||||||
|
"message": "Double quote",
|
||||||
|
"description": "Represents the double quote key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"singleQuoteCharacterDescriptor": {
|
||||||
|
"message": "Single quote",
|
||||||
|
"description": "Represents the ' key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"lessThanCharacterDescriptor": {
|
||||||
|
"message": "Less than",
|
||||||
|
"description": "Represents the < key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"greaterThanCharacterDescriptor": {
|
||||||
|
"message": "Greater than",
|
||||||
|
"description": "Represents the > key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"commaCharacterDescriptor": {
|
||||||
|
"message": "Comma",
|
||||||
|
"description": "Represents the , key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"periodCharacterDescriptor": {
|
||||||
|
"message": "Period",
|
||||||
|
"description": "Represents the . key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"questionCharacterDescriptor": {
|
||||||
|
"message": "Question mark",
|
||||||
|
"description": "Represents the ? key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"forwardSlashCharacterDescriptor": {
|
||||||
|
"message": "Forward slash",
|
||||||
|
"description": "Represents the / key in screen reader content as a readable word"
|
||||||
|
},
|
||||||
|
"lowercaseAriaLabel": {
|
||||||
|
"message": "Lowercase"
|
||||||
|
},
|
||||||
|
"uppercaseAriaLabel": {
|
||||||
|
"message": "Uppercase"
|
||||||
|
},
|
||||||
|
"generatedPassword": {
|
||||||
|
"message": "Generated password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
|||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
import { InlineMenuFillTypes } from "../../enums/autofill-overlay.enum";
|
||||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||||
import { PageDetail } from "../../services/abstractions/autofill.service";
|
import { PageDetail } from "../../services/abstractions/autofill.service";
|
||||||
|
|
||||||
@ -32,14 +33,18 @@ export type WebsiteIconData = {
|
|||||||
icon: string;
|
icon: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateOverlayCiphersParams = {
|
||||||
|
updateAllCipherTypes: boolean;
|
||||||
|
refocusField: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type FocusedFieldData = {
|
export type FocusedFieldData = {
|
||||||
focusedFieldStyles: Partial<CSSStyleDeclaration>;
|
focusedFieldStyles: Partial<CSSStyleDeclaration>;
|
||||||
focusedFieldRects: Partial<DOMRect>;
|
focusedFieldRects: Partial<DOMRect>;
|
||||||
filledByCipherType?: CipherType;
|
inlineMenuFillType?: InlineMenuFillTypes;
|
||||||
tabId?: number;
|
tabId?: number;
|
||||||
frameId?: number;
|
frameId?: number;
|
||||||
accountCreationFieldType?: string;
|
accountCreationFieldType?: string;
|
||||||
showInlineMenuAccountCreation?: boolean;
|
|
||||||
showPasskeys?: boolean;
|
showPasskeys?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,6 +116,12 @@ export type ToggleInlineMenuHiddenMessage = {
|
|||||||
setTransparentInlineMenu?: boolean;
|
setTransparentInlineMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateInlineMenuVisibilityMessage = {
|
||||||
|
overlayElement?: string;
|
||||||
|
isVisible?: boolean;
|
||||||
|
forceUpdate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type OverlayBackgroundExtensionMessage = {
|
export type OverlayBackgroundExtensionMessage = {
|
||||||
command: string;
|
command: string;
|
||||||
portKey?: string;
|
portKey?: string;
|
||||||
@ -119,14 +130,15 @@ export type OverlayBackgroundExtensionMessage = {
|
|||||||
details?: AutofillPageDetails;
|
details?: AutofillPageDetails;
|
||||||
isFieldCurrentlyFocused?: boolean;
|
isFieldCurrentlyFocused?: boolean;
|
||||||
isFieldCurrentlyFilling?: boolean;
|
isFieldCurrentlyFilling?: boolean;
|
||||||
isVisible?: boolean;
|
|
||||||
subFrameData?: SubFrameOffsetData;
|
subFrameData?: SubFrameOffsetData;
|
||||||
focusedFieldData?: FocusedFieldData;
|
focusedFieldData?: FocusedFieldData;
|
||||||
|
isOpeningFullInlineMenu?: boolean;
|
||||||
styles?: Partial<CSSStyleDeclaration>;
|
styles?: Partial<CSSStyleDeclaration>;
|
||||||
data?: LockedVaultPendingNotificationsData;
|
data?: LockedVaultPendingNotificationsData;
|
||||||
} & OverlayAddNewItemMessage &
|
} & OverlayAddNewItemMessage &
|
||||||
CloseInlineMenuMessage &
|
CloseInlineMenuMessage &
|
||||||
ToggleInlineMenuHiddenMessage;
|
ToggleInlineMenuHiddenMessage &
|
||||||
|
UpdateInlineMenuVisibilityMessage;
|
||||||
|
|
||||||
export type OverlayPortMessage = {
|
export type OverlayPortMessage = {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -188,16 +200,12 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
|||||||
updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void;
|
updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void;
|
||||||
checkIsFieldCurrentlyFilling: () => boolean;
|
checkIsFieldCurrentlyFilling: () => boolean;
|
||||||
getAutofillInlineMenuVisibility: () => void;
|
getAutofillInlineMenuVisibility: () => void;
|
||||||
|
openAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||||
getInlineMenuCardsVisibility: () => void;
|
getInlineMenuCardsVisibility: () => void;
|
||||||
getInlineMenuIdentitiesVisibility: () => void;
|
getInlineMenuIdentitiesVisibility: () => void;
|
||||||
openAutofillInlineMenu: () => void;
|
|
||||||
closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||||
checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void;
|
checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void;
|
||||||
focusAutofillInlineMenuList: () => void;
|
focusAutofillInlineMenuList: () => void;
|
||||||
updateAutofillInlineMenuPosition: ({
|
|
||||||
message,
|
|
||||||
sender,
|
|
||||||
}: BackgroundOnMessageHandlerParams) => Promise<void>;
|
|
||||||
getAutofillInlineMenuPosition: () => InlineMenuPosition;
|
getAutofillInlineMenuPosition: () => InlineMenuPosition;
|
||||||
updateAutofillInlineMenuElementIsVisibleStatus: ({
|
updateAutofillInlineMenuElementIsVisibleStatus: ({
|
||||||
message,
|
message,
|
||||||
@ -219,6 +227,7 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
|||||||
addEditCipherSubmitted: () => void;
|
addEditCipherSubmitted: () => void;
|
||||||
editedCipher: () => void;
|
editedCipher: () => void;
|
||||||
deletedCipher: () => void;
|
deletedCipher: () => void;
|
||||||
|
bgSaveCipher: () => void;
|
||||||
fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,14 +250,16 @@ export type InlineMenuButtonPortMessageHandlers = {
|
|||||||
|
|
||||||
export type InlineMenuListPortMessageHandlers = {
|
export type InlineMenuListPortMessageHandlers = {
|
||||||
[key: string]: CallableFunction;
|
[key: string]: CallableFunction;
|
||||||
checkAutofillInlineMenuButtonFocused: () => void;
|
checkAutofillInlineMenuButtonFocused: ({ port }: PortConnectionParam) => void;
|
||||||
autofillInlineMenuBlurred: () => void;
|
autofillInlineMenuBlurred: ({ port }: PortConnectionParam) => void;
|
||||||
unlockVault: ({ port }: PortConnectionParam) => void;
|
unlockVault: ({ port }: PortConnectionParam) => void;
|
||||||
fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
addNewVaultItem: ({ message, port }: PortOnMessageHandlerParams) => void;
|
addNewVaultItem: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
|
updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
|
refreshGeneratedPassword: () => Promise<void>;
|
||||||
|
fillGeneratedPassword: ({ port }: PortConnectionParam) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface OverlayBackground {
|
export interface OverlayBackground {
|
||||||
|
@ -1114,8 +1114,9 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips saving the domain as a never value if the tab url does not match the queue message domain", async () => {
|
it("skips saving the domain as a never value if the tab url does not match the queue message domain", async () => {
|
||||||
const tab = createChromeTabMock({ id: 2, url: "https://example.com" });
|
const tab = createChromeTabMock({ id: 2, url: "https://example.com" });
|
||||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||||
|
const secondaryTab = createChromeTabMock({ id: 3, url: "https://another.com" });
|
||||||
|
const sender = mock<chrome.runtime.MessageSender>({ tab: secondaryTab });
|
||||||
notificationBackground["notificationQueue"] = [
|
notificationBackground["notificationQueue"] = [
|
||||||
mock<AddLoginQueueMessage>({
|
mock<AddLoginQueueMessage>({
|
||||||
type: NotificationQueueMessageType.AddLogin,
|
type: NotificationQueueMessageType.AddLogin,
|
||||||
|
@ -173,13 +173,8 @@ export default class NotificationBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async doNotificationQueueCheck(tab: chrome.tabs.Tab): Promise<void> {
|
private async doNotificationQueueCheck(tab: chrome.tabs.Tab): Promise<void> {
|
||||||
const tabDomain = Utils.getDomain(tab?.url);
|
|
||||||
if (!tabDomain) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueMessage = this.notificationQueue.find(
|
const queueMessage = this.notificationQueue.find(
|
||||||
(message) => message.tab.id === tab.id && message.domain === tabDomain,
|
(message) => message.tab.id === tab.id && this.queueMessageIsFromTabOrigin(message, tab),
|
||||||
);
|
);
|
||||||
if (queueMessage) {
|
if (queueMessage) {
|
||||||
await this.sendNotificationQueueMessage(tab, queueMessage);
|
await this.sendNotificationQueueMessage(tab, queueMessage);
|
||||||
@ -537,8 +532,7 @@ export default class NotificationBackground {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabDomain = Utils.getDomain(tab.url);
|
if (!this.queueMessageIsFromTabOrigin(queueMessage, tab)) {
|
||||||
if (tabDomain != null && tabDomain !== queueMessage.domain) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,8 +679,7 @@ export default class NotificationBackground {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabDomain = Utils.getDomain(tab.url);
|
if (!this.queueMessageIsFromTabOrigin(queueMessage, tab)) {
|
||||||
if (tabDomain != null && tabDomain !== queueMessage.domain) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -829,4 +822,18 @@ export default class NotificationBackground {
|
|||||||
.catch((error) => this.logService.error(error));
|
.catch((error) => this.logService.error(error));
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether the queue message is associated with the passed tab.
|
||||||
|
*
|
||||||
|
* @param queueMessage - The queue message to check
|
||||||
|
* @param tab - The tab to check the queue message against
|
||||||
|
*/
|
||||||
|
private queueMessageIsFromTabOrigin(
|
||||||
|
queueMessage: NotificationQueueMessageItem,
|
||||||
|
tab: chrome.tabs.Tab,
|
||||||
|
) {
|
||||||
|
const tabDomain = Utils.getDomain(tab.url);
|
||||||
|
return tabDomain === queueMessage.domain || tabDomain === Utils.getDomain(queueMessage.tab.url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,27 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
jest.clearAllTimers();
|
jest.clearAllTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("feature flag behavior", () => {
|
||||||
|
let runtimeRemoveListenerSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
runtimeRemoveListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the extension listeners if the current flag value is set to `false`", () => {
|
||||||
|
getFeatureFlagMock$.next(false);
|
||||||
|
|
||||||
|
expect(runtimeRemoveListenerSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores the feature flag change if the previous flag value is equal to the current flag value", () => {
|
||||||
|
getFeatureFlagMock$.next(false);
|
||||||
|
getFeatureFlagMock$.next(false);
|
||||||
|
|
||||||
|
expect(runtimeRemoveListenerSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("setting up the form submission listeners", () => {
|
describe("setting up the form submission listeners", () => {
|
||||||
let fields: MockProxy<AutofillField>[];
|
let fields: MockProxy<AutofillField>[];
|
||||||
let details: MockProxy<AutofillPageDetails>;
|
let details: MockProxy<AutofillPageDetails>;
|
||||||
@ -180,6 +201,40 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores the store request if the sender is not within the website origins set", () => {
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{
|
||||||
|
command: "formFieldSubmitted",
|
||||||
|
uri: "example.com",
|
||||||
|
username: "username",
|
||||||
|
password: "password",
|
||||||
|
newPassword: "newPassword",
|
||||||
|
},
|
||||||
|
mock<chrome.runtime.MessageSender>({ tab: { id: 2 } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores the store request if the form submission does not include a username, password, or newPassword", () => {
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{
|
||||||
|
command: "formFieldSubmitted",
|
||||||
|
uri: "example.com",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
newPassword: "",
|
||||||
|
},
|
||||||
|
sender,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("stores the modified login cipher form data", async () => {
|
it("stores the modified login cipher form data", async () => {
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{
|
{
|
||||||
@ -203,6 +258,41 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("overrides previously stored modified login cipher form data with a subsequent store request", async () => {
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{
|
||||||
|
command: "formFieldSubmitted",
|
||||||
|
uri: "example.com",
|
||||||
|
username: "oldUsername",
|
||||||
|
password: "oldPassword",
|
||||||
|
newPassword: "oldNewPassword",
|
||||||
|
},
|
||||||
|
sender,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{
|
||||||
|
command: "formFieldSubmitted",
|
||||||
|
uri: "example.com",
|
||||||
|
username: "username",
|
||||||
|
password: "",
|
||||||
|
newPassword: "",
|
||||||
|
},
|
||||||
|
sender,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id),
|
||||||
|
).toEqual({
|
||||||
|
uri: "example.com",
|
||||||
|
username: "username",
|
||||||
|
password: "oldPassword",
|
||||||
|
newPassword: "oldNewPassword",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("clears the modified login cipher form data after 5 seconds", () => {
|
it("clears the modified login cipher form data after 5 seconds", () => {
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{
|
{
|
||||||
@ -323,10 +413,9 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
|
|
||||||
it("ignores requests that are not part of an active form submission", async () => {
|
it("ignores requests that are not part of an active form submission", async () => {
|
||||||
triggerWebRequestOnCompletedEvent(
|
triggerWebRequestOnCompletedEvent(
|
||||||
mock<chrome.webRequest.WebRequestDetails>({
|
mock<chrome.webRequest.WebResponseDetails>({
|
||||||
url: sender.url,
|
url: sender.url,
|
||||||
tabId: sender.tab.id,
|
tabId: sender.tab.id,
|
||||||
method: "POST",
|
|
||||||
requestId: "123345",
|
requestId: "123345",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -348,6 +437,25 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
triggerWebRequestOnCompletedEvent(
|
triggerWebRequestOnCompletedEvent(
|
||||||
|
mock<chrome.webRequest.WebResponseDetails>({
|
||||||
|
url: sender.url,
|
||||||
|
tabId: sender.tab.id,
|
||||||
|
requestId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(notificationChangedPasswordSpy).not.toHaveBeenCalled();
|
||||||
|
expect(notificationAddLoginSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the notification fallback timeout if the request is completed with an invalid status code", async () => {
|
||||||
|
const clearFallbackSpy = jest.spyOn(
|
||||||
|
overlayNotificationsBackground as any,
|
||||||
|
"clearNotificationFallbackTimeout",
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestId = "123345";
|
||||||
|
triggerWebRequestOnBeforeRequestEvent(
|
||||||
mock<chrome.webRequest.WebRequestDetails>({
|
mock<chrome.webRequest.WebRequestDetails>({
|
||||||
url: sender.url,
|
url: sender.url,
|
||||||
tabId: sender.tab.id,
|
tabId: sender.tab.id,
|
||||||
@ -355,9 +463,19 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
requestId,
|
requestId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(notificationChangedPasswordSpy).not.toHaveBeenCalled();
|
triggerWebRequestOnCompletedEvent(
|
||||||
expect(notificationAddLoginSpy).not.toHaveBeenCalled();
|
mock<chrome.webRequest.WebResponseDetails>({
|
||||||
|
url: sender.url,
|
||||||
|
tabId: sender.tab.id,
|
||||||
|
statusCode: 404,
|
||||||
|
requestId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(clearFallbackSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -402,10 +520,9 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
triggerWebRequestOnCompletedEvent(
|
triggerWebRequestOnCompletedEvent(
|
||||||
mock<chrome.webRequest.WebRequestDetails>({
|
mock<chrome.webRequest.WebResponseDetails>({
|
||||||
url: sender.url,
|
url: sender.url,
|
||||||
tabId: sender.tab.id,
|
tabId: sender.tab.id,
|
||||||
method: "POST",
|
|
||||||
requestId,
|
requestId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -452,10 +569,9 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
triggerWebRequestOnCompletedEvent(
|
triggerWebRequestOnCompletedEvent(
|
||||||
mock<chrome.webRequest.WebRequestDetails>({
|
mock<chrome.webRequest.WebResponseDetails>({
|
||||||
url: sender.url,
|
url: sender.url,
|
||||||
tabId: sender.tab.id,
|
tabId: sender.tab.id,
|
||||||
method: "POST",
|
|
||||||
requestId,
|
requestId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -560,14 +676,59 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0);
|
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears all associated data with a tab that is entering a `loading` state", () => {
|
describe("tab onUpdated", () => {
|
||||||
triggerTabOnUpdatedEvent(
|
it("skips clearing the website origins if the changeInfo does not contain a `loading` status", () => {
|
||||||
sender.tab.id,
|
triggerTabOnUpdatedEvent(
|
||||||
mock<chrome.tabs.TabChangeInfo>({ status: "loading" }),
|
sender.tab.id,
|
||||||
mock<chrome.tabs.Tab>({ status: "loading" }),
|
mock<chrome.tabs.TabChangeInfo>({ status: "complete" }),
|
||||||
);
|
mock<chrome.tabs.Tab>({ status: "complete" }),
|
||||||
|
);
|
||||||
|
|
||||||
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0);
|
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips clearing the website origins if the changeInfo does not contain a url", () => {
|
||||||
|
triggerTabOnUpdatedEvent(
|
||||||
|
sender.tab.id,
|
||||||
|
mock<chrome.tabs.TabChangeInfo>({ status: "loading", url: "" }),
|
||||||
|
mock<chrome.tabs.Tab>({ status: "loading" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips clearing the website origins if the tab does not contain known website origins", () => {
|
||||||
|
triggerTabOnUpdatedEvent(
|
||||||
|
199,
|
||||||
|
mock<chrome.tabs.TabChangeInfo>({ status: "loading", url: "https://example.com" }),
|
||||||
|
mock<chrome.tabs.Tab>({ status: "loading", id: 199 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips clearing the website origins if the changeInfo's url is present as part of the know website origin match patterns", () => {
|
||||||
|
triggerTabOnUpdatedEvent(
|
||||||
|
sender.tab.id,
|
||||||
|
mock<chrome.tabs.TabChangeInfo>({
|
||||||
|
status: "loading",
|
||||||
|
url: "https://subdomain.example.com",
|
||||||
|
}),
|
||||||
|
mock<chrome.tabs.Tab>({ status: "loading" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears all associated data with a tab that is entering a `loading` state", () => {
|
||||||
|
triggerTabOnUpdatedEvent(
|
||||||
|
sender.tab.id,
|
||||||
|
mock<chrome.tabs.TabChangeInfo>({ status: "loading" }),
|
||||||
|
mock<chrome.tabs.Tab>({ status: "loading" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -333,7 +333,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
|
|
||||||
const response = (await BrowserApi.tabSendMessage(
|
const response = (await BrowserApi.tabSendMessage(
|
||||||
tab,
|
tab,
|
||||||
{ command: "getFormFieldDataForNotification" },
|
{ command: "getInlineMenuFormFieldData" },
|
||||||
{ frameId },
|
{ frameId },
|
||||||
)) as OverlayNotificationsExtensionMessage;
|
)) as OverlayNotificationsExtensionMessage;
|
||||||
if (response) {
|
if (response) {
|
||||||
@ -471,7 +471,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
private shouldTriggerChangePasswordNotification = (
|
private shouldTriggerChangePasswordNotification = (
|
||||||
modifyLoginData: ModifyLoginCipherFormData,
|
modifyLoginData: ModifyLoginCipherFormData,
|
||||||
) => {
|
) => {
|
||||||
return modifyLoginData.newPassword && !modifyLoginData.username;
|
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -480,7 +480,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
* @param modifyLoginData - The modified login form data
|
* @param modifyLoginData - The modified login form data
|
||||||
*/
|
*/
|
||||||
private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
|
private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
|
||||||
return modifyLoginData.username && (modifyLoginData.password || modifyLoginData.newPassword);
|
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -576,8 +576,20 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
* @param changeInfo - The change info of the tab
|
* @param changeInfo - The change info of the tab
|
||||||
*/
|
*/
|
||||||
private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => {
|
private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => {
|
||||||
if (changeInfo.status === "loading" && this.websiteOriginsWithFields.has(tabId)) {
|
if (changeInfo.status !== "loading" || !changeInfo.url) {
|
||||||
this.websiteOriginsWithFields.delete(tabId);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originPatterns = this.websiteOriginsWithFields.get(tabId);
|
||||||
|
if (!originPatterns) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchPatters = generateDomainMatchPatterns(changeInfo.url);
|
||||||
|
if (matchPatters.some((pattern) => originPatterns.has(pattern))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.websiteOriginsWithFields.delete(tabId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -92,7 +92,7 @@ export default class TabsBackground {
|
|||||||
FeatureFlag.InlineMenuPositioningImprovements,
|
FeatureFlag.InlineMenuPositioningImprovements,
|
||||||
);
|
);
|
||||||
const removePageDetailsStatus = new Set(["loading", "unloaded"]);
|
const removePageDetailsStatus = new Set(["loading", "unloaded"]);
|
||||||
if (!!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) {
|
if (!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) {
|
||||||
this.overlayBackground.removePageDetails(tabId);
|
this.overlayBackground.removePageDetails(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
|
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
|
||||||
@ -21,10 +20,10 @@ export type AutofillExtensionMessage = {
|
|||||||
authStatus?: AuthenticationStatus;
|
authStatus?: AuthenticationStatus;
|
||||||
isOpeningFullInlineMenu?: boolean;
|
isOpeningFullInlineMenu?: boolean;
|
||||||
addNewCipherType?: CipherType;
|
addNewCipherType?: CipherType;
|
||||||
|
ignoreFieldFocus?: boolean;
|
||||||
data?: {
|
data?: {
|
||||||
direction?: "previous" | "next" | "current";
|
direction?: "previous" | "next" | "current";
|
||||||
forceCloseInlineMenu?: boolean;
|
forceCloseInlineMenu?: boolean;
|
||||||
newSettingValue?: InlineMenuVisibilitySetting;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import AutofillPageDetails from "../models/autofill-page-details";
|
|||||||
import AutofillScript from "../models/autofill-script";
|
import AutofillScript from "../models/autofill-script";
|
||||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
||||||
import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service";
|
import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service";
|
||||||
|
import { DomElementVisibilityService } from "../services/abstractions/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "../services/abstractions/dom-query.service";
|
import { DomQueryService } from "../services/abstractions/dom-query.service";
|
||||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||||
import {
|
import {
|
||||||
@ -17,6 +18,7 @@ import AutofillInit from "./autofill-init";
|
|||||||
|
|
||||||
describe("AutofillInit", () => {
|
describe("AutofillInit", () => {
|
||||||
let domQueryService: MockProxy<DomQueryService>;
|
let domQueryService: MockProxy<DomQueryService>;
|
||||||
|
let domElementVisibilityService: MockProxy<DomElementVisibilityService>;
|
||||||
let overlayNotificationsContentService: MockProxy<OverlayNotificationsContentService>;
|
let overlayNotificationsContentService: MockProxy<OverlayNotificationsContentService>;
|
||||||
let inlineMenuElements: MockProxy<AutofillInlineMenuContentService>;
|
let inlineMenuElements: MockProxy<AutofillInlineMenuContentService>;
|
||||||
let autofillOverlayContentService: MockProxy<AutofillOverlayContentService>;
|
let autofillOverlayContentService: MockProxy<AutofillOverlayContentService>;
|
||||||
@ -32,11 +34,13 @@ describe("AutofillInit", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
domQueryService = mock<DomQueryService>();
|
domQueryService = mock<DomQueryService>();
|
||||||
|
domElementVisibilityService = mock<DomElementVisibilityService>();
|
||||||
overlayNotificationsContentService = mock<OverlayNotificationsContentService>();
|
overlayNotificationsContentService = mock<OverlayNotificationsContentService>();
|
||||||
inlineMenuElements = mock<AutofillInlineMenuContentService>();
|
inlineMenuElements = mock<AutofillInlineMenuContentService>();
|
||||||
autofillOverlayContentService = mock<AutofillOverlayContentService>();
|
autofillOverlayContentService = mock<AutofillOverlayContentService>();
|
||||||
autofillInit = new AutofillInit(
|
autofillInit = new AutofillInit(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
autofillOverlayContentService,
|
autofillOverlayContentService,
|
||||||
inlineMenuElements,
|
inlineMenuElements,
|
||||||
overlayNotificationsContentService,
|
overlayNotificationsContentService,
|
||||||
|
@ -4,9 +4,9 @@ import AutofillPageDetails from "../models/autofill-page-details";
|
|||||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
|
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
|
||||||
import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service";
|
import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service";
|
||||||
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
|
||||||
|
import { DomElementVisibilityService } from "../services/abstractions/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "../services/abstractions/dom-query.service";
|
import { DomQueryService } from "../services/abstractions/dom-query.service";
|
||||||
import { CollectAutofillContentService } from "../services/collect-autofill-content.service";
|
import { CollectAutofillContentService } from "../services/collect-autofill-content.service";
|
||||||
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
|
||||||
import InsertAutofillContentService from "../services/insert-autofill-content.service";
|
import InsertAutofillContentService from "../services/insert-autofill-content.service";
|
||||||
import { sendExtensionMessage } from "../utils";
|
import { sendExtensionMessage } from "../utils";
|
||||||
|
|
||||||
@ -18,7 +18,6 @@ import {
|
|||||||
|
|
||||||
class AutofillInit implements AutofillInitInterface {
|
class AutofillInit implements AutofillInitInterface {
|
||||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||||
private readonly domElementVisibilityService: DomElementVisibilityService;
|
|
||||||
private readonly collectAutofillContentService: CollectAutofillContentService;
|
private readonly collectAutofillContentService: CollectAutofillContentService;
|
||||||
private readonly insertAutofillContentService: InsertAutofillContentService;
|
private readonly insertAutofillContentService: InsertAutofillContentService;
|
||||||
private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined;
|
private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined;
|
||||||
@ -33,26 +32,25 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
* CollectAutofillContentService and InsertAutofillContentService classes.
|
* CollectAutofillContentService and InsertAutofillContentService classes.
|
||||||
*
|
*
|
||||||
* @param domQueryService - Service used to handle DOM queries.
|
* @param domQueryService - Service used to handle DOM queries.
|
||||||
|
* @param domElementVisibilityService - Used to check if an element is viewable.
|
||||||
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
|
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
|
||||||
* @param autofillInlineMenuContentService - The inline menu content service, potentially undefined.
|
* @param autofillInlineMenuContentService - The inline menu content service, potentially undefined.
|
||||||
* @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined.
|
* @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
domQueryService: DomQueryService,
|
domQueryService: DomQueryService,
|
||||||
|
domElementVisibilityService: DomElementVisibilityService,
|
||||||
private autofillOverlayContentService?: AutofillOverlayContentService,
|
private autofillOverlayContentService?: AutofillOverlayContentService,
|
||||||
private autofillInlineMenuContentService?: AutofillInlineMenuContentService,
|
private autofillInlineMenuContentService?: AutofillInlineMenuContentService,
|
||||||
private overlayNotificationsContentService?: OverlayNotificationsContentService,
|
private overlayNotificationsContentService?: OverlayNotificationsContentService,
|
||||||
) {
|
) {
|
||||||
this.domElementVisibilityService = new DomElementVisibilityService(
|
|
||||||
this.autofillInlineMenuContentService,
|
|
||||||
);
|
|
||||||
this.collectAutofillContentService = new CollectAutofillContentService(
|
this.collectAutofillContentService = new CollectAutofillContentService(
|
||||||
this.domElementVisibilityService,
|
domElementVisibilityService,
|
||||||
domQueryService,
|
domQueryService,
|
||||||
this.autofillOverlayContentService,
|
this.autofillOverlayContentService,
|
||||||
);
|
);
|
||||||
this.insertAutofillContentService = new InsertAutofillContentService(
|
this.insertAutofillContentService = new InsertAutofillContentService(
|
||||||
this.domElementVisibilityService,
|
domElementVisibilityService,
|
||||||
this.collectAutofillContentService,
|
this.collectAutofillContentService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
||||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||||
|
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "../services/dom-query.service";
|
import { DomQueryService } from "../services/dom-query.service";
|
||||||
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
||||||
import { setupAutofillInitDisconnectAction } from "../utils";
|
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||||
@ -8,20 +9,25 @@ import AutofillInit from "./autofill-init";
|
|||||||
|
|
||||||
(function (windowContext) {
|
(function (windowContext) {
|
||||||
if (!windowContext.bitwardenAutofillInit) {
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
|
let inlineMenuContentService: AutofillInlineMenuContentService;
|
||||||
|
if (globalThis.self === globalThis.top) {
|
||||||
|
inlineMenuContentService = new AutofillInlineMenuContentService();
|
||||||
|
}
|
||||||
|
|
||||||
const domQueryService = new DomQueryService();
|
const domQueryService = new DomQueryService();
|
||||||
|
const domElementVisibilityService = new DomElementVisibilityService(inlineMenuContentService);
|
||||||
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
const autofillOverlayContentService = new AutofillOverlayContentService(
|
const autofillOverlayContentService = new AutofillOverlayContentService(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
inlineMenuFieldQualificationService,
|
inlineMenuFieldQualificationService,
|
||||||
);
|
);
|
||||||
let inlineMenuElements: AutofillInlineMenuContentService;
|
|
||||||
if (globalThis.self === globalThis.top) {
|
|
||||||
inlineMenuElements = new AutofillInlineMenuContentService();
|
|
||||||
}
|
|
||||||
windowContext.bitwardenAutofillInit = new AutofillInit(
|
windowContext.bitwardenAutofillInit = new AutofillInit(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
autofillOverlayContentService,
|
autofillOverlayContentService,
|
||||||
inlineMenuElements,
|
inlineMenuContentService,
|
||||||
);
|
);
|
||||||
setupAutofillInitDisconnectAction(windowContext);
|
setupAutofillInitDisconnectAction(windowContext);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
|
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
|
||||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||||
|
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "../services/dom-query.service";
|
import { DomQueryService } from "../services/dom-query.service";
|
||||||
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
||||||
import { setupAutofillInitDisconnectAction } from "../utils";
|
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||||
@ -9,9 +10,11 @@ import AutofillInit from "./autofill-init";
|
|||||||
(function (windowContext) {
|
(function (windowContext) {
|
||||||
if (!windowContext.bitwardenAutofillInit) {
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
const domQueryService = new DomQueryService();
|
const domQueryService = new DomQueryService();
|
||||||
|
const domElementVisibilityService = new DomElementVisibilityService();
|
||||||
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
const autofillOverlayContentService = new AutofillOverlayContentService(
|
const autofillOverlayContentService = new AutofillOverlayContentService(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
inlineMenuFieldQualificationService,
|
inlineMenuFieldQualificationService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -22,6 +25,7 @@ import AutofillInit from "./autofill-init";
|
|||||||
|
|
||||||
windowContext.bitwardenAutofillInit = new AutofillInit(
|
windowContext.bitwardenAutofillInit = new AutofillInit(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
autofillOverlayContentService,
|
autofillOverlayContentService,
|
||||||
null,
|
null,
|
||||||
overlayNotificationsContentService,
|
overlayNotificationsContentService,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
||||||
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
|
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
|
||||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||||
|
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "../services/dom-query.service";
|
import { DomQueryService } from "../services/dom-query.service";
|
||||||
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
||||||
import { setupAutofillInitDisconnectAction } from "../utils";
|
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||||
@ -9,24 +10,27 @@ import AutofillInit from "./autofill-init";
|
|||||||
|
|
||||||
(function (windowContext) {
|
(function (windowContext) {
|
||||||
if (!windowContext.bitwardenAutofillInit) {
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
const domQueryService = new DomQueryService();
|
let inlineMenuContentService: AutofillInlineMenuContentService;
|
||||||
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
|
||||||
const autofillOverlayContentService = new AutofillOverlayContentService(
|
|
||||||
domQueryService,
|
|
||||||
inlineMenuFieldQualificationService,
|
|
||||||
);
|
|
||||||
|
|
||||||
let inlineMenuElements: AutofillInlineMenuContentService;
|
|
||||||
let overlayNotificationsContentService: OverlayNotificationsContentService;
|
let overlayNotificationsContentService: OverlayNotificationsContentService;
|
||||||
if (globalThis.self === globalThis.top) {
|
if (globalThis.self === globalThis.top) {
|
||||||
inlineMenuElements = new AutofillInlineMenuContentService();
|
inlineMenuContentService = new AutofillInlineMenuContentService();
|
||||||
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const domQueryService = new DomQueryService();
|
||||||
|
const domElementVisibilityService = new DomElementVisibilityService(inlineMenuContentService);
|
||||||
|
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
|
const autofillOverlayContentService = new AutofillOverlayContentService(
|
||||||
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
|
inlineMenuFieldQualificationService,
|
||||||
|
);
|
||||||
|
|
||||||
windowContext.bitwardenAutofillInit = new AutofillInit(
|
windowContext.bitwardenAutofillInit = new AutofillInit(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
autofillOverlayContentService,
|
autofillOverlayContentService,
|
||||||
inlineMenuElements,
|
inlineMenuContentService,
|
||||||
overlayNotificationsContentService,
|
overlayNotificationsContentService,
|
||||||
);
|
);
|
||||||
setupAutofillInitDisconnectAction(windowContext);
|
setupAutofillInitDisconnectAction(windowContext);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "../services/dom-query.service";
|
import { DomQueryService } from "../services/dom-query.service";
|
||||||
import { setupAutofillInitDisconnectAction } from "../utils";
|
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||||
|
|
||||||
@ -6,7 +7,11 @@ import AutofillInit from "./autofill-init";
|
|||||||
(function (windowContext) {
|
(function (windowContext) {
|
||||||
if (!windowContext.bitwardenAutofillInit) {
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
const domQueryService = new DomQueryService();
|
const domQueryService = new DomQueryService();
|
||||||
windowContext.bitwardenAutofillInit = new AutofillInit(domQueryService);
|
const domElementVisibilityService = new DomElementVisibilityService();
|
||||||
|
windowContext.bitwardenAutofillInit = new AutofillInit(
|
||||||
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
|
);
|
||||||
setupAutofillInitDisconnectAction(windowContext);
|
setupAutofillInitDisconnectAction(windowContext);
|
||||||
|
|
||||||
windowContext.bitwardenAutofillInit.init();
|
windowContext.bitwardenAutofillInit.init();
|
||||||
|
@ -73,6 +73,10 @@ class LegacyAutofillOverlayContentService implements LegacyAutofillOverlayConten
|
|||||||
* Satisfy the AutofillOverlayContentService interface.
|
* Satisfy the AutofillOverlayContentService interface.
|
||||||
*/
|
*/
|
||||||
messageHandlers = {} as AutofillOverlayContentExtensionMessageHandlers;
|
messageHandlers = {} as AutofillOverlayContentExtensionMessageHandlers;
|
||||||
|
clearUserFilledFields() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
async setupOverlayListeners(
|
async setupOverlayListeners(
|
||||||
autofillFieldElement: ElementWithOpId<FormFieldElement>,
|
autofillFieldElement: ElementWithOpId<FormFieldElement>,
|
||||||
autofillFieldData: AutofillField,
|
autofillFieldData: AutofillField,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
export const AutofillOverlayElement = {
|
export const AutofillOverlayElement = {
|
||||||
Button: "autofill-inline-menu-button",
|
Button: "autofill-inline-menu-button",
|
||||||
List: "autofill-inline-menu-list",
|
List: "autofill-inline-menu-list",
|
||||||
@ -19,4 +21,20 @@ export const RedirectFocusDirection = {
|
|||||||
Next: "next",
|
Next: "next",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export enum InlineMenuFillType {
|
||||||
|
AccountCreationUsername = 5,
|
||||||
|
PasswordGeneration = 6,
|
||||||
|
CurrentPasswordUpdate = 7,
|
||||||
|
}
|
||||||
|
export type InlineMenuFillTypes = InlineMenuFillType | CipherType;
|
||||||
|
|
||||||
|
export const InlineMenuAccountCreationFieldType = {
|
||||||
|
Text: "text",
|
||||||
|
Email: "email",
|
||||||
|
Password: "password",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type InlineMenuAccountCreationFieldTypes =
|
||||||
|
(typeof InlineMenuAccountCreationFieldType)[keyof typeof InlineMenuAccountCreationFieldType];
|
||||||
|
|
||||||
export const MAX_SUB_FRAME_DEPTH = 8;
|
export const MAX_SUB_FRAME_DEPTH = 8;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
|
||||||
|
|
||||||
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
||||||
|
import {
|
||||||
|
InlineMenuAccountCreationFieldTypes,
|
||||||
|
InlineMenuFillTypes,
|
||||||
|
} from "../enums/autofill-overlay.enum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single field that is collected from the page source and is potentially autofilled.
|
* Represents a single field that is collected from the page source and is potentially autofilled.
|
||||||
@ -107,15 +109,17 @@ export default class AutofillField {
|
|||||||
*/
|
*/
|
||||||
maxLength?: number | null;
|
maxLength?: number | null;
|
||||||
|
|
||||||
|
dataSetValues?: string;
|
||||||
|
|
||||||
rel?: string | null;
|
rel?: string | null;
|
||||||
|
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
|
||||||
filledByCipherType?: CipherType;
|
inlineMenuFillType?: InlineMenuFillTypes;
|
||||||
|
|
||||||
showInlineMenuAccountCreation?: boolean;
|
|
||||||
|
|
||||||
showPasskeys?: boolean;
|
showPasskeys?: boolean;
|
||||||
|
|
||||||
fieldQualifier?: AutofillFieldQualifierType;
|
fieldQualifier?: AutofillFieldQualifierType;
|
||||||
|
|
||||||
|
accountCreationFieldType?: InlineMenuAccountCreationFieldTypes;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ export type AutofillInlineMenuIframeExtensionMessage = {
|
|||||||
styles?: Partial<CSSStyleDeclaration>;
|
styles?: Partial<CSSStyleDeclaration>;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
portKey?: string;
|
portKey?: string;
|
||||||
|
generatedPassword?: string;
|
||||||
|
refreshPassword?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutofillInlineMenuIframeExtensionMessageParam = {
|
export type AutofillInlineMenuIframeExtensionMessageParam = {
|
||||||
@ -23,6 +25,9 @@ export type BackgroundPortMessageHandlers = {
|
|||||||
}: AutofillInlineMenuIframeExtensionMessageParam) => void;
|
}: AutofillInlineMenuIframeExtensionMessageParam) => void;
|
||||||
updateAutofillInlineMenuColorScheme: () => void;
|
updateAutofillInlineMenuColorScheme: () => void;
|
||||||
fadeInAutofillInlineMenuIframe: () => void;
|
fadeInAutofillInlineMenuIframe: () => void;
|
||||||
|
updateAutofillInlineMenuGeneratedPassword: ({
|
||||||
|
message,
|
||||||
|
}: AutofillInlineMenuIframeExtensionMessageParam) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AutofillInlineMenuIframeService {
|
export interface AutofillInlineMenuIframeService {
|
||||||
|
@ -1,25 +1,34 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
|
||||||
|
|
||||||
import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background";
|
import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background";
|
||||||
|
import { InlineMenuFillTypes } from "../../../enums/autofill-overlay.enum";
|
||||||
|
|
||||||
type AutofillInlineMenuListMessage = { command: string };
|
type AutofillInlineMenuListMessage = { command: string };
|
||||||
|
|
||||||
export type UpdateAutofillInlineMenuListCiphersMessage = AutofillInlineMenuListMessage & {
|
export type UpdateAutofillInlineMenuListCiphersParams = {
|
||||||
ciphers: InlineMenuCipherData[];
|
ciphers: InlineMenuCipherData[];
|
||||||
showInlineMenuAccountCreation?: boolean;
|
showInlineMenuAccountCreation?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateAutofillInlineMenuListCiphersMessage = AutofillInlineMenuListMessage &
|
||||||
|
UpdateAutofillInlineMenuListCiphersParams;
|
||||||
|
|
||||||
|
export type UpdateAutofillInlineMenuGeneratedPasswordMessage = AutofillInlineMenuListMessage & {
|
||||||
|
generatedPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage & {
|
export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage & {
|
||||||
authStatus: AuthenticationStatus;
|
authStatus: AuthenticationStatus;
|
||||||
styleSheetUrl: string;
|
styleSheetUrl: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
translations: Record<string, string>;
|
translations: Record<string, string>;
|
||||||
ciphers?: InlineMenuCipherData[];
|
ciphers?: InlineMenuCipherData[];
|
||||||
filledByCipherType?: CipherType;
|
inlineMenuFillType?: InlineMenuFillTypes;
|
||||||
showInlineMenuAccountCreation?: boolean;
|
showInlineMenuAccountCreation?: boolean;
|
||||||
showPasskeysLabels?: boolean;
|
showPasskeysLabels?: boolean;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
|
generatedPassword?: string;
|
||||||
|
showSaveLoginMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutofillInlineMenuListWindowMessageHandlers = {
|
export type AutofillInlineMenuListWindowMessageHandlers = {
|
||||||
@ -31,5 +40,10 @@ export type AutofillInlineMenuListWindowMessageHandlers = {
|
|||||||
}: {
|
}: {
|
||||||
message: UpdateAutofillInlineMenuListCiphersMessage;
|
message: UpdateAutofillInlineMenuListCiphersMessage;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
updateAutofillInlineMenuGeneratedPassword: ({
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
message: UpdateAutofillInlineMenuGeneratedPasswordMessage;
|
||||||
|
}) => void;
|
||||||
focusAutofillInlineMenuList: () => void;
|
focusAutofillInlineMenuList: () => void;
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
import AutofillInit from "../../../content/autofill-init";
|
import AutofillInit from "../../../content/autofill-init";
|
||||||
import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum";
|
import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum";
|
||||||
import { DomQueryService } from "../../../services/abstractions/dom-query.service";
|
import { DomQueryService } from "../../../services/abstractions/dom-query.service";
|
||||||
|
import DomElementVisibilityService from "../../../services/dom-element-visibility.service";
|
||||||
import { createMutationRecordMock } from "../../../spec/autofill-mocks";
|
import { createMutationRecordMock } from "../../../spec/autofill-mocks";
|
||||||
import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils";
|
import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils";
|
||||||
import { ElementWithOpId } from "../../../types";
|
import { ElementWithOpId } from "../../../types";
|
||||||
@ -11,6 +12,7 @@ import { AutofillInlineMenuContentService } from "./autofill-inline-menu-content
|
|||||||
|
|
||||||
describe("AutofillInlineMenuContentService", () => {
|
describe("AutofillInlineMenuContentService", () => {
|
||||||
let domQueryService: MockProxy<DomQueryService>;
|
let domQueryService: MockProxy<DomQueryService>;
|
||||||
|
let domElementVisibilityService: DomElementVisibilityService;
|
||||||
let autofillInlineMenuContentService: AutofillInlineMenuContentService;
|
let autofillInlineMenuContentService: AutofillInlineMenuContentService;
|
||||||
let autofillInit: AutofillInit;
|
let autofillInit: AutofillInit;
|
||||||
let sendExtensionMessageSpy: jest.SpyInstance;
|
let sendExtensionMessageSpy: jest.SpyInstance;
|
||||||
@ -22,8 +24,14 @@ describe("AutofillInlineMenuContentService", () => {
|
|||||||
globalThis.document.body.innerHTML = "";
|
globalThis.document.body.innerHTML = "";
|
||||||
globalThis.requestIdleCallback = jest.fn((cb, options) => setTimeout(cb, 100));
|
globalThis.requestIdleCallback = jest.fn((cb, options) => setTimeout(cb, 100));
|
||||||
domQueryService = mock<DomQueryService>();
|
domQueryService = mock<DomQueryService>();
|
||||||
|
domElementVisibilityService = new DomElementVisibilityService();
|
||||||
autofillInlineMenuContentService = new AutofillInlineMenuContentService();
|
autofillInlineMenuContentService = new AutofillInlineMenuContentService();
|
||||||
autofillInit = new AutofillInit(domQueryService, null, autofillInlineMenuContentService);
|
autofillInit = new AutofillInit(
|
||||||
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
|
null,
|
||||||
|
autofillInlineMenuContentService,
|
||||||
|
);
|
||||||
autofillInit.init();
|
autofillInit.init();
|
||||||
observeContainerMutationsSpy = jest.spyOn(
|
observeContainerMutationsSpy = jest.spyOn(
|
||||||
autofillInlineMenuContentService["containerElementMutationObserver"] as any,
|
autofillInlineMenuContentService["containerElementMutationObserver"] as any,
|
||||||
@ -37,6 +45,11 @@ describe("AutofillInlineMenuContentService", () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
Object.defineProperty(document, "activeElement", {
|
||||||
|
value: null,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isElementInlineMenu", () => {
|
describe("isElementInlineMenu", () => {
|
||||||
@ -197,6 +210,31 @@ describe("AutofillInlineMenuContentService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("appends the inline menu element to a containing `dialog` element if the element is a modal", async () => {
|
||||||
|
isInlineMenuButtonVisibleSpy.mockResolvedValue(false);
|
||||||
|
const dialogElement = document.createElement("dialog");
|
||||||
|
dialogElement.setAttribute("open", "true");
|
||||||
|
jest.spyOn(dialogElement, "matches").mockReturnValue(true);
|
||||||
|
const dialogAppendSpy = jest.spyOn(dialogElement, "appendChild");
|
||||||
|
const inputElement = document.createElement("input");
|
||||||
|
dialogElement.appendChild(inputElement);
|
||||||
|
document.body.appendChild(dialogElement);
|
||||||
|
Object.defineProperty(document, "activeElement", {
|
||||||
|
value: inputElement,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendMockExtensionMessage({
|
||||||
|
command: "appendAutofillInlineMenuToDom",
|
||||||
|
overlayElement: AutofillOverlayElement.Button,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(dialogAppendSpy).toHaveBeenCalledWith(
|
||||||
|
autofillInlineMenuContentService["buttonElement"],
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the autofill inline menu from the page. This will initially
|
* Removes the autofill inline menu from the page. This will initially
|
||||||
* unobserve the body element to ensure the mutation observer no
|
* unobserve the menu container to ensure the mutation observer no
|
||||||
* longer triggers.
|
* longer triggers.
|
||||||
*/
|
*/
|
||||||
private closeInlineMenu = (message?: AutofillExtensionMessage) => {
|
private closeInlineMenu = (message?: AutofillExtensionMessage) => {
|
||||||
@ -190,15 +190,15 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the inline menu element to the body element. This method will also
|
* Appends the inline menu element to the menu container. This method will also
|
||||||
* observe the body element to ensure that the inline menu element is not
|
* observe the menu container to ensure that the inline menu element is not
|
||||||
* interfered with by any DOM changes.
|
* interfered with by any DOM changes.
|
||||||
*
|
*
|
||||||
* @param element - The inline menu element to append to the body element.
|
* @param element - The inline menu element to append to the menu container.
|
||||||
*/
|
*/
|
||||||
private appendInlineMenuElementToDom(element: HTMLElement) {
|
private appendInlineMenuElementToDom(element: HTMLElement) {
|
||||||
const parentDialogElement = globalThis.document.activeElement?.closest("dialog");
|
const parentDialogElement = globalThis.document.activeElement?.closest("dialog");
|
||||||
if (parentDialogElement && parentDialogElement.open && parentDialogElement.matches(":modal")) {
|
if (parentDialogElement?.open && parentDialogElement.matches(":modal")) {
|
||||||
this.observeContainerElement(parentDialogElement);
|
this.observeContainerElement(parentDialogElement);
|
||||||
parentDialogElement.appendChild(element);
|
parentDialogElement.appendChild(element);
|
||||||
return;
|
return;
|
||||||
@ -273,10 +273,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up mutation observers for the inline menu elements, the body element, and
|
* Sets up mutation observers for the inline menu elements, the menu container, and
|
||||||
* the document element. The mutation observers are used to remove any styles that
|
* the document element. The mutation observers are used to remove any styles that
|
||||||
* are added to the inline menu elements by the website. They are also used to ensure
|
* are added to the inline menu elements by the website. They are also used to ensure
|
||||||
* that the inline menu elements are always present at the bottom of the body element.
|
* that the inline menu elements are always present at the bottom of the menu container.
|
||||||
*/
|
*/
|
||||||
private setupMutationObserver = () => {
|
private setupMutationObserver = () => {
|
||||||
this.inlineMenuElementsMutationObserver = new MutationObserver(
|
this.inlineMenuElementsMutationObserver = new MutationObserver(
|
||||||
@ -441,10 +441,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the behavior of a persistent child element that is forcing itself to
|
* Handles the behavior of a persistent child element that is forcing itself to
|
||||||
* the bottom of the body element. This method will ensure that the inline menu
|
* the bottom of the menu container. This method will ensure that the inline menu
|
||||||
* elements are not obscured by the persistent child element.
|
* elements are not obscured by the persistent child element.
|
||||||
*
|
*
|
||||||
* @param lastChild - The last child of the body element.
|
* @param lastChild - The last child of the menu container.
|
||||||
*/
|
*/
|
||||||
private handlePersistentLastChildOverride(lastChild: Element) {
|
private handlePersistentLastChildOverride(lastChild: Element) {
|
||||||
const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
|
const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
|
||||||
@ -460,11 +460,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies if the last child of the body element is overlaying the inline menu elements.
|
* Verifies if the last child of the menu container is overlaying the inline menu elements.
|
||||||
* This is triggered when the last child of the body is being forced by some script to
|
* This is triggered when the last child of the menu container is being forced by some
|
||||||
* be an element other than the inline menu elements.
|
* script to be an element other than the inline menu elements.
|
||||||
*
|
*
|
||||||
* @param lastChild - The last child of the body element.
|
* @param lastChild - The last child of the menu container.
|
||||||
*/
|
*/
|
||||||
private verifyInlineMenuIsNotObscured = async (lastChild: Element) => {
|
private verifyInlineMenuIsNotObscured = async (lastChild: Element) => {
|
||||||
const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage(
|
const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage(
|
||||||
@ -495,7 +495,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the timeout that is used to verify that the last child of the body element
|
* Clears the timeout that is used to verify that the last child of the menu container
|
||||||
* is not overlaying the inline menu elements.
|
* is not overlaying the inline menu elements.
|
||||||
*/
|
*/
|
||||||
private clearPersistentLastChildOverrideTimeout() {
|
private clearPersistentLastChildOverrideTimeout() {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
exports[`AutofillInlineMenuIframeService initMenuIframe sets up the iframe's attributes 1`] = `
|
exports[`AutofillInlineMenuIframeService initMenuIframe sets up the iframe's attributes 1`] = `
|
||||||
<iframe
|
<iframe
|
||||||
allowtransparency="true"
|
allowtransparency="true"
|
||||||
|
scrolling="no"
|
||||||
src="chrome-extension://id/overlay/menu.html"
|
src="chrome-extension://id/overlay/menu.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;"
|
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"
|
tabindex="-1"
|
||||||
|
@ -104,9 +104,10 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
expect(globalThis.setTimeout).not.toHaveBeenCalled();
|
expect(globalThis.setTimeout).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("announces the aria alert if the aria alert element is populated", () => {
|
it("announces the aria alert if the aria alert element is populated", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.spyOn(globalThis, "setTimeout");
|
jest.spyOn(globalThis, "setTimeout");
|
||||||
|
sendExtensionMessageSpy.mockResolvedValue(true);
|
||||||
autofillInlineMenuIframeService["ariaAlertElement"] = document.createElement("div");
|
autofillInlineMenuIframeService["ariaAlertElement"] = document.createElement("div");
|
||||||
autofillInlineMenuIframeService["ariaAlertTimeout"] = setTimeout(jest.fn(), 2000);
|
autofillInlineMenuIframeService["ariaAlertTimeout"] = setTimeout(jest.fn(), 2000);
|
||||||
|
|
||||||
@ -114,6 +115,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
|
|
||||||
expect(globalThis.setTimeout).toHaveBeenCalled();
|
expect(globalThis.setTimeout).toHaveBeenCalled();
|
||||||
jest.advanceTimersByTime(2000);
|
jest.advanceTimersByTime(2000);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(shadowAppendSpy).toHaveBeenCalledWith(
|
expect(shadowAppendSpy).toHaveBeenCalledWith(
|
||||||
autofillInlineMenuIframeService["ariaAlertElement"],
|
autofillInlineMenuIframeService["ariaAlertElement"],
|
||||||
@ -363,16 +365,18 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
expect(autofillInlineMenuIframeService["iframe"].style.left).toBe(styles.left);
|
expect(autofillInlineMenuIframeService["iframe"].style.left).toBe(styles.left);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("announces the opening of the iframe using an aria alert", () => {
|
it("announces the opening of the iframe using an aria alert", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
sendExtensionMessageSpy.mockResolvedValue(true);
|
||||||
const styles = { top: "100px", left: "100px" };
|
const styles = { top: "100px", left: "100px" };
|
||||||
|
|
||||||
sendPortMessage(portSpy, {
|
sendPortMessage(portSpy, {
|
||||||
command: "updateAutofillInlineMenuPosition",
|
command: "updateAutofillInlineMenuPosition",
|
||||||
styles,
|
styles,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.advanceTimersByTime(2000);
|
jest.advanceTimersByTime(2000);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(shadowAppendSpy).toHaveBeenCalledWith(
|
expect(shadowAppendSpy).toHaveBeenCalledWith(
|
||||||
autofillInlineMenuIframeService["ariaAlertElement"],
|
autofillInlineMenuIframeService["ariaAlertElement"],
|
||||||
);
|
);
|
||||||
@ -452,6 +456,19 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
jest.advanceTimersByTime(10);
|
jest.advanceTimersByTime(10);
|
||||||
expect(autofillInlineMenuIframeService["iframe"].style.opacity).toBe("1");
|
expect(autofillInlineMenuIframeService["iframe"].style.opacity).toBe("1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("triggers an aria alert when the password in regenerated", () => {
|
||||||
|
jest.spyOn(autofillInlineMenuIframeService as any, "createAriaAlertElement");
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
|
refreshPassword: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(autofillInlineMenuIframeService["createAriaAlertElement"]).toHaveBeenCalledWith(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
title: "",
|
title: "",
|
||||||
allowtransparency: "true",
|
allowtransparency: "true",
|
||||||
tabIndex: "-1",
|
tabIndex: "-1",
|
||||||
|
scrolling: "no",
|
||||||
};
|
};
|
||||||
private foreignMutationsCount = 0;
|
private foreignMutationsCount = 0;
|
||||||
private mutationObserverIterations = 0;
|
private mutationObserverIterations = 0;
|
||||||
@ -55,6 +56,8 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
updateAutofillInlineMenuColorScheme: () => this.updateAutofillInlineMenuColorScheme(),
|
updateAutofillInlineMenuColorScheme: () => this.updateAutofillInlineMenuColorScheme(),
|
||||||
triggerDelayedAutofillInlineMenuClosure: () => this.handleDelayedAutofillInlineMenuClosure(),
|
triggerDelayedAutofillInlineMenuClosure: () => this.handleDelayedAutofillInlineMenuClosure(),
|
||||||
fadeInAutofillInlineMenuIframe: () => this.handleFadeInInlineMenuIframe(),
|
fadeInAutofillInlineMenuIframe: () => this.handleFadeInInlineMenuIframe(),
|
||||||
|
updateAutofillInlineMenuGeneratedPassword: ({ message }) =>
|
||||||
|
this.handleUpdateGeneratedPassword(message),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -88,7 +91,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
this.iframe.addEventListener(EVENTS.LOAD, this.setupPortMessageListener);
|
this.iframe.addEventListener(EVENTS.LOAD, this.setupPortMessageListener);
|
||||||
|
|
||||||
if (this.ariaAlert) {
|
if (this.ariaAlert) {
|
||||||
this.createAriaAlertElement(this.ariaAlert);
|
this.createAriaAlertElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.shadow.appendChild(this.iframe);
|
this.shadow.appendChild(this.iframe);
|
||||||
@ -98,13 +101,11 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
/**
|
/**
|
||||||
* Creates an aria alert element that is used to announce to screen readers
|
* Creates an aria alert element that is used to announce to screen readers
|
||||||
* when the iframe is loaded.
|
* when the iframe is loaded.
|
||||||
*
|
|
||||||
* @param ariaAlertText - Text to announce to screen readers when the iframe is loaded
|
|
||||||
*/
|
*/
|
||||||
private createAriaAlertElement(ariaAlertText: string) {
|
private createAriaAlertElement(assertive = false) {
|
||||||
this.ariaAlertElement = globalThis.document.createElement("div");
|
this.ariaAlertElement = globalThis.document.createElement("div");
|
||||||
this.ariaAlertElement.setAttribute("role", "alert");
|
this.ariaAlertElement.setAttribute("role", "alert");
|
||||||
this.ariaAlertElement.setAttribute("aria-live", "polite");
|
this.ariaAlertElement.setAttribute("aria-live", assertive ? "assertive" : "polite");
|
||||||
this.ariaAlertElement.setAttribute("aria-atomic", "true");
|
this.ariaAlertElement.setAttribute("aria-atomic", "true");
|
||||||
this.updateElementStyles(this.ariaAlertElement, {
|
this.updateElementStyles(this.ariaAlertElement, {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@ -116,7 +117,6 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
opacity: "0",
|
opacity: "0",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
});
|
});
|
||||||
this.ariaAlertElement.textContent = ariaAlertText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,26 +129,42 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
this.port.onDisconnect.addListener(this.handlePortDisconnect);
|
this.port.onDisconnect.addListener(this.handlePortDisconnect);
|
||||||
this.port.onMessage.addListener(this.handlePortMessage);
|
this.port.onMessage.addListener(this.handlePortMessage);
|
||||||
|
|
||||||
this.announceAriaAlert();
|
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Announces the aria alert element to screen readers when the iframe is loaded.
|
* Announces the aria alert element to screen readers when the iframe is loaded.
|
||||||
|
*
|
||||||
|
* @param textContent - The text content to announce
|
||||||
|
* @param delay - The delay before announcing the text content
|
||||||
|
* @param triggeredByUser - Identifies whether we should present the alert regardless of field focus
|
||||||
*/
|
*/
|
||||||
private announceAriaAlert() {
|
private announceAriaAlert(textContent: string, delay: number, triggeredByUser = false) {
|
||||||
if (!this.ariaAlertElement) {
|
if (!this.ariaAlertElement || !textContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ariaAlertElement.remove();
|
this.ariaAlertElement.remove();
|
||||||
|
this.ariaAlertElement.textContent = textContent;
|
||||||
|
this.clearAriaAlert();
|
||||||
|
|
||||||
|
this.ariaAlertTimeout = globalThis.setTimeout(async () => {
|
||||||
|
const isFieldFocused = await this.sendExtensionMessage("checkIsFieldCurrentlyFocused");
|
||||||
|
if (isFieldFocused || triggeredByUser) {
|
||||||
|
this.shadow.appendChild(this.ariaAlertElement);
|
||||||
|
}
|
||||||
|
this.ariaAlertTimeout = null;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears any existing aria alert that could be announced.
|
||||||
|
*/
|
||||||
|
clearAriaAlert() {
|
||||||
if (this.ariaAlertTimeout) {
|
if (this.ariaAlertTimeout) {
|
||||||
clearTimeout(this.ariaAlertTimeout);
|
clearTimeout(this.ariaAlertTimeout);
|
||||||
|
this.ariaAlertTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ariaAlertTimeout = globalThis.setTimeout(
|
|
||||||
() => this.shadow.appendChild(this.ariaAlertElement),
|
|
||||||
2000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,6 +181,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
|
|
||||||
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px" });
|
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px" });
|
||||||
this.unobserveIframe();
|
this.unobserveIframe();
|
||||||
|
this.clearAriaAlert();
|
||||||
this.port?.onMessage.removeListener(this.handlePortMessage);
|
this.port?.onMessage.removeListener(this.handlePortMessage);
|
||||||
this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
|
this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
|
||||||
this.port?.disconnect();
|
this.port?.disconnect();
|
||||||
@ -267,7 +284,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
this.handleFadeInInlineMenuIframe();
|
this.handleFadeInInlineMenuIframe();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.announceAriaAlert();
|
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -355,10 +372,28 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
|
|
||||||
this.delayedCloseTimeout = globalThis.setTimeout(() => {
|
this.delayedCloseTimeout = globalThis.setTimeout(() => {
|
||||||
this.updateElementStyles(this.iframe, { transition: this.fadeInOpacityTransition });
|
this.updateElementStyles(this.iframe, { transition: this.fadeInOpacityTransition });
|
||||||
|
this.port?.disconnect();
|
||||||
|
this.port = null;
|
||||||
this.forceCloseInlineMenu();
|
this.forceCloseInlineMenu();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles updating the generated password in the inline menu iframe. Triggers
|
||||||
|
* an aria alert if the user initiated the password regeneration.
|
||||||
|
*
|
||||||
|
* @param message - The message sent from the iframe
|
||||||
|
*/
|
||||||
|
private handleUpdateGeneratedPassword = (message: AutofillInlineMenuIframeExtensionMessage) => {
|
||||||
|
this.postMessageToIFrame(message);
|
||||||
|
|
||||||
|
if (message.refreshPassword) {
|
||||||
|
this.clearAriaAlert();
|
||||||
|
this.createAriaAlertElement(true);
|
||||||
|
this.announceAriaAlert(chrome.i18n.getMessage("passwordRegenerated"), 500, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles mutations to the iframe element. The ensures that the iframe
|
* Handles mutations to the iframe element. The ensures that the iframe
|
||||||
* element's styles are not modified by a third party source.
|
* element's styles are not modified by a third party source.
|
||||||
|
@ -9,7 +9,7 @@ export class AutofillInlineMenuListIframe extends AutofillInlineMenuIframeElemen
|
|||||||
AutofillOverlayPort.List,
|
AutofillOverlayPort.List,
|
||||||
{
|
{
|
||||||
height: "0px",
|
height: "0px",
|
||||||
minWidth: "250px",
|
minWidth: "260px",
|
||||||
maxHeight: "180px",
|
maxHeight: "180px",
|
||||||
boxShadow: "rgba(0, 0, 0, 0.1) 2px 4px 6px 0px",
|
boxShadow: "rgba(0, 0, 0, 0.1) 2px 4px 6px 0px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
|
@ -80,7 +80,9 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
|
|
||||||
it("does not post a message to close the autofill inline menu if the button element is hovered", async () => {
|
it("does not post a message to close the autofill inline menu if the button element is hovered", async () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
jest.spyOn(autofillInlineMenuButton["buttonElement"], "matches").mockReturnValue(true);
|
jest
|
||||||
|
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||||
|
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@ -90,9 +92,26 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("triggers a recheck of the button focus state on mouseout", async () => {
|
||||||
|
jest.spyOn(globalThis.document, "removeEventListener");
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||||
|
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||||
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||||
|
|
||||||
|
expect(globalThis.document.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
"mouseout",
|
||||||
|
autofillInlineMenuButton["handleMouseOutEvent"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("posts a message to close the autofill inline menu if the element is not focused during the focus check", async () => {
|
it("posts a message to close the autofill inline menu if the element is not focused during the focus check", async () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
jest.spyOn(autofillInlineMenuButton["buttonElement"], "matches").mockReturnValue(false);
|
jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
@ -117,10 +117,34 @@ export class AutofillInlineMenuButton extends AutofillInlineMenuPageElement {
|
|||||||
* to the parent window indicating that the inline menu should be closed.
|
* to the parent window indicating that the inline menu should be closed.
|
||||||
*/
|
*/
|
||||||
private checkButtonFocused() {
|
private checkButtonFocused() {
|
||||||
if (globalThis.document.hasFocus() || this.buttonElement.matches(":hover")) {
|
if (globalThis.document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isButtonHovered()) {
|
||||||
|
globalThis.document.addEventListener(EVENTS.MOUSEOUT, this.handleMouseOutEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postMessageToParent({ command: "triggerDelayedAutofillInlineMenuClosure" });
|
this.postMessageToParent({ command: "triggerDelayedAutofillInlineMenuClosure" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a re-check of the button's focus status when the mouse leaves the button.
|
||||||
|
*/
|
||||||
|
private handleMouseOutEvent = () => {
|
||||||
|
globalThis.document.removeEventListener(EVENTS.MOUSEOUT, this.handleMouseOutEvent);
|
||||||
|
this.checkButtonFocused();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies whether the button is currently hovered.
|
||||||
|
*/
|
||||||
|
private isButtonHovered() {
|
||||||
|
const hoveredElement = this.buttonElement?.querySelector(":hover");
|
||||||
|
return !!(
|
||||||
|
hoveredElement &&
|
||||||
|
(hoveredElement === this.buttonElement || this.buttonElement.contains(hoveredElement))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,54 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList creates the build save login item view 1`] = `
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-container theme_light"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="save-login inline-menu-list-message"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-button-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label=""
|
||||||
|
class="add-new-item-button inline-menu-list-button inline-menu-list-action"
|
||||||
|
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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that does not have a fill by cipher type 1`] = `
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that does not have a fill by cipher type 1`] = `
|
||||||
<div
|
<div
|
||||||
class="inline-menu-list-container theme_light"
|
class="inline-menu-list-container theme_light"
|
||||||
@ -2177,7 +2226,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline men
|
|||||||
class="locked-inline-menu inline-menu-list-message"
|
class="locked-inline-menu inline-menu-list-message"
|
||||||
id="locked-inline-menu-description"
|
id="locked-inline-menu-description"
|
||||||
>
|
>
|
||||||
unlockYourAccount
|
unlockYourAccountToViewAutofillSuggestions
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="inline-menu-list-button-container"
|
class="inline-menu-list-button-container"
|
||||||
@ -2220,3 +2269,188 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline men
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the password generator view creates the views for the password generator 1`] = `
|
||||||
|
<div
|
||||||
|
class="password-generator-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="password-generator-actions"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label=""
|
||||||
|
class="fill-generated-password-button inline-menu-list-action"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 24 25"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21.803 3.035a7.453 7.453 0 0 0-2.427-1.567 7.763 7.763 0 0 0-2.877-.551c-.988 0-1.967.187-2.878.55a7.455 7.455 0 0 0-2.427 1.568A7.193 7.193 0 0 0 9.283 6.23a6.936 6.936 0 0 0-.023 3.675.556.556 0 0 1-.16.549L.656 18.61a.77.77 0 0 0-.233.468l-.415 3.756a.722.722 0 0 0 .04.354.773.773 0 0 0 .203.3.85.85 0 0 0 .697.201l5.141-.855a.832.832 0 0 0 .461-.241.757.757 0 0 0 .211-.458l.108-1.162a.554.554 0 0 1 .17-.35.62.62 0 0 1 .365-.167l1.2-.105a.832.832 0 0 0 .503-.23.756.756 0 0 0 .23-.482l.124-1.326a.361.361 0 0 1 .111-.23.4.4 0 0 1 .24-.108l1.381-.113a.815.815 0 0 0 .501-.225l2.473-2.386a.506.506 0 0 1 .48-.126 7.904 7.904 0 0 0 1.912.235 7.68 7.68 0 0 0 2.846-.539 7.344 7.344 0 0 0 2.402-1.546C23.213 11.905 24 10.069 24 8.155c0-1.914-.787-3.752-2.194-5.122l-.003.002Zm-10.81 7.148a5.496 5.496 0 0 1-.25-3.208 5.677 5.677 0 0 1 1.6-2.835 5.828 5.828 0 0 1 1.902-1.233 6.075 6.075 0 0 1 4.515 0 5.829 5.829 0 0 1 1.902 1.233c1.107 1.073 1.726 2.514 1.726 4.016 0 1.501-.62 2.943-1.726 4.016a5.925 5.925 0 0 1-2.93 1.537 6.135 6.135 0 0 1-3.339-.245.844.844 0 0 0-.85.182l-2.498 2.409a1.124 1.124 0 0 1-.682.308l-1.687.142a.839.839 0 0 0-.503.23.754.754 0 0 0-.23.482l-.105 1.13a.594.594 0 0 1-.181.374.653.653 0 0 1-.39.178l-1.171.1a.832.832 0 0 0-.503.23.755.755 0 0 0-.23.483l-.122 1.313a.474.474 0 0 1-.13.287.518.518 0 0 1-.288.151l-2.66.439a.36.36 0 0 1-.286-.084.314.314 0 0 1-.102-.266l.182-1.758a.724.724 0 0 1 .222-.449l8.636-8.333a.778.778 0 0 0 .215-.39.756.756 0 0 0-.036-.439h-.001Zm6.976-1.226c-.474 0-.938-.134-1.332-.384a2.31 2.31 0 0 1-.884-1.022 2.17 2.17 0 0 1-.137-1.317c.093-.442.321-.848.657-1.166a2.441 2.441 0 0 1 1.228-.624 2.516 2.516 0 0 1 1.386.13 2.37 2.37 0 0 1 1.077.84c.263.374.404.814.404 1.265 0 .605-.253 1.184-.703 1.611-.45.428-1.06.667-1.696.667Zm0-3.56c-.266 0-.527.075-.75.216-.221.14-.394.34-.496.575a1.22 1.22 0 0 0-.077.74c.053.249.18.477.37.657.189.18.43.3.691.35.262.05.533.025.78-.072.246-.097.457-.261.606-.472a1.235 1.235 0 0 0-.168-1.619 1.369 1.369 0 0 0-.954-.376v.002l-.002-.001Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .308h24v24H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="password-generator-content"
|
||||||
|
id="password-generator-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="password-generator-heading"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-label=": g e n e r a t e d P a s s w o r d 1 "
|
||||||
|
class="colorized-password"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
g
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
e
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
n
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
e
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
r
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
a
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
t
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
e
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
d
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
a
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
s
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
s
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
w
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
o
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
r
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-letter"
|
||||||
|
>
|
||||||
|
d
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-special"
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="password-number"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label=""
|
||||||
|
class="refresh-generated-password-button inline-menu-list-action"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="21"
|
||||||
|
viewBox="0 0 20 21"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M18.383 11.37a.678.678 0 0 0-.496.086.65.65 0 0 0-.291.402 7.457 7.457 0 0 1-2.451 3.912 7.754 7.754 0 0 1-4.328 1.78 7.761 7.761 0 0 1-4.554-.901 7.502 7.502 0 0 1-3.167-3.318c-.025-.064.03-.159.165-.14l1.039.417a.687.687 0 0 0 .51.005.662.662 0 0 0 .365-.346.62.62 0 0 0-.142-.694.64.64 0 0 0-.214-.136l-2.656-1.061a.686.686 0 0 0-.854.31L.065 14.139a.621.621 0 0 0 .31.847.69.69 0 0 0 .639-.033.653.653 0 0 0 .247-.261l.4-.792a.167.167 0 0 1 .124-.077.173.173 0 0 1 .075.01.16.16 0 0 1 .063.04 8.813 8.813 0 0 0 3.29 3.627 9.109 9.109 0 0 0 4.764 1.358c.312 0 .632-.015.961-.044a9.223 9.223 0 0 0 5.065-2.116 8.871 8.871 0 0 0 2.89-4.578.628.628 0 0 0-.274-.656.655.655 0 0 0-.236-.095v.001Zm1.25-5.735a.693.693 0 0 0-.64.033.659.659 0 0 0-.247.262l-.4.79a.166.166 0 0 1-.261.028 8.809 8.809 0 0 0-3.29-3.63 9.113 9.113 0 0 0-4.764-1.36c-.311 0-.631.014-.961.045A9.224 9.224 0 0 0 4.004 3.92a8.863 8.863 0 0 0-2.89 4.58.622.622 0 0 0 .276.658.657.657 0 0 0 .237.094c.17.036.349.005.496-.086a.65.65 0 0 0 .29-.402 7.452 7.452 0 0 1 2.452-3.911 7.764 7.764 0 0 1 4.328-1.781 7.761 7.761 0 0 1 4.553.902 7.508 7.508 0 0 1 3.168 3.317c.023.063-.03.16-.165.138l-1.042-.42a.688.688 0 0 0-.509-.004.666.666 0 0 0-.367.345.622.622 0 0 0 .357.83l2.65 1.06c.156.064.33.067.489.01a.665.665 0 0 0 .365-.318l1.243-2.454a.622.622 0 0 0-.302-.843Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .421h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
@ -13,6 +13,7 @@ import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils
|
|||||||
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
|
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
|
||||||
|
|
||||||
describe("AutofillInlineMenuList", () => {
|
describe("AutofillInlineMenuList", () => {
|
||||||
|
const generatedPassword = "generatedPassword!1";
|
||||||
globalThis.customElements.define("autofill-inline-menu-list", AutofillInlineMenuList);
|
globalThis.customElements.define("autofill-inline-menu-list", AutofillInlineMenuList);
|
||||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||||
observe: jest.fn(),
|
observe: jest.fn(),
|
||||||
@ -83,7 +84,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
createInitAutofillInlineMenuListMessageMock({
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
ciphers: [],
|
ciphers: [],
|
||||||
filledByCipherType: CipherType.Card,
|
inlineMenuFillType: CipherType.Card,
|
||||||
portKey,
|
portKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -96,7 +97,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
createInitAutofillInlineMenuListMessageMock({
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
ciphers: [],
|
ciphers: [],
|
||||||
filledByCipherType: CipherType.Identity,
|
inlineMenuFillType: CipherType.Identity,
|
||||||
portKey,
|
portKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -109,7 +110,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
createInitAutofillInlineMenuListMessageMock({
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
ciphers: [],
|
ciphers: [],
|
||||||
filledByCipherType: undefined,
|
inlineMenuFillType: undefined,
|
||||||
portKey,
|
portKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -142,7 +143,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
it("creates the views for a list of card ciphers", () => {
|
it("creates the views for a list of card ciphers", () => {
|
||||||
postWindowMessage(
|
postWindowMessage(
|
||||||
createInitAutofillInlineMenuListMessageMock({
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
filledByCipherType: CipherType.Card,
|
inlineMenuFillType: CipherType.Card,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
createAutofillOverlayCipherDataMock(1, {
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
type: CipherType.Card,
|
type: CipherType.Card,
|
||||||
@ -172,7 +173,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
it("creates the views for a list of identity ciphers", () => {
|
it("creates the views for a list of identity ciphers", () => {
|
||||||
postWindowMessage(
|
postWindowMessage(
|
||||||
createInitAutofillInlineMenuListMessageMock({
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
filledByCipherType: CipherType.Card,
|
inlineMenuFillType: CipherType.Card,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
createAutofillOverlayCipherDataMock(1, {
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
type: CipherType.Identity,
|
type: CipherType.Identity,
|
||||||
@ -228,6 +229,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
describe("fill cipher button event listeners", () => {
|
describe("fill cipher button event listeners", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
|
||||||
|
jest.spyOn(autofillInlineMenuList as any, "isListHovered").mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("filling a cipher", () => {
|
describe("filling a cipher", () => {
|
||||||
@ -473,7 +475,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
postWindowMessage(
|
postWindowMessage(
|
||||||
createInitAutofillInlineMenuListMessageMock({
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
filledByCipherType: CipherType.Login,
|
inlineMenuFillType: CipherType.Login,
|
||||||
showInlineMenuAccountCreation: true,
|
showInlineMenuAccountCreation: true,
|
||||||
portKey,
|
portKey,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
@ -718,6 +720,171 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the password generator view", () => {
|
||||||
|
it("creates the views for the password generator", async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
generatedPassword,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(autofillInlineMenuList["passwordGeneratorContainer"]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fill generated password button event listeners", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({ generatedPassword, portKey }),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers a fill of the generated password on click", () => {
|
||||||
|
const fillGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".fill-generated-password-button");
|
||||||
|
|
||||||
|
fillGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
|
{ command: "fillGeneratedPassword", portKey },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("keyup events on the fill generated password button", () => {
|
||||||
|
it("skips acting on keyup events that have the shiftKey pressed in combination", () => {
|
||||||
|
const fillGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".fill-generated-password-button");
|
||||||
|
|
||||||
|
fillGeneratedPasswordButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keyup", { code: "Space", shiftKey: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||||
|
{ command: "fillGeneratedPassword", portKey },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers a fill of the generated password on keyup of the `Space` key", () => {
|
||||||
|
const fillGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".fill-generated-password-button");
|
||||||
|
|
||||||
|
fillGeneratedPasswordButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keyup", { code: "Space" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
|
{ command: "fillGeneratedPassword", portKey },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("focuses the refresh generated password button on `ArrowRight`", () => {
|
||||||
|
const fillGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".fill-generated-password-button");
|
||||||
|
const refreshGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".refresh-generated-password-button");
|
||||||
|
jest.spyOn(refreshGeneratedPasswordButton as HTMLElement, "focus");
|
||||||
|
|
||||||
|
fillGeneratedPasswordButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keyup", { code: "ArrowRight" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect((refreshGeneratedPasswordButton as HTMLElement).focus).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("refresh generated password button event listeners", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({ generatedPassword, portKey }),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers a refresh of the generated password on click", () => {
|
||||||
|
const refreshGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".refresh-generated-password-button");
|
||||||
|
|
||||||
|
refreshGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
|
{ command: "refreshGeneratedPassword", portKey },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("keyup events on the refresh generated password button", () => {
|
||||||
|
it("skips acting on keyup events that have the shiftKey pressed in combination", () => {
|
||||||
|
const refreshGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".refresh-generated-password-button");
|
||||||
|
|
||||||
|
refreshGeneratedPasswordButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keyup", { code: "Space", shiftKey: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||||
|
{ command: "refreshGeneratedPassword", portKey },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers a refresh of the generated password on press of the `Space` key", () => {
|
||||||
|
const refreshGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".refresh-generated-password-button");
|
||||||
|
|
||||||
|
refreshGeneratedPasswordButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keyup", { code: "Space" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
|
{ command: "refreshGeneratedPassword", portKey },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("focuses the fill generated password button on `ArrowLeft`", () => {
|
||||||
|
const fillGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".fill-generated-password-button");
|
||||||
|
const refreshGeneratedPasswordButton = autofillInlineMenuList[
|
||||||
|
"passwordGeneratorContainer"
|
||||||
|
].querySelector(".refresh-generated-password-button");
|
||||||
|
jest.spyOn(fillGeneratedPasswordButton as HTMLElement, "focus");
|
||||||
|
|
||||||
|
refreshGeneratedPasswordButton.dispatchEvent(
|
||||||
|
new KeyboardEvent("keyup", { code: "ArrowLeft" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect((fillGeneratedPasswordButton as HTMLElement).focus).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the build save login item view", async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
showSaveLoginMenu: true,
|
||||||
|
generatedPassword,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("global event listener handlers", () => {
|
describe("global event listener handlers", () => {
|
||||||
@ -736,19 +903,35 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
it("does not post a `checkAutofillInlineMenuButtonFocused` message if the inline menu list is currently hovered", () => {
|
it("does not post a `checkAutofillInlineMenuButtonFocused` message if the inline menu list is currently hovered", () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "matches")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(true);
|
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("triggers a recheck of the list focus state on mouseout", async () => {
|
||||||
|
jest.spyOn(globalThis.document, "removeEventListener");
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
|
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||||
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||||
|
expect(globalThis.document.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
"mouseout",
|
||||||
|
autofillInlineMenuList["handleMouseOutEvent"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("posts a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is not currently focused", () => {
|
it("posts a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is not currently focused", () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "matches")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(false);
|
.mockReturnValue(null);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
||||||
|
|
||||||
@ -767,6 +950,109 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
expect(updateCiphersSpy).toHaveBeenCalled();
|
expect(updateCiphersSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("updating the password generator view", () => {
|
||||||
|
let buildPasswordGeneratorSpy: jest.SpyInstance;
|
||||||
|
let buildColorizedPasswordElementSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
buildPasswordGeneratorSpy = jest.spyOn(
|
||||||
|
autofillInlineMenuList as any,
|
||||||
|
"buildPasswordGenerator",
|
||||||
|
);
|
||||||
|
buildColorizedPasswordElementSpy = jest.spyOn(
|
||||||
|
autofillInlineMenuList as any,
|
||||||
|
"buildColorizedPasswordElement",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips updating the password generator if the user is not authed", async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
authStatus: AuthenticationStatus.Locked,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({
|
||||||
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
|
generatedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips update the password generator if the message does not contain a password", async () => {
|
||||||
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword" });
|
||||||
|
|
||||||
|
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds the password generator if the colorized password element is not present", async () => {
|
||||||
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({
|
||||||
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
|
generatedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildPasswordGeneratorSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces the colorized password element if it is present", async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
generatedPassword,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({
|
||||||
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
|
generatedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(buildColorizedPasswordElementSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("displaying the save login view", () => {
|
||||||
|
let buildSaveLoginInlineMenuListSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
buildSaveLoginInlineMenuListSpy = jest.spyOn(
|
||||||
|
autofillInlineMenuList as any,
|
||||||
|
"buildSaveLoginInlineMenuList",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips displaying the save login item view if the user is not authenticated", async () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
authStatus: AuthenticationStatus.Locked,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
||||||
|
|
||||||
|
expect(buildSaveLoginInlineMenuListSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds the save login item view", async () => {
|
||||||
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
||||||
|
|
||||||
|
expect(buildSaveLoginInlineMenuListSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("directing user focus into the inline menu list", () => {
|
describe("directing user focus into the inline menu list", () => {
|
||||||
it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => {
|
it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => {
|
||||||
postWindowMessage(
|
postWindowMessage(
|
||||||
|
@ -1,29 +1,36 @@
|
|||||||
import "@webcomponents/custom-elements";
|
import "@webcomponents/custom-elements";
|
||||||
import "lit/polyfill-support.js";
|
import "lit/polyfill-support.js";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants";
|
import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||||
import { buildSvgDomElement, throttle } from "../../../../utils";
|
import { InlineMenuFillTypes } from "../../../../enums/autofill-overlay.enum";
|
||||||
|
import { buildSvgDomElement, specialCharacterToKeyMap, throttle } from "../../../../utils";
|
||||||
import {
|
import {
|
||||||
creditCardIcon,
|
creditCardIcon,
|
||||||
globeIcon,
|
globeIcon,
|
||||||
idCardIcon,
|
idCardIcon,
|
||||||
lockIcon,
|
lockIcon,
|
||||||
|
passkeyIcon,
|
||||||
plusIcon,
|
plusIcon,
|
||||||
viewCipherIcon,
|
viewCipherIcon,
|
||||||
passkeyIcon,
|
keyIcon,
|
||||||
|
refreshIcon,
|
||||||
spinnerIcon,
|
spinnerIcon,
|
||||||
} from "../../../../utils/svg-icons";
|
} from "../../../../utils/svg-icons";
|
||||||
import {
|
import {
|
||||||
AutofillInlineMenuListWindowMessageHandlers,
|
AutofillInlineMenuListWindowMessageHandlers,
|
||||||
InitAutofillInlineMenuListMessage,
|
InitAutofillInlineMenuListMessage,
|
||||||
|
UpdateAutofillInlineMenuGeneratedPasswordMessage,
|
||||||
|
UpdateAutofillInlineMenuListCiphersParams,
|
||||||
} from "../../abstractions/autofill-inline-menu-list";
|
} from "../../abstractions/autofill-inline-menu-list";
|
||||||
import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element";
|
import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element";
|
||||||
|
|
||||||
export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||||
private inlineMenuListContainer: HTMLDivElement;
|
private inlineMenuListContainer: HTMLDivElement;
|
||||||
|
private passwordGeneratorContainer: HTMLDivElement;
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
||||||
private ciphers: InlineMenuCipherData[] = [];
|
private ciphers: InlineMenuCipherData[] = [];
|
||||||
@ -31,7 +38,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
private cipherListScrollIsDebounced = false;
|
private cipherListScrollIsDebounced = false;
|
||||||
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
|
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
|
||||||
private currentCipherIndex = 0;
|
private currentCipherIndex = 0;
|
||||||
private filledByCipherType: CipherType;
|
private inlineMenuFillType: InlineMenuFillTypes;
|
||||||
private showInlineMenuAccountCreation: boolean;
|
private showInlineMenuAccountCreation: boolean;
|
||||||
private showPasskeysLabels: boolean;
|
private showPasskeysLabels: boolean;
|
||||||
private newItemButtonElement: HTMLButtonElement;
|
private newItemButtonElement: HTMLButtonElement;
|
||||||
@ -42,14 +49,17 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
private lastPasskeysListItemHeight: number;
|
private lastPasskeysListItemHeight: number;
|
||||||
private ciphersListHeight: number;
|
private ciphersListHeight: number;
|
||||||
private isPasskeyAuthInProgress = false;
|
private isPasskeyAuthInProgress = false;
|
||||||
|
private authStatus: AuthenticationStatus;
|
||||||
private readonly showCiphersPerPage = 6;
|
private readonly showCiphersPerPage = 6;
|
||||||
private readonly headingBorderClass = "inline-menu-list-heading--bordered";
|
private readonly headingBorderClass = "inline-menu-list-heading--bordered";
|
||||||
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
|
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
|
||||||
{
|
{
|
||||||
initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message),
|
initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message),
|
||||||
checkAutofillInlineMenuListFocused: () => this.checkInlineMenuListFocused(),
|
checkAutofillInlineMenuListFocused: () => this.checkInlineMenuListFocused(),
|
||||||
updateAutofillInlineMenuListCiphers: ({ message }) =>
|
updateAutofillInlineMenuListCiphers: ({ message }) => this.updateListItems(message),
|
||||||
this.updateListItems(message.ciphers, message.showInlineMenuAccountCreation),
|
updateAutofillInlineMenuGeneratedPassword: ({ message }) =>
|
||||||
|
this.handleUpdateAutofillInlineMenuGeneratedPassword(message),
|
||||||
|
showSaveLoginInlineMenuList: () => this.handleShowSaveLoginInlineMenuList(),
|
||||||
focusAutofillInlineMenuList: () => this.focusInlineMenuList(),
|
focusAutofillInlineMenuList: () => this.focusInlineMenuList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,27 +73,22 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
* Initializes the inline menu list and updates the list items with the passed ciphers.
|
* Initializes the inline menu list and updates the list items with the passed ciphers.
|
||||||
* If the auth status is not `Unlocked`, the locked inline menu is built.
|
* If the auth status is not `Unlocked`, the locked inline menu is built.
|
||||||
*
|
*
|
||||||
* @param translations - The translations to use for the inline menu list.
|
* @param message - The message containing the data to initialize the inline menu list.
|
||||||
* @param styleSheetUrl - The URL of the stylesheet to use for the inline menu list.
|
|
||||||
* @param theme - The theme to use for the inline menu list.
|
|
||||||
* @param authStatus - The current authentication status.
|
|
||||||
* @param ciphers - The ciphers to display in the inline menu list.
|
|
||||||
* @param portKey - Background generated key that allows the port to communicate with the background.
|
|
||||||
* @param filledByCipherType - The type of cipher that fills the current field.
|
|
||||||
* @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields.
|
|
||||||
* @param showPasskeysLabels - Whether passkeys labels are shown in the inline menu list.
|
|
||||||
*/
|
*/
|
||||||
private async initAutofillInlineMenuList({
|
private async initAutofillInlineMenuList(message: InitAutofillInlineMenuListMessage) {
|
||||||
translations,
|
const {
|
||||||
styleSheetUrl,
|
translations,
|
||||||
theme,
|
styleSheetUrl,
|
||||||
authStatus,
|
theme,
|
||||||
ciphers,
|
authStatus,
|
||||||
portKey,
|
ciphers,
|
||||||
filledByCipherType,
|
portKey,
|
||||||
showInlineMenuAccountCreation,
|
inlineMenuFillType,
|
||||||
showPasskeysLabels,
|
showInlineMenuAccountCreation,
|
||||||
}: InitAutofillInlineMenuListMessage) {
|
showPasskeysLabels,
|
||||||
|
generatedPassword,
|
||||||
|
showSaveLoginMenu,
|
||||||
|
} = message;
|
||||||
const linkElement = await this.initAutofillInlineMenuPage(
|
const linkElement = await this.initAutofillInlineMenuPage(
|
||||||
"list",
|
"list",
|
||||||
styleSheetUrl,
|
styleSheetUrl,
|
||||||
@ -91,7 +96,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
portKey,
|
portKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.filledByCipherType = filledByCipherType;
|
this.authStatus = authStatus;
|
||||||
|
this.inlineMenuFillType = inlineMenuFillType;
|
||||||
this.showPasskeysLabels = showPasskeysLabels;
|
this.showPasskeysLabels = showPasskeysLabels;
|
||||||
|
|
||||||
const themeClass = `theme_${theme}`;
|
const themeClass = `theme_${theme}`;
|
||||||
@ -103,12 +109,25 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
|
|
||||||
this.shadowDom.append(linkElement, this.inlineMenuListContainer);
|
this.shadowDom.append(linkElement, this.inlineMenuListContainer);
|
||||||
|
|
||||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||||
this.updateListItems(ciphers, showInlineMenuAccountCreation);
|
this.buildLockedInlineMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buildLockedInlineMenu();
|
if (showSaveLoginMenu) {
|
||||||
|
this.buildSaveLoginInlineMenuList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generatedPassword) {
|
||||||
|
this.buildPasswordGenerator(generatedPassword);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateListItems({
|
||||||
|
ciphers,
|
||||||
|
showInlineMenuAccountCreation,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,7 +138,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
const lockedInlineMenu = globalThis.document.createElement("div");
|
const lockedInlineMenu = globalThis.document.createElement("div");
|
||||||
lockedInlineMenu.id = "locked-inline-menu-description";
|
lockedInlineMenu.id = "locked-inline-menu-description";
|
||||||
lockedInlineMenu.classList.add("locked-inline-menu", "inline-menu-list-message");
|
lockedInlineMenu.classList.add("locked-inline-menu", "inline-menu-list-message");
|
||||||
lockedInlineMenu.textContent = this.getTranslation("unlockYourAccount");
|
lockedInlineMenu.textContent = this.getTranslation(
|
||||||
|
"unlockYourAccountToViewAutofillSuggestions",
|
||||||
|
);
|
||||||
|
|
||||||
const unlockButtonElement = globalThis.document.createElement("button");
|
const unlockButtonElement = globalThis.document.createElement("button");
|
||||||
unlockButtonElement.id = "unlock-button";
|
unlockButtonElement.id = "unlock-button";
|
||||||
@ -139,6 +160,30 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
this.inlineMenuListContainer.append(lockedInlineMenu, inlineMenuListButtonContainer);
|
this.inlineMenuListContainer.append(lockedInlineMenu, inlineMenuListButtonContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the inline menu list as a prompt that asks the user if they'd like to save the login data.
|
||||||
|
*/
|
||||||
|
private buildSaveLoginInlineMenuList() {
|
||||||
|
const saveLoginMessage = globalThis.document.createElement("div");
|
||||||
|
saveLoginMessage.classList.add("save-login", "inline-menu-list-message");
|
||||||
|
saveLoginMessage.textContent = this.getTranslation("saveLoginToBitwarden");
|
||||||
|
|
||||||
|
const newItemButton = this.buildNewItemButton(true);
|
||||||
|
this.showInlineMenuAccountCreation = true;
|
||||||
|
|
||||||
|
this.inlineMenuListContainer.append(saveLoginMessage, newItemButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the show save login inline menu list message that is triggered from the background script.
|
||||||
|
*/
|
||||||
|
private handleShowSaveLoginInlineMenuList() {
|
||||||
|
if (this.authStatus === AuthenticationStatus.Unlocked) {
|
||||||
|
this.resetInlineMenuContainer();
|
||||||
|
this.buildSaveLoginInlineMenuList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the click event for the unlock button.
|
* Handles the click event for the unlock button.
|
||||||
* Sends a message to the parent window to unlock the vault.
|
* Sends a message to the parent window to unlock the vault.
|
||||||
@ -147,6 +192,224 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
this.postMessageToParent({ command: "unlockVault" });
|
this.postMessageToParent({ command: "unlockVault" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the password generator within the inline menu.
|
||||||
|
*
|
||||||
|
* @param generatedPassword - The generated password to display.
|
||||||
|
*/
|
||||||
|
private buildPasswordGenerator(generatedPassword: string) {
|
||||||
|
this.passwordGeneratorContainer = globalThis.document.createElement("div");
|
||||||
|
this.passwordGeneratorContainer.classList.add("password-generator-container");
|
||||||
|
|
||||||
|
const passwordGeneratorActions = globalThis.document.createElement("div");
|
||||||
|
passwordGeneratorActions.classList.add("password-generator-actions");
|
||||||
|
|
||||||
|
const fillGeneratedPasswordButton = globalThis.document.createElement("button");
|
||||||
|
fillGeneratedPasswordButton.tabIndex = -1;
|
||||||
|
fillGeneratedPasswordButton.classList.add(
|
||||||
|
"fill-generated-password-button",
|
||||||
|
"inline-menu-list-action",
|
||||||
|
);
|
||||||
|
fillGeneratedPasswordButton.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
this.getTranslation("fillGeneratedPassword"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passwordGeneratorHeading = globalThis.document.createElement("div");
|
||||||
|
passwordGeneratorHeading.classList.add("password-generator-heading");
|
||||||
|
passwordGeneratorHeading.textContent = this.getTranslation("fillGeneratedPassword");
|
||||||
|
|
||||||
|
const passwordGeneratorContent = globalThis.document.createElement("div");
|
||||||
|
passwordGeneratorContent.id = "password-generator-content";
|
||||||
|
passwordGeneratorContent.classList.add("password-generator-content");
|
||||||
|
passwordGeneratorContent.append(
|
||||||
|
passwordGeneratorHeading,
|
||||||
|
this.buildColorizedPasswordElement(generatedPassword),
|
||||||
|
);
|
||||||
|
|
||||||
|
fillGeneratedPasswordButton.append(buildSvgDomElement(keyIcon), passwordGeneratorContent);
|
||||||
|
fillGeneratedPasswordButton.addEventListener(
|
||||||
|
EVENTS.CLICK,
|
||||||
|
this.handleFillGeneratedPasswordClick,
|
||||||
|
);
|
||||||
|
fillGeneratedPasswordButton.addEventListener(
|
||||||
|
EVENTS.KEYUP,
|
||||||
|
this.handleFillGeneratedPasswordKeyUp,
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshGeneratedPasswordButton = globalThis.document.createElement("button");
|
||||||
|
refreshGeneratedPasswordButton.tabIndex = -1;
|
||||||
|
refreshGeneratedPasswordButton.classList.add(
|
||||||
|
"refresh-generated-password-button",
|
||||||
|
"inline-menu-list-action",
|
||||||
|
);
|
||||||
|
refreshGeneratedPasswordButton.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
this.getTranslation("regeneratePassword"),
|
||||||
|
);
|
||||||
|
refreshGeneratedPasswordButton.appendChild(buildSvgDomElement(refreshIcon));
|
||||||
|
refreshGeneratedPasswordButton.addEventListener(
|
||||||
|
EVENTS.CLICK,
|
||||||
|
this.handleRefreshGeneratedPasswordClick,
|
||||||
|
);
|
||||||
|
refreshGeneratedPasswordButton.addEventListener(
|
||||||
|
EVENTS.KEYUP,
|
||||||
|
this.handleRefreshGeneratedPasswordKeyUp,
|
||||||
|
);
|
||||||
|
|
||||||
|
passwordGeneratorActions.append(fillGeneratedPasswordButton, refreshGeneratedPasswordButton);
|
||||||
|
|
||||||
|
this.passwordGeneratorContainer.appendChild(passwordGeneratorActions);
|
||||||
|
this.inlineMenuListContainer.appendChild(this.passwordGeneratorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the colorized password content element.
|
||||||
|
*
|
||||||
|
* @param password - The password to display.
|
||||||
|
*/
|
||||||
|
private buildColorizedPasswordElement(password: string) {
|
||||||
|
let ariaDescription = `${this.getTranslation("generatedPassword")}: `;
|
||||||
|
const passwordContainer = globalThis.document.createElement("div");
|
||||||
|
passwordContainer.classList.add("colorized-password");
|
||||||
|
const appendPasswordCharacter = (character: string, type: string) => {
|
||||||
|
const characterElement = globalThis.document.createElement("div");
|
||||||
|
characterElement.classList.add(`password-${type}`);
|
||||||
|
characterElement.textContent = character;
|
||||||
|
|
||||||
|
passwordContainer.appendChild(characterElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordArray = Array.from(password);
|
||||||
|
for (let i = 0; i < passwordArray.length; i++) {
|
||||||
|
const character = passwordArray[i];
|
||||||
|
|
||||||
|
if (character.match(/\W/)) {
|
||||||
|
appendPasswordCharacter(character, "special");
|
||||||
|
ariaDescription += `${this.getTranslation(specialCharacterToKeyMap[character])} `;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character.match(/\d/)) {
|
||||||
|
appendPasswordCharacter(character, "number");
|
||||||
|
ariaDescription += `${character} `;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendPasswordCharacter(character, "letter");
|
||||||
|
ariaDescription +=
|
||||||
|
character === character.toLowerCase()
|
||||||
|
? `${this.getTranslation("lowercaseAriaLabel")} ${character} `
|
||||||
|
: `${this.getTranslation("uppercaseAriaLabel")} ${character} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordContainer.setAttribute("aria-label", ariaDescription);
|
||||||
|
return passwordContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the click event for the fill generated password button. Triggers
|
||||||
|
* a message to the background script to fill the generated password.
|
||||||
|
*/
|
||||||
|
private handleFillGeneratedPasswordClick = () => {
|
||||||
|
this.postMessageToParent({ command: "fillGeneratedPassword" });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the keyup event for the fill generated password button.
|
||||||
|
*
|
||||||
|
* @param event - The keyup event.
|
||||||
|
*/
|
||||||
|
private handleFillGeneratedPasswordKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code === "Space") {
|
||||||
|
this.handleFillGeneratedPasswordClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.code === "ArrowRight" &&
|
||||||
|
event.target instanceof HTMLElement &&
|
||||||
|
event.target.nextElementSibling
|
||||||
|
) {
|
||||||
|
(event.target.nextElementSibling as HTMLElement).focus();
|
||||||
|
event.target.parentElement.classList.add("remove-outline");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the click event of the password regenerator button.
|
||||||
|
*
|
||||||
|
* @param event - The click event.
|
||||||
|
*/
|
||||||
|
private handleRefreshGeneratedPasswordClick = (event?: MouseEvent) => {
|
||||||
|
if (event) {
|
||||||
|
(event.target as HTMLElement)
|
||||||
|
.closest(".password-generator-actions")
|
||||||
|
?.classList.add("remove-outline");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postMessageToParent({ command: "refreshGeneratedPassword" });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the keyup event for the password regenerator button.
|
||||||
|
*
|
||||||
|
* @param event - The keyup event.
|
||||||
|
*/
|
||||||
|
private handleRefreshGeneratedPasswordKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code === "Space") {
|
||||||
|
this.handleRefreshGeneratedPasswordClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.code === "ArrowLeft" &&
|
||||||
|
event.target instanceof HTMLElement &&
|
||||||
|
event.target.previousElementSibling
|
||||||
|
) {
|
||||||
|
(event.target.previousElementSibling as HTMLElement).focus();
|
||||||
|
event.target.parentElement.classList.remove("remove-outline");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the generated password content element with the passed generated password.
|
||||||
|
*
|
||||||
|
* @param message - The message containing the generated password.
|
||||||
|
*/
|
||||||
|
private handleUpdateAutofillInlineMenuGeneratedPassword(
|
||||||
|
message: UpdateAutofillInlineMenuGeneratedPasswordMessage,
|
||||||
|
) {
|
||||||
|
if (this.authStatus !== AuthenticationStatus.Unlocked || !message.generatedPassword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordGeneratorContentElement = this.inlineMenuListContainer.querySelector(
|
||||||
|
"#password-generator-content",
|
||||||
|
);
|
||||||
|
const colorizedPasswordElement =
|
||||||
|
passwordGeneratorContentElement?.querySelector(".colorized-password");
|
||||||
|
if (!colorizedPasswordElement) {
|
||||||
|
this.resetInlineMenuContainer();
|
||||||
|
this.buildPasswordGenerator(message.generatedPassword);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
colorizedPasswordElement.replaceWith(
|
||||||
|
this.buildColorizedPasswordElement(message.generatedPassword),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the list items with the passed ciphers.
|
* Updates the list items with the passed ciphers.
|
||||||
* If no ciphers are passed, the no results inline menu is built.
|
* If no ciphers are passed, the no results inline menu is built.
|
||||||
@ -154,10 +417,10 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
* @param ciphers - The ciphers to display in the inline menu list.
|
* @param ciphers - The ciphers to display in the inline menu list.
|
||||||
* @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields.
|
* @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields.
|
||||||
*/
|
*/
|
||||||
private updateListItems(
|
private updateListItems({
|
||||||
ciphers: InlineMenuCipherData[],
|
ciphers,
|
||||||
showInlineMenuAccountCreation?: boolean,
|
showInlineMenuAccountCreation,
|
||||||
) {
|
}: UpdateAutofillInlineMenuListCiphersParams) {
|
||||||
if (this.isPasskeyAuthInProgress) {
|
if (this.isPasskeyAuthInProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -221,7 +484,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
/**
|
/**
|
||||||
* Builds a "New Item" button and returns the container of that button.
|
* Builds a "New Item" button and returns the container of that button.
|
||||||
*/
|
*/
|
||||||
private buildNewItemButton() {
|
private buildNewItemButton(showLogin = false) {
|
||||||
this.newItemButtonElement = globalThis.document.createElement("button");
|
this.newItemButtonElement = globalThis.document.createElement("button");
|
||||||
this.newItemButtonElement.tabIndex = -1;
|
this.newItemButtonElement.tabIndex = -1;
|
||||||
this.newItemButtonElement.id = "new-item-button";
|
this.newItemButtonElement.id = "new-item-button";
|
||||||
@ -230,8 +493,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
"inline-menu-list-button",
|
"inline-menu-list-button",
|
||||||
"inline-menu-list-action",
|
"inline-menu-list-action",
|
||||||
);
|
);
|
||||||
this.newItemButtonElement.textContent = this.getNewItemButtonText();
|
this.newItemButtonElement.textContent = this.getNewItemButtonText(showLogin);
|
||||||
this.newItemButtonElement.setAttribute("aria-label", this.getNewItemAriaLabel());
|
this.newItemButtonElement.setAttribute("aria-label", this.getNewItemAriaLabel(showLogin));
|
||||||
this.newItemButtonElement.prepend(buildSvgDomElement(plusIcon));
|
this.newItemButtonElement.prepend(buildSvgDomElement(plusIcon));
|
||||||
this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
|
this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
|
||||||
|
|
||||||
@ -241,8 +504,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
/**
|
/**
|
||||||
* Gets the new item text for the button based on the cipher type the focused field is filled by.
|
* Gets the new item text for the button based on the cipher type the focused field is filled by.
|
||||||
*/
|
*/
|
||||||
private getNewItemButtonText() {
|
private getNewItemButtonText(showLogin: boolean) {
|
||||||
if (this.isFilledByLoginCipher() || this.showInlineMenuAccountCreation) {
|
if (this.isFilledByLoginCipher() || this.showInlineMenuAccountCreation || showLogin) {
|
||||||
return this.getTranslation("newLogin");
|
return this.getTranslation("newLogin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,17 +523,17 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
/**
|
/**
|
||||||
* Gets the aria label for the new item button based on the cipher type the focused field is filled by.
|
* Gets the aria label for the new item button based on the cipher type the focused field is filled by.
|
||||||
*/
|
*/
|
||||||
private getNewItemAriaLabel() {
|
private getNewItemAriaLabel(showLogin: boolean) {
|
||||||
if (this.isFilledByLoginCipher() || this.showInlineMenuAccountCreation) {
|
if (this.isFilledByLoginCipher() || this.showInlineMenuAccountCreation || showLogin) {
|
||||||
return this.getTranslation("addNewLoginItem");
|
return this.getTranslation("addNewLoginItemAria");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFilledByCardCipher()) {
|
if (this.isFilledByCardCipher()) {
|
||||||
return this.getTranslation("addNewCardItem");
|
return this.getTranslation("addNewCardItemAria");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFilledByIdentityCipher()) {
|
if (this.isFilledByIdentityCipher()) {
|
||||||
return this.getTranslation("addNewIdentityItem");
|
return this.getTranslation("addNewIdentityItemAria");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getTranslation("addNewVaultItem");
|
return this.getTranslation("addNewVaultItem");
|
||||||
@ -294,7 +557,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
* Sends a message to the parent window to add a new vault item.
|
* Sends a message to the parent window to add a new vault item.
|
||||||
*/
|
*/
|
||||||
private handeNewItemButtonClick = () => {
|
private handeNewItemButtonClick = () => {
|
||||||
let addNewCipherType = this.filledByCipherType;
|
let addNewCipherType = this.inlineMenuFillType;
|
||||||
|
|
||||||
if (this.showInlineMenuAccountCreation) {
|
if (this.showInlineMenuAccountCreation) {
|
||||||
addNewCipherType = CipherType.Login;
|
addNewCipherType = CipherType.Login;
|
||||||
@ -560,7 +823,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
"aria-label",
|
"aria-label",
|
||||||
`${
|
`${
|
||||||
cipher.login?.passkey
|
cipher.login?.passkey
|
||||||
? this.getTranslation("logInWithPasskey")
|
? this.getTranslation("logInWithPasskeyAriaLabel")
|
||||||
: this.getTranslation("fillCredentialsFor")
|
: this.getTranslation("fillCredentialsFor")
|
||||||
} ${cipher.name}`,
|
} ${cipher.name}`,
|
||||||
);
|
);
|
||||||
@ -589,7 +852,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
if (username) {
|
if (username) {
|
||||||
fillCipherElement.setAttribute(
|
fillCipherElement.setAttribute(
|
||||||
"aria-description",
|
"aria-description",
|
||||||
`${this.getTranslation("username")}: ${username}`,
|
`${this.getTranslation("username")?.toLowerCase()}: ${username}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -980,13 +1243,38 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
* If not focused, will check if the button element is focused.
|
* If not focused, will check if the button element is focused.
|
||||||
*/
|
*/
|
||||||
private checkInlineMenuListFocused() {
|
private checkInlineMenuListFocused() {
|
||||||
if (globalThis.document.hasFocus() || this.inlineMenuListContainer.matches(":hover")) {
|
if (globalThis.document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isListHovered()) {
|
||||||
|
globalThis.document.addEventListener(EVENTS.MOUSEOUT, this.handleMouseOutEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postMessageToParent({ command: "checkAutofillInlineMenuButtonFocused" });
|
this.postMessageToParent({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a re-check of the list's focus status when the mouse leaves the list.
|
||||||
|
*/
|
||||||
|
private handleMouseOutEvent = () => {
|
||||||
|
globalThis.document.removeEventListener(EVENTS.MOUSEOUT, this.handleMouseOutEvent);
|
||||||
|
this.checkInlineMenuListFocused();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether the inline menu list iframe is currently hovered.
|
||||||
|
*/
|
||||||
|
private isListHovered = () => {
|
||||||
|
const hoveredElement = this.inlineMenuListContainer?.querySelector(":hover");
|
||||||
|
return !!(
|
||||||
|
hoveredElement &&
|
||||||
|
(hoveredElement === this.inlineMenuListContainer ||
|
||||||
|
this.inlineMenuListContainer.contains(hoveredElement))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focuses the inline menu list iframe. The element that receives focus is
|
* Focuses the inline menu list iframe. The element that receives focus is
|
||||||
* determined by the presence of the unlock button, new item button, or
|
* determined by the presence of the unlock button, new item button, or
|
||||||
@ -1157,21 +1445,21 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
* Identifies if the current focused field is filled by a login cipher.
|
* Identifies if the current focused field is filled by a login cipher.
|
||||||
*/
|
*/
|
||||||
private isFilledByLoginCipher = () => {
|
private isFilledByLoginCipher = () => {
|
||||||
return this.filledByCipherType === CipherType.Login;
|
return this.inlineMenuFillType === CipherType.Login;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies if the current focused field is filled by a card cipher.
|
* Identifies if the current focused field is filled by a card cipher.
|
||||||
*/
|
*/
|
||||||
private isFilledByCardCipher = () => {
|
private isFilledByCardCipher = () => {
|
||||||
return this.filledByCipherType === CipherType.Card;
|
return this.inlineMenuFillType === CipherType.Card;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies if the current focused field is filled by an identity cipher.
|
* Identifies if the current focused field is filled by an identity cipher.
|
||||||
*/
|
*/
|
||||||
private isFilledByIdentityCipher = () => {
|
private isFilledByIdentityCipher = () => {
|
||||||
return this.filledByCipherType === CipherType.Identity;
|
return this.inlineMenuFillType === CipherType.Identity;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,10 +5,14 @@
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -24,6 +28,10 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-menu-list-message {
|
.inline-menu-list-message {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@ -34,7 +42,8 @@ body {
|
|||||||
color: themed("textColor");
|
color: themed("textColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
&.no-items {
|
&.no-items,
|
||||||
|
&.save-login {
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,7 +237,7 @@ body {
|
|||||||
padding: 0.7rem 0.3rem 0.7rem 0.7rem;
|
padding: 0.7rem 0.3rem 0.7rem 0.7rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
|
|
||||||
&:focus-within:not(.remove-outline) {
|
&:has(:focus-visible):not(.remove-outline) {
|
||||||
outline-width: 0.2rem;
|
outline-width: 0.2rem;
|
||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
|
|
||||||
@ -428,3 +437,136 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password generator styles
|
||||||
|
.password-generator-container {
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-generator-actions {
|
||||||
|
display: flex;
|
||||||
|
align-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0.8rem 0.4rem 1.1rem 0.65rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include themify($themes) {
|
||||||
|
background: themed("backgroundOffsetColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:focus-visible):not(.remove-outline) {
|
||||||
|
outline-width: 0.2rem;
|
||||||
|
outline-style: solid;
|
||||||
|
|
||||||
|
@include themify($themes) {
|
||||||
|
outline-color: themed("focusOutlineColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-menu-list-action {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("textColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
path {
|
||||||
|
@include themify($themes) {
|
||||||
|
fill: themed("primaryColor") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-generated-password-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: calc(100% - 4rem);
|
||||||
|
outline: none;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 3.2rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
margin-right: 0.65rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-generated-password-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 {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
|
||||||
|
path {
|
||||||
|
@include themify($themes) {
|
||||||
|
fill: themed("primaryColor") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-generator-content {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-generator-heading {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
font-family: $font-family-sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorized-password {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-family: $font-family-source-code-pro;
|
||||||
|
letter-spacing: 0.05rem;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.password-special {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("passwordSpecialColor") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-number {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("passwordNumberColor") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
require("./menu-container.scss");
|
||||||
|
|
||||||
import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container";
|
import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container";
|
||||||
|
|
||||||
(() => new AutofillInlineMenuContainer())();
|
(() => new AutofillInlineMenuContainer())();
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
|
|
||||||
import AutofillInit from "../../../content/autofill-init";
|
import AutofillInit from "../../../content/autofill-init";
|
||||||
import { DomQueryService } from "../../../services/abstractions/dom-query.service";
|
import { DomQueryService } from "../../../services/abstractions/dom-query.service";
|
||||||
|
import DomElementVisibilityService from "../../../services/dom-element-visibility.service";
|
||||||
import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils";
|
import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils";
|
||||||
import { NotificationTypeData } from "../abstractions/overlay-notifications-content.service";
|
import { NotificationTypeData } from "../abstractions/overlay-notifications-content.service";
|
||||||
|
|
||||||
@ -10,15 +11,18 @@ import { OverlayNotificationsContentService } from "./overlay-notifications-cont
|
|||||||
describe("OverlayNotificationsContentService", () => {
|
describe("OverlayNotificationsContentService", () => {
|
||||||
let overlayNotificationsContentService: OverlayNotificationsContentService;
|
let overlayNotificationsContentService: OverlayNotificationsContentService;
|
||||||
let domQueryService: MockProxy<DomQueryService>;
|
let domQueryService: MockProxy<DomQueryService>;
|
||||||
|
let domElementVisibilityService: DomElementVisibilityService;
|
||||||
let autofillInit: AutofillInit;
|
let autofillInit: AutofillInit;
|
||||||
let bodyAppendChildSpy: jest.SpyInstance;
|
let bodyAppendChildSpy: jest.SpyInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
domQueryService = mock<DomQueryService>();
|
domQueryService = mock<DomQueryService>();
|
||||||
|
domElementVisibilityService = new DomElementVisibilityService();
|
||||||
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
||||||
autofillInit = new AutofillInit(
|
autofillInit = new AutofillInit(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
overlayNotificationsContentService,
|
overlayNotificationsContentService,
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
||||||
|
|
||||||
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
|
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
|
||||||
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
|
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
|
||||||
import AutofillField from "../../models/autofill-field";
|
import AutofillField from "../../models/autofill-field";
|
||||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||||
import { ElementWithOpId, FormFieldElement } from "../../types";
|
import { ElementWithOpId, FormFieldElement } from "../../types";
|
||||||
|
|
||||||
export type OpenAutofillInlineMenuOptions = {
|
|
||||||
isFocusingFieldElement?: boolean;
|
|
||||||
isOpeningFullInlineMenu?: boolean;
|
|
||||||
authStatus?: AuthenticationStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
|
export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
|
||||||
subFrameDepth: number;
|
subFrameDepth: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotificationFormFieldData = {
|
export type InlineMenuFormFieldData = {
|
||||||
uri: string;
|
uri: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
@ -25,21 +17,22 @@ export type NotificationFormFieldData = {
|
|||||||
|
|
||||||
export type AutofillOverlayContentExtensionMessageHandlers = {
|
export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||||
[key: string]: CallableFunction;
|
[key: string]: CallableFunction;
|
||||||
openAutofillInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
|
|
||||||
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
blurMostRecentlyFocusedField: () => void;
|
focusMostRecentlyFocusedField: () => void;
|
||||||
|
blurMostRecentlyFocusedField: () => Promise<void>;
|
||||||
unsetMostRecentlyFocusedField: () => void;
|
unsetMostRecentlyFocusedField: () => void;
|
||||||
checkIsMostRecentlyFocusedFieldWithinViewport: () => Promise<boolean>;
|
checkIsMostRecentlyFocusedFieldWithinViewport: () => Promise<boolean>;
|
||||||
bgUnlockPopoutOpened: () => void;
|
bgUnlockPopoutOpened: () => Promise<void>;
|
||||||
bgVaultItemRepromptPopoutOpened: () => void;
|
bgVaultItemRepromptPopoutOpened: () => Promise<void>;
|
||||||
redirectAutofillInlineMenuFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
|
redirectAutofillInlineMenuFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
updateAutofillInlineMenuVisibility: ({ message }: AutofillExtensionMessageParam) => void;
|
|
||||||
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
|
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
|
||||||
getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void;
|
getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
checkMostRecentlyFocusedFieldHasValue: () => boolean;
|
checkMostRecentlyFocusedFieldHasValue: () => boolean;
|
||||||
setupRebuildSubFrameOffsetsListeners: () => void;
|
setupRebuildSubFrameOffsetsListeners: () => void;
|
||||||
destroyAutofillInlineMenuListeners: () => void;
|
destroyAutofillInlineMenuListeners: () => void;
|
||||||
getFormFieldDataForNotification: () => Promise<NotificationFormFieldData>;
|
getInlineMenuFormFieldData: ({
|
||||||
|
message,
|
||||||
|
}: AutofillExtensionMessageParam) => Promise<InlineMenuFormFieldData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AutofillOverlayContentService {
|
export interface AutofillOverlayContentService {
|
||||||
@ -52,5 +45,6 @@ export interface AutofillOverlayContentService {
|
|||||||
pageDetails: AutofillPageDetails,
|
pageDetails: AutofillPageDetails,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
blurMostRecentlyFocusedField(isClosingInlineMenu?: boolean): void;
|
blurMostRecentlyFocusedField(isClosingInlineMenu?: boolean): void;
|
||||||
|
clearUserFilledFields(): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface DomElementVisibilityService {
|
export interface DomElementVisibilityService {
|
||||||
isFormFieldViewable: (element: HTMLElement) => Promise<boolean>;
|
isElementViewable: (element: HTMLElement) => Promise<boolean>;
|
||||||
isElementHiddenByCss: (element: HTMLElement) => boolean;
|
isElementHiddenByCss: (element: HTMLElement) => boolean;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ export type SubmitButtonKeywordsMap = WeakMap<HTMLElement, string>;
|
|||||||
export interface InlineMenuFieldQualificationService {
|
export interface InlineMenuFieldQualificationService {
|
||||||
isUsernameField(field: AutofillField): boolean;
|
isUsernameField(field: AutofillField): boolean;
|
||||||
isCurrentPasswordField(field: AutofillField): boolean;
|
isCurrentPasswordField(field: AutofillField): boolean;
|
||||||
|
isUpdateCurrentPasswordField(field: AutofillField): boolean;
|
||||||
isNewPasswordField(field: AutofillField): boolean;
|
isNewPasswordField(field: AutofillField): boolean;
|
||||||
isEmailField(field: AutofillField): boolean;
|
isEmailField(field: AutofillField): boolean;
|
||||||
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
||||||
|
@ -29,8 +29,6 @@ export class AutoFillConstants {
|
|||||||
|
|
||||||
static readonly TotpFieldNames: string[] = [
|
static readonly TotpFieldNames: string[] = [
|
||||||
"totp",
|
"totp",
|
||||||
"2fa",
|
|
||||||
"mfa",
|
|
||||||
"totpcode",
|
"totpcode",
|
||||||
"2facode",
|
"2facode",
|
||||||
"approvals_code",
|
"approvals_code",
|
||||||
@ -44,11 +42,11 @@ export class AutoFillConstants {
|
|||||||
"twofactor",
|
"twofactor",
|
||||||
"twofa",
|
"twofa",
|
||||||
"twofactorcode",
|
"twofactorcode",
|
||||||
"verificationCode",
|
"verificationcode",
|
||||||
"verification code",
|
"verification code",
|
||||||
];
|
];
|
||||||
|
|
||||||
static readonly AmbiguousTotpFieldNames: string[] = ["code", "pin", "otc", "otp"];
|
static readonly AmbiguousTotpFieldNames: string[] = ["code", "pin", "otc", "otp", "2fa", "mfa"];
|
||||||
|
|
||||||
static readonly SearchFieldNames: string[] = ["search", "query", "find", "go"];
|
static readonly SearchFieldNames: string[] = ["search", "query", "find", "go"];
|
||||||
|
|
||||||
@ -373,6 +371,7 @@ export class IdentityAutoFillConstants {
|
|||||||
"label-left",
|
"label-left",
|
||||||
"label-top",
|
"label-top",
|
||||||
"data-recurly",
|
"data-recurly",
|
||||||
|
"accountCreationFieldType",
|
||||||
];
|
];
|
||||||
|
|
||||||
static readonly FullNameFieldNames: string[] = ["name", "full-name", "your-name"];
|
static readonly FullNameFieldNames: string[] = ["name", "full-name", "your-name"];
|
||||||
@ -875,7 +874,7 @@ export const SubmitLoginButtonNames: string[] = [
|
|||||||
"submit",
|
"submit",
|
||||||
"continue",
|
"continue",
|
||||||
"next",
|
"next",
|
||||||
"go",
|
"verify",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SubmitChangePasswordButtonNames: string[] = [
|
export const SubmitChangePasswordButtonNames: string[] = [
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillOverlayVisibility, EVENTS } from "@bitwarden/common/autofill/constants";
|
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import AutofillInit from "../content/autofill-init";
|
import AutofillInit from "../content/autofill-init";
|
||||||
import {
|
import {
|
||||||
AutofillOverlayElement,
|
AutofillOverlayElement,
|
||||||
|
InlineMenuFillType,
|
||||||
MAX_SUB_FRAME_DEPTH,
|
MAX_SUB_FRAME_DEPTH,
|
||||||
RedirectFocusDirection,
|
RedirectFocusDirection,
|
||||||
} from "../enums/autofill-overlay.enum";
|
} from "../enums/autofill-overlay.enum";
|
||||||
@ -24,6 +24,7 @@ import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../
|
|||||||
|
|
||||||
import { AutoFillConstants } from "./autofill-constants";
|
import { AutoFillConstants } from "./autofill-constants";
|
||||||
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
|
||||||
|
import DomElementVisibilityService from "./dom-element-visibility.service";
|
||||||
import { DomQueryService } from "./dom-query.service";
|
import { DomQueryService } from "./dom-query.service";
|
||||||
import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service";
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ const defaultWindowReadyState = document.readyState;
|
|||||||
const defaultDocumentVisibilityState = document.visibilityState;
|
const defaultDocumentVisibilityState = document.visibilityState;
|
||||||
describe("AutofillOverlayContentService", () => {
|
describe("AutofillOverlayContentService", () => {
|
||||||
let domQueryService: DomQueryService;
|
let domQueryService: DomQueryService;
|
||||||
|
let domElementVisibilityService: DomElementVisibilityService;
|
||||||
let autofillInit: AutofillInit;
|
let autofillInit: AutofillInit;
|
||||||
let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
||||||
let autofillOverlayContentService: AutofillOverlayContentService;
|
let autofillOverlayContentService: AutofillOverlayContentService;
|
||||||
@ -38,15 +40,23 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
const sendResponseSpy = jest.fn();
|
const sendResponseSpy = jest.fn();
|
||||||
const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
|
const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
domQueryService = new DomQueryService();
|
domQueryService = new DomQueryService();
|
||||||
|
domElementVisibilityService = new DomElementVisibilityService();
|
||||||
autofillOverlayContentService = new AutofillOverlayContentService(
|
autofillOverlayContentService = new AutofillOverlayContentService(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
inlineMenuFieldQualificationService,
|
inlineMenuFieldQualificationService,
|
||||||
);
|
);
|
||||||
autofillInit = new AutofillInit(domQueryService, autofillOverlayContentService);
|
autofillInit = new AutofillInit(
|
||||||
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
|
autofillOverlayContentService,
|
||||||
|
);
|
||||||
autofillInit.init();
|
autofillInit.init();
|
||||||
|
autofillOverlayContentService["showInlineMenuCards"] = true;
|
||||||
|
autofillOverlayContentService["showInlineMenuIdentities"] = true;
|
||||||
sendExtensionMessageSpy = jest
|
sendExtensionMessageSpy = jest
|
||||||
.spyOn(autofillOverlayContentService as any, "sendExtensionMessage")
|
.spyOn(autofillOverlayContentService as any, "sendExtensionMessage")
|
||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
@ -122,14 +132,17 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sets up a focus out listener for the window", () => {
|
it("sets up a focus out listener for the window", () => {
|
||||||
const handleFormFieldBlurEventSpy = jest.spyOn(
|
const handleWindowFocusOutEventSpy = jest.spyOn(
|
||||||
autofillOverlayContentService as any,
|
autofillOverlayContentService as any,
|
||||||
"handleFormFieldBlurEvent",
|
"handleWindowFocusOutEvent",
|
||||||
);
|
);
|
||||||
|
|
||||||
autofillOverlayContentService.init();
|
autofillOverlayContentService.init();
|
||||||
|
|
||||||
expect(window.addEventListener).toHaveBeenCalledWith("focusout", handleFormFieldBlurEventSpy);
|
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||||
|
"focusout",
|
||||||
|
handleWindowFocusOutEventSpy,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -225,39 +238,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
|
expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("identifies the overlay visibility setting", () => {
|
|
||||||
it("defaults the overlay visibility setting to `OnFieldFocus` if a value is not set", async () => {
|
|
||||||
sendExtensionMessageSpy.mockResolvedValueOnce(undefined);
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] = undefined;
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("getAutofillInlineMenuVisibility");
|
|
||||||
expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual(
|
|
||||||
AutofillOverlayVisibility.OnFieldFocus,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the overlay visibility setting to the value returned from the background script", async () => {
|
|
||||||
sendExtensionMessageSpy.mockResolvedValueOnce(AutofillOverlayVisibility.OnFieldFocus);
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] = undefined;
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual(
|
|
||||||
AutofillOverlayVisibility.OnFieldFocus,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sets up form field element listeners", () => {
|
describe("sets up form field element listeners", () => {
|
||||||
it("removes all cached event listeners from the form field element", async () => {
|
it("removes all cached event listeners from the form field element", async () => {
|
||||||
jest.spyOn(autofillFieldElement, "removeEventListener");
|
jest.spyOn(autofillFieldElement, "removeEventListener");
|
||||||
@ -377,14 +357,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
|
|
||||||
it("opens the overlay list and focuses it after a delay if it is not visible when the `ArrowDown` key is pressed", async () => {
|
it("opens the overlay list and focuses it after a delay if it is not visible when the `ArrowDown` key is pressed", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
const updateMostRecentlyFocusedFieldSpy = jest.spyOn(
|
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
||||||
autofillOverlayContentService as any,
|
|
||||||
"updateMostRecentlyFocusedField",
|
|
||||||
);
|
|
||||||
const openAutofillOverlaySpy = jest.spyOn(
|
|
||||||
autofillOverlayContentService as any,
|
|
||||||
"openInlineMenu",
|
|
||||||
);
|
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
||||||
.mockResolvedValue(false);
|
.mockResolvedValue(false);
|
||||||
@ -392,8 +365,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
|
autofillFieldElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(updateMostRecentlyFocusedFieldSpy).toHaveBeenCalledWith(autofillFieldElement);
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu", {
|
||||||
expect(openAutofillOverlaySpy).toHaveBeenCalledWith({
|
|
||||||
isOpeningFullInlineMenu: true,
|
isOpeningFullInlineMenu: true,
|
||||||
});
|
});
|
||||||
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("focusAutofillInlineMenuList");
|
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("focusAutofillInlineMenuList");
|
||||||
@ -441,13 +413,11 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
const randomElement = document.createElement(
|
const randomElement = document.createElement(
|
||||||
"input",
|
"input",
|
||||||
) as ElementWithOpId<FillableFormFieldElement>;
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
jest.spyOn(autofillOverlayContentService as any, "qualifyUserFilledLoginField");
|
jest.spyOn(autofillOverlayContentService as any, "qualifyUserFilledField");
|
||||||
|
|
||||||
autofillOverlayContentService["storeModifiedFormElement"](randomElement);
|
autofillOverlayContentService["storeModifiedFormElement"](randomElement);
|
||||||
|
|
||||||
expect(
|
expect(autofillOverlayContentService["qualifyUserFilledField"]).not.toHaveBeenCalled();
|
||||||
autofillOverlayContentService["qualifyUserFilledLoginField"],
|
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets the field as the most recently focused form field element", async () => {
|
it("sets the field as the most recently focused form field element", async () => {
|
||||||
@ -499,8 +469,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes the overlay if the form field element has a value and the user is not authed", async () => {
|
it("Closes the inline menu list and does not re-open the inline menu if the field has a value", async () => {
|
||||||
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(false);
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "test";
|
(autofillFieldElement as HTMLInputElement).value = "test";
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
@ -515,16 +484,10 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
overlayElement: AutofillOverlayElement.List,
|
overlayElement: AutofillOverlayElement.List,
|
||||||
forceCloseInlineMenu: true,
|
forceCloseInlineMenu: true,
|
||||||
});
|
});
|
||||||
|
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("openAutofillInlineMenu");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes the overlay if the form field element has a value and the overlay ciphers are populated", async () => {
|
it("opens the inline menu if the field does not have a value", async () => {
|
||||||
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
|
|
||||||
jest
|
|
||||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated")
|
|
||||||
.mockResolvedValue(true);
|
|
||||||
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "test";
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
@ -533,60 +496,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillFieldElement.dispatchEvent(new Event("input"));
|
autofillFieldElement.dispatchEvent(new Event("input"));
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu");
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
forceCloseInlineMenu: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens the autofill inline menu if the form field is empty", async () => {
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "openInlineMenu");
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "";
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("input"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["openInlineMenu"]).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens the autofill inline menu if the form field is empty and the user is authed", async () => {
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "openInlineMenu");
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "";
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("input"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["openInlineMenu"]).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens the autofill inline menu if the form field is empty and the overlay ciphers are not populated", async () => {
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(false);
|
|
||||||
jest
|
|
||||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated")
|
|
||||||
.mockResolvedValue(false);
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "openInlineMenu");
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "";
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("input"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["openInlineMenu"]).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("input changes on a field filled by a card cipher", () => {
|
describe("input changes on a field filled by a card cipher", () => {
|
||||||
@ -605,7 +515,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
elementNumber: 3,
|
elementNumber: 3,
|
||||||
autoCompleteType: "cc-number",
|
autoCompleteType: "cc-number",
|
||||||
type: "text",
|
type: "text",
|
||||||
filledByCipherType: CipherType.Card,
|
inlineMenuFillType: CipherType.Card,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
});
|
});
|
||||||
selectFieldElement = document.createElement(
|
selectFieldElement = document.createElement(
|
||||||
@ -617,7 +527,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
elementNumber: 4,
|
elementNumber: 4,
|
||||||
autoCompleteType: "cc-type",
|
autoCompleteType: "cc-type",
|
||||||
type: "select",
|
type: "select",
|
||||||
filledByCipherType: CipherType.Card,
|
inlineMenuFillType: CipherType.Card,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
});
|
});
|
||||||
pageDetailsMock.fields = [inputFieldData, selectFieldData];
|
pageDetailsMock.fields = [inputFieldData, selectFieldData];
|
||||||
@ -625,7 +535,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
|
|
||||||
it("only stores the element if the form field is a select element", async () => {
|
it("only stores the element if the form field is a select element", async () => {
|
||||||
jest.spyOn(autofillOverlayContentService as any, "storeModifiedFormElement");
|
jest.spyOn(autofillOverlayContentService as any, "storeModifiedFormElement");
|
||||||
jest.spyOn(autofillOverlayContentService as any, "hideInlineMenuListOnFilledField");
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
selectFieldElement,
|
selectFieldElement,
|
||||||
@ -638,9 +547,10 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(autofillOverlayContentService["storeModifiedFormElement"]).toHaveBeenCalledWith(
|
expect(autofillOverlayContentService["storeModifiedFormElement"]).toHaveBeenCalledWith(
|
||||||
selectFieldElement,
|
selectFieldElement,
|
||||||
);
|
);
|
||||||
expect(
|
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith(
|
||||||
autofillOverlayContentService["hideInlineMenuListOnFilledField"],
|
"openAutofillInlineMenu",
|
||||||
).not.toHaveBeenCalled();
|
expect.any(Object),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores cardholder name fields", async () => {
|
it("stores cardholder name fields", async () => {
|
||||||
@ -752,7 +662,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
elementNumber: 3,
|
elementNumber: 3,
|
||||||
autoCompleteType: "given-name",
|
autoCompleteType: "given-name",
|
||||||
type: "text",
|
type: "text",
|
||||||
filledByCipherType: CipherType.Identity,
|
inlineMenuFillType: CipherType.Identity,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
});
|
});
|
||||||
pageDetailsMock.fields = [inputFieldData];
|
pageDetailsMock.fields = [inputFieldData];
|
||||||
@ -1026,6 +936,70 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("input changes on a field for an account creation form", () => {
|
||||||
|
const inputFieldData = createAutofillFieldMock({
|
||||||
|
form: "validFormId",
|
||||||
|
autoCompleteType: "username",
|
||||||
|
type: "text",
|
||||||
|
});
|
||||||
|
const passwordFieldData = createAutofillFieldMock({
|
||||||
|
type: "password",
|
||||||
|
autoCompleteType: "new-password",
|
||||||
|
form: "validFormId",
|
||||||
|
placeholder: "new password",
|
||||||
|
});
|
||||||
|
const confirmPasswordFieldData = createAutofillFieldMock({
|
||||||
|
type: "password",
|
||||||
|
autoCompleteType: "new-password",
|
||||||
|
form: "validFormId",
|
||||||
|
placeholder: "confirm password",
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest
|
||||||
|
.spyOn(inlineMenuFieldQualificationService, "isFieldForLoginForm")
|
||||||
|
.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores fields account username fields", async () => {
|
||||||
|
const inputFieldElement = document.createElement(
|
||||||
|
"input",
|
||||||
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
|
||||||
|
pageDetailsMock.fields = [inputFieldData, passwordFieldData, confirmPasswordFieldData];
|
||||||
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].username).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores new password fields", async () => {
|
||||||
|
const inputFieldElement = document.createElement(
|
||||||
|
"input",
|
||||||
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
|
||||||
|
pageDetailsMock.fields = [inputFieldData, passwordFieldData, confirmPasswordFieldData];
|
||||||
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
|
inputFieldElement,
|
||||||
|
passwordFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].newPassword).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("form field click event listener", () => {
|
describe("form field click event listener", () => {
|
||||||
@ -1088,8 +1062,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
it("skips triggering the handler logic if autofill is currently filling", async () => {
|
it("skips triggering the handler logic if autofill is currently filling", async () => {
|
||||||
isFieldCurrentlyFillingSpy.mockResolvedValue(true);
|
isFieldCurrentlyFillingSpy.mockResolvedValue(true);
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnFieldFocus;
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
@ -1102,6 +1074,22 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(updateMostRecentlyFocusedFieldSpy).not.toHaveBeenCalled();
|
expect(updateMostRecentlyFocusedFieldSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("triggers a re-collection of page details when the field is focused if a dom change has occurred", async () => {
|
||||||
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
|
autofillFieldElement,
|
||||||
|
autofillFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
||||||
|
|
||||||
|
autofillFieldElement.dispatchEvent(new Event("focus"));
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", {
|
||||||
|
sender: "autofillOverlayContentService",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("closes the inline menu if the focused element is a select element", async () => {
|
it("closes the inline menu if the focused element is a select element", async () => {
|
||||||
const selectFieldElement = document.createElement(
|
const selectFieldElement = document.createElement(
|
||||||
"select",
|
"select",
|
||||||
@ -1138,48 +1126,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes the overlay list if the autofill visibility is set to onClick", async () => {
|
it("opens the autofill inline menu ", async () => {
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnButtonClick;
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("focus"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
forceCloseInlineMenu: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes the overlay list if the form element has a value and the focused field is newly focused", async () => {
|
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = document.createElement(
|
|
||||||
"input",
|
|
||||||
) as ElementWithOpId<HTMLInputElement>;
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "test";
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("focus"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
forceCloseInlineMenu: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens the autofill inline menu if the form element has no value", async () => {
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "";
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnFieldFocus;
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
@ -1191,44 +1138,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu");
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens the autofill inline menu if the overlay ciphers are not populated and the user is authed", async () => {
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "";
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnFieldFocus;
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("focus"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates the overlay button position if the focus event is not opening the overlay", async () => {
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnFieldFocus;
|
|
||||||
(autofillFieldElement as HTMLInputElement).value = "test";
|
|
||||||
jest
|
|
||||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated")
|
|
||||||
.mockReturnValue(true);
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("focus"));
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.Button,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hidden form field focus event", () => {
|
describe("hidden form field focus event", () => {
|
||||||
@ -1273,7 +1182,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
pageDetailsMock,
|
pageDetailsMock,
|
||||||
);
|
);
|
||||||
autofillOverlayContentService["formFieldElements"].delete(autofillFieldElement);
|
autofillOverlayContentService["hiddenFormFieldElements"].delete(autofillFieldElement);
|
||||||
|
|
||||||
autofillFieldElement.dispatchEvent(new Event("focus"));
|
autofillFieldElement.dispatchEvent(new Event("focus"));
|
||||||
|
|
||||||
@ -1415,10 +1324,10 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
elementNumber: 3,
|
elementNumber: 3,
|
||||||
autoCompleteType: "username",
|
autoCompleteType: "username",
|
||||||
placeholder: "new username",
|
placeholder: "new username",
|
||||||
type: "text",
|
type: "email",
|
||||||
viewable: true,
|
viewable: true,
|
||||||
});
|
});
|
||||||
const passwordAccountFieldData = createAutofillFieldMock({
|
const newPasswordFieldData = createAutofillFieldMock({
|
||||||
opid: "create-account-password-field",
|
opid: "create-account-password-field",
|
||||||
form: "validFormId",
|
form: "validFormId",
|
||||||
elementNumber: 4,
|
elementNumber: 4,
|
||||||
@ -1429,13 +1338,13 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
pageDetailsMock.fields = [inputAccountFieldData, passwordAccountFieldData];
|
pageDetailsMock.fields = [inputAccountFieldData, newPasswordFieldData];
|
||||||
jest
|
jest
|
||||||
.spyOn(inlineMenuFieldQualificationService, "isFieldForLoginForm")
|
.spyOn(inlineMenuFieldQualificationService, "isFieldForLoginForm")
|
||||||
.mockReturnValue(false);
|
.mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets up the field listeners on the field", async () => {
|
it("sets up the field listeners on a username account creation field", async () => {
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
inputAccountFieldData,
|
inputAccountFieldData,
|
||||||
@ -1464,8 +1373,46 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
||||||
expect(inputAccountFieldData.filledByCipherType).toEqual(CipherType.Identity);
|
expect(inputAccountFieldData.inlineMenuFillType).toEqual(
|
||||||
expect(inputAccountFieldData.showInlineMenuAccountCreation).toEqual(true);
|
InlineMenuFillType.AccountCreationUsername,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up field a current password field within an update password form", async () => {
|
||||||
|
const currentPasswordFieldData = createAutofillFieldMock({
|
||||||
|
opid: "current-password-field",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 5,
|
||||||
|
autoCompleteType: "current-password",
|
||||||
|
placeholder: "current password",
|
||||||
|
type: "password",
|
||||||
|
viewable: true,
|
||||||
|
});
|
||||||
|
const confirmNewPasswordFieldData = createAutofillFieldMock({
|
||||||
|
opid: "confirm-new-password-field",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 6,
|
||||||
|
autoCompleteType: "new-password",
|
||||||
|
placeholder: "confirm new password",
|
||||||
|
type: "password",
|
||||||
|
viewable: true,
|
||||||
|
});
|
||||||
|
pageDetailsMock.fields = [
|
||||||
|
currentPasswordFieldData,
|
||||||
|
newPasswordFieldData,
|
||||||
|
confirmNewPasswordFieldData,
|
||||||
|
];
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
|
autofillFieldElement,
|
||||||
|
currentPasswordFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(currentPasswordFieldData.inlineMenuFillType).toEqual(
|
||||||
|
InlineMenuFillType.CurrentPasswordUpdate,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1519,11 +1466,13 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends a `formFieldSubmitted` message to the background on interaction of a generic input element", async () => {
|
it("sends a `formFieldSubmitted` message to the background on interaction of a generic input element", async () => {
|
||||||
|
domElementVisibilityService.isElementViewable = jest.fn().mockReturnValue(true);
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
pageDetailsMock,
|
pageDetailsMock,
|
||||||
);
|
);
|
||||||
|
await flushPromises();
|
||||||
genericSubmitElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
genericSubmitElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||||
@ -1532,6 +1481,59 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("triggering submission trough interaction of a button element", () => {
|
||||||
|
let buttonElement: HTMLButtonElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
buttonElement = document.createElement("button");
|
||||||
|
buttonElement.textContent = "Login In";
|
||||||
|
buttonElement.type = "button";
|
||||||
|
form.appendChild(buttonElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends a `formFieldSubmitted` message to the background on interaction of a button element", async () => {
|
||||||
|
domElementVisibilityService.isElementViewable = jest.fn().mockReturnValue(true);
|
||||||
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
|
autofillFieldElement,
|
||||||
|
autofillFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
buttonElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||||
|
"formFieldSubmitted",
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("triggering submission through interaction of an anchor element", () => {
|
||||||
|
let anchorElement: HTMLAnchorElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
anchorElement = document.createElement("a");
|
||||||
|
anchorElement.textContent = "Login In";
|
||||||
|
form.appendChild(anchorElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends a `formFieldSubmitted` message to the background on interaction of an anchor element", async () => {
|
||||||
|
domElementVisibilityService.isElementViewable = jest.fn().mockReturnValue(true);
|
||||||
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
|
autofillFieldElement,
|
||||||
|
autofillFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
anchorElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||||
|
"formFieldSubmitted",
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("listeners set up on a fields without a form", () => {
|
describe("listeners set up on a fields without a form", () => {
|
||||||
@ -1596,12 +1598,14 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("triggers submission through interaction of a submit button", async () => {
|
it("triggers submission through interaction of a submit button", async () => {
|
||||||
|
domElementVisibilityService.isElementViewable = jest.fn().mockReturnValue(true);
|
||||||
const submitButton = document.querySelector("button");
|
const submitButton = document.querySelector("button");
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
await autofillOverlayContentService.setupOverlayListeners(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
pageDetailsMock,
|
pageDetailsMock,
|
||||||
);
|
);
|
||||||
|
await flushPromises();
|
||||||
submitButton.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
submitButton.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||||
@ -1611,6 +1615,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("captures submit buttons when the field is structured within a shadow DOM", async () => {
|
it("captures submit buttons when the field is structured within a shadow DOM", async () => {
|
||||||
|
domElementVisibilityService.isElementViewable = jest.fn().mockReturnValue(true);
|
||||||
document.body.innerHTML = `<div id="form-div">
|
document.body.innerHTML = `<div id="form-div">
|
||||||
<div id="shadow-root"></div>
|
<div id="shadow-root"></div>
|
||||||
<button id="button-el">Change Password</button>
|
<button id="button-el">Change Password</button>
|
||||||
@ -1641,6 +1646,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
pageDetailsMock,
|
pageDetailsMock,
|
||||||
);
|
);
|
||||||
|
await flushPromises();
|
||||||
buttonElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
buttonElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||||
@ -1687,34 +1693,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets the most recently focused field to the passed form field element if the value is not set", async () => {
|
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
|
|
||||||
|
|
||||||
await autofillOverlayContentService.setupOverlayListeners(
|
|
||||||
autofillFieldElement,
|
|
||||||
autofillFieldData,
|
|
||||||
pageDetailsMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["mostRecentlyFocusedField"]).toEqual(
|
|
||||||
autofillFieldElement,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("focusMostRecentlyFocusedField", () => {
|
|
||||||
it("focuses the most recently focused overlay field", () => {
|
|
||||||
const mostRecentlyFocusedField = document.createElement(
|
|
||||||
"input",
|
|
||||||
) as ElementWithOpId<HTMLInputElement>;
|
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = mostRecentlyFocusedField;
|
|
||||||
jest.spyOn(mostRecentlyFocusedField, "focus");
|
|
||||||
|
|
||||||
autofillOverlayContentService["focusMostRecentlyFocusedField"]();
|
|
||||||
|
|
||||||
expect(mostRecentlyFocusedField.focus).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("handleOverlayRepositionEvent", () => {
|
describe("handleOverlayRepositionEvent", () => {
|
||||||
@ -1758,144 +1736,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("extension onMessage handlers", () => {
|
describe("extension onMessage handlers", () => {
|
||||||
describe("openAutofillInlineMenu message handler", () => {
|
|
||||||
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
document.body.innerHTML = `
|
|
||||||
<form id="validFormId">
|
|
||||||
<input type="text" id="username-field" placeholder="username" />
|
|
||||||
<input type="password" id="password-field" placeholder="password" />
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
autofillFieldElement = document.getElementById(
|
|
||||||
"username-field",
|
|
||||||
) as ElementWithOpId<FormFieldElement>;
|
|
||||||
autofillFieldElement.opid = "op-1";
|
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips opening the overlay if a field has not been recently focused", () => {
|
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
|
|
||||||
|
|
||||||
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("focuses the most recent overlay field if the field is not focused", () => {
|
|
||||||
jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
|
|
||||||
Object.defineProperty(document, "activeElement", {
|
|
||||||
value: document.createElement("div"),
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
const focusMostRecentOverlayFieldSpy = jest.spyOn(
|
|
||||||
autofillOverlayContentService as any,
|
|
||||||
"focusMostRecentlyFocusedField",
|
|
||||||
);
|
|
||||||
|
|
||||||
sendMockExtensionMessage({
|
|
||||||
command: "openAutofillInlineMenu",
|
|
||||||
isFocusingFieldElement: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(focusMostRecentOverlayFieldSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips focusing the most recent overlay field if the field is already focused", () => {
|
|
||||||
jest.spyOn(autofillFieldElement, "getRootNode").mockReturnValue(document);
|
|
||||||
Object.defineProperty(document, "activeElement", {
|
|
||||||
value: autofillFieldElement,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
const focusMostRecentOverlayFieldSpy = jest.spyOn(
|
|
||||||
autofillOverlayContentService as any,
|
|
||||||
"focusMostRecentlyFocusedField",
|
|
||||||
);
|
|
||||||
|
|
||||||
sendMockExtensionMessage({
|
|
||||||
command: "openAutofillInlineMenu",
|
|
||||||
isFocusingFieldElement: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(focusMostRecentOverlayFieldSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stores the user's auth status", () => {
|
|
||||||
autofillOverlayContentService["authStatus"] = undefined;
|
|
||||||
|
|
||||||
sendMockExtensionMessage({
|
|
||||||
command: "openAutofillInlineMenu",
|
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["authStatus"]).toEqual(AuthenticationStatus.Unlocked);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens both autofill inline menu elements", () => {
|
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
|
||||||
|
|
||||||
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.Button,
|
|
||||||
});
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens the autofill inline menu button only if overlay visibility is set for onButtonClick", () => {
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnButtonClick;
|
|
||||||
|
|
||||||
sendMockExtensionMessage({
|
|
||||||
command: "openAutofillInlineMenu",
|
|
||||||
isOpeningFullInlineMenu: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.Button,
|
|
||||||
});
|
|
||||||
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith(
|
|
||||||
"updateAutofillInlineMenuPosition",
|
|
||||||
{
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("overrides the onButtonClick visibility setting to open both overlay elements", () => {
|
|
||||||
autofillOverlayContentService["inlineMenuVisibility"] =
|
|
||||||
AutofillOverlayVisibility.OnButtonClick;
|
|
||||||
|
|
||||||
sendMockExtensionMessage({
|
|
||||||
command: "openAutofillInlineMenu",
|
|
||||||
isOpeningFullInlineMenu: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.Button,
|
|
||||||
});
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends an extension message requesting an re-collection of page details if they need to update", () => {
|
|
||||||
jest.spyOn(autofillOverlayContentService as any, "sendExtensionMessage");
|
|
||||||
autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
|
||||||
|
|
||||||
autofillOverlayContentService["openInlineMenu"]();
|
|
||||||
sendMockExtensionMessage({ command: "openAutofillInlineMenu" });
|
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", {
|
|
||||||
sender: "autofillOverlayContentService",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("addNewVaultItemFromOverlay message handler", () => {
|
describe("addNewVaultItemFromOverlay message handler", () => {
|
||||||
it("skips sending the message if the overlay list is not visible", async () => {
|
it("skips sending the message if the overlay list is not visible", async () => {
|
||||||
jest
|
jest
|
||||||
@ -2052,7 +1892,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillOverlayContentService["focusedFieldData"] = {
|
autofillOverlayContentService["focusedFieldData"] = {
|
||||||
focusedFieldStyles: { paddingRight: "10", paddingLeft: "10" },
|
focusedFieldStyles: { paddingRight: "10", paddingLeft: "10" },
|
||||||
focusedFieldRects: { width: 10, height: 10, top: 10, left: 10 },
|
focusedFieldRects: { width: 10, height: 10, top: 10, left: 10 },
|
||||||
filledByCipherType: CipherType.Login,
|
inlineMenuFillType: CipherType.Login,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2070,6 +1910,20 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("focusMostRecentlyFocusedField message handler", () => {
|
||||||
|
it("focuses the most recently focused field", async () => {
|
||||||
|
autofillOverlayContentService["mostRecentlyFocusedField"] =
|
||||||
|
mock<ElementWithOpId<FormFieldElement>>();
|
||||||
|
|
||||||
|
sendMockExtensionMessage({
|
||||||
|
command: "focusMostRecentlyFocusedField",
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["mostRecentlyFocusedField"].focus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("messages that trigger a blur of the most recently focused field", () => {
|
describe("messages that trigger a blur of the most recently focused field", () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
"blurMostRecentlyFocusedField",
|
"blurMostRecentlyFocusedField",
|
||||||
@ -2088,7 +1942,9 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(autofillOverlayContentService["mostRecentlyFocusedField"].blur).toHaveBeenCalled();
|
expect(autofillOverlayContentService["mostRecentlyFocusedField"].blur).toHaveBeenCalled();
|
||||||
|
|
||||||
if (isClosingInlineMenu) {
|
if (isClosingInlineMenu) {
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -2234,19 +2090,6 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("updateAutofillInlineMenuVisibility message handler", () => {
|
|
||||||
it("updates the inlineMenuVisibility property", () => {
|
|
||||||
sendMockExtensionMessage({
|
|
||||||
command: "updateAutofillInlineMenuVisibility",
|
|
||||||
data: { newSettingValue: AutofillOverlayVisibility.OnButtonClick },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual(
|
|
||||||
AutofillOverlayVisibility.OnButtonClick,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getSubFrameOffsets message handler", () => {
|
describe("getSubFrameOffsets message handler", () => {
|
||||||
const iframeSource = "https://example.com/";
|
const iframeSource = "https://example.com/";
|
||||||
const originalLocation = globalThis.location;
|
const originalLocation = globalThis.location;
|
||||||
@ -2578,14 +2421,14 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getFormFieldDataForNotification message handler", () => {
|
describe("getInlineMenuFormFieldData message handler", () => {
|
||||||
it("returns early if a field is currently focused", async () => {
|
it("returns early if a field is currently focused", async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillOverlayContentService as any, "isFieldCurrentlyFocused")
|
.spyOn(autofillOverlayContentService as any, "isFieldCurrentlyFocused")
|
||||||
.mockReturnValue(true);
|
.mockReturnValue(true);
|
||||||
|
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{ command: "getFormFieldDataForNotification" },
|
{ command: "getInlineMenuFormFieldData" },
|
||||||
mock<chrome.runtime.MessageSender>(),
|
mock<chrome.runtime.MessageSender>(),
|
||||||
sendResponseSpy,
|
sendResponseSpy,
|
||||||
);
|
);
|
||||||
@ -2596,7 +2439,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
|
|
||||||
it("returns the form field data for a notification", async () => {
|
it("returns the form field data for a notification", async () => {
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{ command: "getFormFieldDataForNotification" },
|
{ command: "getInlineMenuFormFieldData" },
|
||||||
mock<chrome.runtime.MessageSender>(),
|
mock<chrome.runtime.MessageSender>(),
|
||||||
sendResponseSpy,
|
sendResponseSpy,
|
||||||
);
|
);
|
||||||
|
@ -2,15 +2,12 @@ import "@webcomponents/custom-elements";
|
|||||||
import "lit/polyfill-support.js";
|
import "lit/polyfill-support.js";
|
||||||
import { FocusableElement, tabbable } from "tabbable";
|
import { FocusableElement, tabbable } from "tabbable";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
||||||
import {
|
import {
|
||||||
EVENTS,
|
EVENTS,
|
||||||
AutofillOverlayVisibility,
|
|
||||||
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
|
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
|
||||||
AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT,
|
AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT,
|
||||||
AUTOFILL_OVERLAY_HANDLE_SCROLL,
|
AUTOFILL_OVERLAY_HANDLE_SCROLL,
|
||||||
} from "@bitwarden/common/autofill/constants";
|
} from "@bitwarden/common/autofill/constants";
|
||||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -24,6 +21,8 @@ import { AutofillExtensionMessage } from "../content/abstractions/autofill-init"
|
|||||||
import { AutofillFieldQualifier, AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
import { AutofillFieldQualifier, AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
||||||
import {
|
import {
|
||||||
AutofillOverlayElement,
|
AutofillOverlayElement,
|
||||||
|
InlineMenuAccountCreationFieldType,
|
||||||
|
InlineMenuFillType,
|
||||||
MAX_SUB_FRAME_DEPTH,
|
MAX_SUB_FRAME_DEPTH,
|
||||||
RedirectFocusDirection,
|
RedirectFocusDirection,
|
||||||
} from "../enums/autofill-overlay.enum";
|
} from "../enums/autofill-overlay.enum";
|
||||||
@ -31,9 +30,12 @@ import AutofillField from "../models/autofill-field";
|
|||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
import {
|
import {
|
||||||
|
currentlyInSandboxedIframe,
|
||||||
|
debounce,
|
||||||
elementIsFillableFormField,
|
elementIsFillableFormField,
|
||||||
elementIsSelectElement,
|
elementIsSelectElement,
|
||||||
getAttributeBoolean,
|
getAttributeBoolean,
|
||||||
|
nodeIsAnchorElement,
|
||||||
nodeIsButtonElement,
|
nodeIsButtonElement,
|
||||||
nodeIsTypeSubmitElement,
|
nodeIsTypeSubmitElement,
|
||||||
sendExtensionMessage,
|
sendExtensionMessage,
|
||||||
@ -43,17 +45,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
AutofillOverlayContentExtensionMessageHandlers,
|
AutofillOverlayContentExtensionMessageHandlers,
|
||||||
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
||||||
NotificationFormFieldData,
|
InlineMenuFormFieldData,
|
||||||
OpenAutofillInlineMenuOptions,
|
|
||||||
SubFrameDataFromWindowMessage,
|
SubFrameDataFromWindowMessage,
|
||||||
} from "./abstractions/autofill-overlay-content.service";
|
} from "./abstractions/autofill-overlay-content.service";
|
||||||
|
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
|
||||||
import { DomQueryService } from "./abstractions/dom-query.service";
|
import { DomQueryService } from "./abstractions/dom-query.service";
|
||||||
import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
|
import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
|
||||||
import { AutoFillConstants } from "./autofill-constants";
|
import { AutoFillConstants } from "./autofill-constants";
|
||||||
|
|
||||||
export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface {
|
export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface {
|
||||||
pageDetailsUpdateRequired = false;
|
pageDetailsUpdateRequired = false;
|
||||||
inlineMenuVisibility: InlineMenuVisibilitySetting;
|
|
||||||
private showInlineMenuIdentities: boolean;
|
private showInlineMenuIdentities: boolean;
|
||||||
private showInlineMenuCards: boolean;
|
private showInlineMenuCards: boolean;
|
||||||
private readonly findTabs = tabbable;
|
private readonly findTabs = tabbable;
|
||||||
@ -66,7 +67,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
private fieldsWithSubmitElements: WeakMap<FillableFormFieldElement, HTMLElement> = new WeakMap();
|
private fieldsWithSubmitElements: WeakMap<FillableFormFieldElement, HTMLElement> = new WeakMap();
|
||||||
private ignoredFieldTypes: Set<string> = new Set(AutoFillConstants.ExcludedInlineMenuTypes);
|
private ignoredFieldTypes: Set<string> = new Set(AutoFillConstants.ExcludedInlineMenuTypes);
|
||||||
private userFilledFields: Record<string, FillableFormFieldElement> = {};
|
private userFilledFields: Record<string, FillableFormFieldElement> = {};
|
||||||
private authStatus: AuthenticationStatus;
|
|
||||||
private focusableElements: FocusableElement[] = [];
|
private focusableElements: FocusableElement[] = [];
|
||||||
private mostRecentlyFocusedField: ElementWithOpId<FormFieldElement>;
|
private mostRecentlyFocusedField: ElementWithOpId<FormFieldElement>;
|
||||||
private focusedFieldData: FocusedFieldData;
|
private focusedFieldData: FocusedFieldData;
|
||||||
@ -74,8 +74,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
private focusInlineMenuListTimeout: number | NodeJS.Timeout;
|
private focusInlineMenuListTimeout: number | NodeJS.Timeout;
|
||||||
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
||||||
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
|
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
|
||||||
openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message),
|
|
||||||
addNewVaultItemFromOverlay: ({ message }) => this.addNewVaultItem(message),
|
addNewVaultItemFromOverlay: ({ message }) => this.addNewVaultItem(message),
|
||||||
|
focusMostRecentlyFocusedField: () => this.focusMostRecentlyFocusedField(),
|
||||||
blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(),
|
blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(),
|
||||||
unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(),
|
unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(),
|
||||||
checkIsMostRecentlyFocusedFieldWithinViewport: () =>
|
checkIsMostRecentlyFocusedFieldWithinViewport: () =>
|
||||||
@ -84,20 +84,25 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
bgVaultItemRepromptPopoutOpened: () => this.blurMostRecentlyFocusedField(true),
|
bgVaultItemRepromptPopoutOpened: () => this.blurMostRecentlyFocusedField(true),
|
||||||
redirectAutofillInlineMenuFocusOut: ({ message }) =>
|
redirectAutofillInlineMenuFocusOut: ({ message }) =>
|
||||||
this.redirectInlineMenuFocusOut(message?.data?.direction),
|
this.redirectInlineMenuFocusOut(message?.data?.direction),
|
||||||
updateAutofillInlineMenuVisibility: ({ message }) => this.updateInlineMenuVisibility(message),
|
|
||||||
getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message),
|
getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message),
|
||||||
getSubFrameOffsetsFromWindowMessage: ({ message }) =>
|
getSubFrameOffsetsFromWindowMessage: ({ message }) =>
|
||||||
this.getSubFrameOffsetsFromWindowMessage(message),
|
this.getSubFrameOffsetsFromWindowMessage(message),
|
||||||
checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(),
|
checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(),
|
||||||
setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(),
|
setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(),
|
||||||
destroyAutofillInlineMenuListeners: () => this.destroy(),
|
destroyAutofillInlineMenuListeners: () => this.destroy(),
|
||||||
getFormFieldDataForNotification: () => this.handleGetFormFieldDataForNotificationMessage(),
|
getInlineMenuFormFieldData: ({ message }) =>
|
||||||
|
this.handleGetInlineMenuFormFieldDataMessage(message),
|
||||||
};
|
};
|
||||||
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
|
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
|
||||||
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
|
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
|
||||||
[AutofillFieldQualifier.password]:
|
[AutofillFieldQualifier.password]:
|
||||||
this.inlineMenuFieldQualificationService.isCurrentPasswordField,
|
this.inlineMenuFieldQualificationService.isCurrentPasswordField,
|
||||||
};
|
};
|
||||||
|
private readonly accountCreationFieldQualifiers: Record<string, CallableFunction> = {
|
||||||
|
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
|
||||||
|
[AutofillFieldQualifier.newPassword]:
|
||||||
|
this.inlineMenuFieldQualificationService.isNewPasswordField,
|
||||||
|
};
|
||||||
private readonly cardFieldQualifiers: Record<string, CallableFunction> = {
|
private readonly cardFieldQualifiers: Record<string, CallableFunction> = {
|
||||||
[AutofillFieldQualifier.cardholderName]:
|
[AutofillFieldQualifier.cardholderName]:
|
||||||
this.inlineMenuFieldQualificationService.isFieldForCardholderName,
|
this.inlineMenuFieldQualificationService.isFieldForCardholderName,
|
||||||
@ -144,12 +149,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
this.inlineMenuFieldQualificationService.isFieldForIdentityEmail,
|
this.inlineMenuFieldQualificationService.isFieldForIdentityEmail,
|
||||||
[AutofillFieldQualifier.identityUsername]:
|
[AutofillFieldQualifier.identityUsername]:
|
||||||
this.inlineMenuFieldQualificationService.isFieldForIdentityUsername,
|
this.inlineMenuFieldQualificationService.isFieldForIdentityUsername,
|
||||||
[AutofillFieldQualifier.newPassword]:
|
|
||||||
this.inlineMenuFieldQualificationService.isNewPasswordField,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private domQueryService: DomQueryService,
|
private domQueryService: DomQueryService,
|
||||||
|
private domElementVisibilityService: DomElementVisibilityService,
|
||||||
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
|
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -158,6 +162,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* The observers will be instantiated on DOMContentLoaded if the page is current loading.
|
* The observers will be instantiated on DOMContentLoaded if the page is current loading.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
|
void this.getInlineMenuCardsVisibility();
|
||||||
|
void this.getInlineMenuIdentitiesVisibility();
|
||||||
|
|
||||||
if (globalThis.document.readyState === "loading") {
|
if (globalThis.document.readyState === "loading") {
|
||||||
globalThis.document.addEventListener(EVENTS.DOMCONTENTLOADED, this.setupGlobalEventListeners);
|
globalThis.document.addEventListener(EVENTS.DOMCONTENTLOADED, this.setupGlobalEventListeners);
|
||||||
return;
|
return;
|
||||||
@ -187,19 +194,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
autofillFieldData: AutofillField,
|
autofillFieldData: AutofillField,
|
||||||
pageDetails: AutofillPageDetails,
|
pageDetails: AutofillPageDetails,
|
||||||
) {
|
) {
|
||||||
if (!this.inlineMenuVisibility) {
|
|
||||||
await this.getInlineMenuVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showInlineMenuCards == null) {
|
|
||||||
await this.getInlineMenuCardsVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showInlineMenuIdentities == null) {
|
|
||||||
await this.getInlineMenuIdentitiesVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
currentlyInSandboxedIframe() ||
|
||||||
this.formFieldElements.has(formFieldElement) ||
|
this.formFieldElements.has(formFieldElement) ||
|
||||||
this.isIgnoredField(autofillFieldData, pageDetails)
|
this.isIgnoredField(autofillFieldData, pageDetails)
|
||||||
) {
|
) {
|
||||||
@ -213,76 +209,33 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
await this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData);
|
await this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles opening the autofill inline menu. Will conditionally open
|
|
||||||
* the inline menu based on the current inline menu visibility setting.
|
|
||||||
* Allows you to optionally focus the field element when opening the inline menu.
|
|
||||||
* Will also optionally ignore the inline menu visibility setting and open the
|
|
||||||
*
|
|
||||||
* @param options - Options for opening the autofill inline menu.
|
|
||||||
*/
|
|
||||||
openInlineMenu(options: OpenAutofillInlineMenuOptions = {}) {
|
|
||||||
const { isFocusingFieldElement, isOpeningFullInlineMenu, authStatus } = options;
|
|
||||||
if (!this.mostRecentlyFocusedField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.pageDetailsUpdateRequired) {
|
|
||||||
void this.sendExtensionMessage("bgCollectPageDetails", {
|
|
||||||
sender: "autofillOverlayContentService",
|
|
||||||
});
|
|
||||||
this.pageDetailsUpdateRequired = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFocusingFieldElement && !this.recentlyFocusedFieldIsCurrentlyFocused()) {
|
|
||||||
this.focusMostRecentlyFocusedField();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof authStatus !== "undefined") {
|
|
||||||
this.authStatus = authStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick &&
|
|
||||||
!isOpeningFullInlineMenu
|
|
||||||
) {
|
|
||||||
this.updateInlineMenuButtonPosition();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateInlineMenuElementsPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focuses the most recently focused field element.
|
|
||||||
*/
|
|
||||||
focusMostRecentlyFocusedField() {
|
|
||||||
this.mostRecentlyFocusedField?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes focus from the most recently focused field element.
|
* Removes focus from the most recently focused field element.
|
||||||
*/
|
*/
|
||||||
blurMostRecentlyFocusedField(isClosingInlineMenu: boolean = false) {
|
async blurMostRecentlyFocusedField(isClosingInlineMenu: boolean = false) {
|
||||||
this.mostRecentlyFocusedField?.blur();
|
this.mostRecentlyFocusedField?.blur();
|
||||||
|
|
||||||
if (isClosingInlineMenu) {
|
if (isClosingInlineMenu) {
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu");
|
await this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the most recently focused field within the current frame to a `null` value.
|
* Clears all cached user filled fields.
|
||||||
*/
|
*/
|
||||||
unsetMostRecentlyFocusedField() {
|
clearUserFilledFields() {
|
||||||
this.mostRecentlyFocusedField = null;
|
Object.keys(this.userFilledFields).forEach((key) => {
|
||||||
|
if (this.userFilledFields[key]) {
|
||||||
|
delete this.userFilledFields[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats any found user filled fields for a login cipher and sends a message
|
* Formats any found user filled fields for a login cipher and sends a message
|
||||||
* to the background script to add a new cipher.
|
* to the background script to add a new cipher.
|
||||||
*/
|
*/
|
||||||
async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
|
private async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
|
||||||
const command = "autofillOverlayAddNewVaultItem";
|
const command = "autofillOverlayAddNewVaultItem";
|
||||||
const password =
|
const password =
|
||||||
this.userFilledFields["newPassword"]?.value || this.userFilledFields["password"]?.value;
|
this.userFilledFields["newPassword"]?.value || this.userFilledFields["password"]?.value;
|
||||||
@ -295,7 +248,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
hostname: globalThis.document.location.hostname,
|
hostname: globalThis.document.location.hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
void this.sendExtensionMessage(command, { addNewCipherType, login });
|
await this.sendExtensionMessage(command, { addNewCipherType, login });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -310,7 +263,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
cvv: this.userFilledFields["cardCvv"]?.value || "",
|
cvv: this.userFilledFields["cardCvv"]?.value || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
void this.sendExtensionMessage(command, { addNewCipherType, card });
|
await this.sendExtensionMessage(command, { addNewCipherType, card });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -335,10 +288,24 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
username: this.userFilledFields["identityUsername"]?.value || "",
|
username: this.userFilledFields["identityUsername"]?.value || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
void this.sendExtensionMessage(command, { addNewCipherType, identity });
|
await this.sendExtensionMessage(command, { addNewCipherType, identity });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the most recently focused field element.
|
||||||
|
*/
|
||||||
|
private focusMostRecentlyFocusedField() {
|
||||||
|
this.mostRecentlyFocusedField?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the most recently focused field within the current frame to a `null` value.
|
||||||
|
*/
|
||||||
|
private unsetMostRecentlyFocusedField() {
|
||||||
|
this.mostRecentlyFocusedField = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirects the keyboard focus out of the inline menu, selecting the element that is
|
* Redirects the keyboard focus out of the inline menu, selecting the element that is
|
||||||
* either previous or next in the tab order. If the direction is current, the most
|
* either previous or next in the tab order. If the direction is current, the most
|
||||||
@ -436,23 +403,23 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* @param formFieldElement - The form field element to set up the submit button listeners for.
|
* @param formFieldElement - The form field element to set up the submit button listeners for.
|
||||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
*/
|
*/
|
||||||
private setupFormSubmissionEventListeners(
|
private async setupFormSubmissionEventListeners(
|
||||||
formFieldElement: ElementWithOpId<FormFieldElement>,
|
formFieldElement: ElementWithOpId<FormFieldElement>,
|
||||||
autofillFieldData: AutofillField,
|
autofillFieldData: AutofillField,
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!elementIsFillableFormField(formFieldElement) ||
|
!elementIsFillableFormField(formFieldElement) ||
|
||||||
autofillFieldData.filledByCipherType === CipherType.Card
|
autofillFieldData.inlineMenuFillType === CipherType.Card
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autofillFieldData.form) {
|
if (autofillFieldData.form) {
|
||||||
this.setupSubmitListenerOnFieldWithForms(formFieldElement);
|
await this.setupSubmitListenerOnFieldWithForms(formFieldElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupSubmitListenerOnFormlessField(formFieldElement);
|
await this.setupSubmitListenerOnFormlessField(formFieldElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -462,13 +429,20 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
*
|
*
|
||||||
* @param formFieldElement - The form field element to set up the submit listener for.
|
* @param formFieldElement - The form field element to set up the submit listener for.
|
||||||
*/
|
*/
|
||||||
private setupSubmitListenerOnFieldWithForms(formFieldElement: FillableFormFieldElement) {
|
private async setupSubmitListenerOnFieldWithForms(formFieldElement: FillableFormFieldElement) {
|
||||||
const formElement = formFieldElement.form;
|
const formElement = formFieldElement.form;
|
||||||
if (formElement && !this.formElements.has(formElement)) {
|
if (formElement && !this.formElements.has(formElement)) {
|
||||||
this.formElements.add(formElement);
|
this.formElements.add(formElement);
|
||||||
formElement.addEventListener(EVENTS.SUBMIT, this.handleFormFieldSubmitEvent);
|
formElement.addEventListener(EVENTS.SUBMIT, this.handleFormFieldSubmitEvent);
|
||||||
|
|
||||||
const closesSubmitButton = this.findSubmitButton(formElement);
|
const closesSubmitButton = await this.findSubmitButton(formElement);
|
||||||
|
|
||||||
|
// If we cannot find a submit button within the form, check for a submit button outside the form.
|
||||||
|
if (!closesSubmitButton) {
|
||||||
|
await this.setupSubmitListenerOnFormlessField(formFieldElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setupSubmitButtonEventListeners(closesSubmitButton);
|
this.setupSubmitButtonEventListeners(closesSubmitButton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -479,9 +453,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
*
|
*
|
||||||
* @param formFieldElement - The form field element to set up the submit listener for.
|
* @param formFieldElement - The form field element to set up the submit listener for.
|
||||||
*/
|
*/
|
||||||
private setupSubmitListenerOnFormlessField(formFieldElement: FillableFormFieldElement) {
|
private async setupSubmitListenerOnFormlessField(formFieldElement: FillableFormFieldElement) {
|
||||||
if (formFieldElement && !this.fieldsWithSubmitElements.has(formFieldElement)) {
|
if (formFieldElement && !this.fieldsWithSubmitElements.has(formFieldElement)) {
|
||||||
const closesSubmitButton = this.findClosestFormlessSubmitButton(formFieldElement);
|
const closesSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement);
|
||||||
this.setupSubmitButtonEventListeners(closesSubmitButton);
|
this.setupSubmitButtonEventListeners(closesSubmitButton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -491,13 +465,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
*
|
*
|
||||||
* @param formFieldElement - The form field element to find the closest formless submit button for.
|
* @param formFieldElement - The form field element to find the closest formless submit button for.
|
||||||
*/
|
*/
|
||||||
private findClosestFormlessSubmitButton(
|
private async findClosestFormlessSubmitButton(
|
||||||
formFieldElement: FillableFormFieldElement,
|
formFieldElement: FillableFormFieldElement,
|
||||||
): HTMLElement | null {
|
): Promise<HTMLElement | null> {
|
||||||
let currentElement: HTMLElement = formFieldElement;
|
let currentElement: HTMLElement = formFieldElement;
|
||||||
|
|
||||||
while (currentElement && currentElement.tagName !== "HTML") {
|
while (currentElement && currentElement.tagName !== "HTML") {
|
||||||
const submitButton = this.findSubmitButton(currentElement);
|
const submitButton = await this.findSubmitButton(currentElement);
|
||||||
if (submitButton) {
|
if (submitButton) {
|
||||||
this.formFieldElements.forEach((_, element) => {
|
this.formFieldElements.forEach((_, element) => {
|
||||||
if (currentElement.contains(element)) {
|
if (currentElement.contains(element)) {
|
||||||
@ -525,8 +499,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
*
|
*
|
||||||
* @param element - The element to find the submit button within.
|
* @param element - The element to find the submit button within.
|
||||||
*/
|
*/
|
||||||
private findSubmitButton(element: HTMLElement): HTMLElement | null {
|
private async findSubmitButton(element: HTMLElement): Promise<HTMLElement | null> {
|
||||||
const genericSubmitElement = this.querySubmitButtonElement(
|
const genericSubmitElement = await this.querySubmitButtonElement(
|
||||||
element,
|
element,
|
||||||
"[type='submit']",
|
"[type='submit']",
|
||||||
(node: Node) => nodeIsTypeSubmitElement(node),
|
(node: Node) => nodeIsTypeSubmitElement(node),
|
||||||
@ -535,7 +509,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return genericSubmitElement;
|
return genericSubmitElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitButtonElement = this.querySubmitButtonElement(
|
const submitButtonElement = await this.querySubmitButtonElement(
|
||||||
element,
|
element,
|
||||||
"button, [type='button']",
|
"button, [type='button']",
|
||||||
(node: Node) => nodeIsButtonElement(node),
|
(node: Node) => nodeIsButtonElement(node),
|
||||||
@ -543,6 +517,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
if (submitButtonElement) {
|
if (submitButtonElement) {
|
||||||
return submitButtonElement;
|
return submitButtonElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the submit button is not a traditional button element, check for an anchor element that contains submission keywords.
|
||||||
|
const submitAnchorElement = await this.querySubmitButtonElement(element, "a", (node: Node) =>
|
||||||
|
nodeIsAnchorElement(node),
|
||||||
|
);
|
||||||
|
if (submitAnchorElement) {
|
||||||
|
return submitAnchorElement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -552,7 +534,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* @param selector - The selector to use to query the element for a submit button.
|
* @param selector - The selector to use to query the element for a submit button.
|
||||||
* @param treeWalkerFilter - The tree walker filter to use when querying the element.
|
* @param treeWalkerFilter - The tree walker filter to use when querying the element.
|
||||||
*/
|
*/
|
||||||
private querySubmitButtonElement(
|
private async querySubmitButtonElement(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
selector: string,
|
selector: string,
|
||||||
treeWalkerFilter: CallableFunction,
|
treeWalkerFilter: CallableFunction,
|
||||||
@ -564,7 +546,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
);
|
);
|
||||||
for (let index = 0; index < submitButtonElements.length; index++) {
|
for (let index = 0; index < submitButtonElements.length; index++) {
|
||||||
const submitElement = submitButtonElements[index];
|
const submitElement = submitButtonElements[index];
|
||||||
if (this.isElementSubmitButton(submitElement)) {
|
if (
|
||||||
|
this.isElementSubmitButton(submitElement) &&
|
||||||
|
(await this.domElementVisibilityService.isElementViewable(submitElement))
|
||||||
|
) {
|
||||||
return submitElement;
|
return submitElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -624,26 +609,27 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* Handles the repositioning of the autofill overlay when the form is submitted.
|
* Handles the repositioning of the autofill overlay when the form is submitted.
|
||||||
*/
|
*/
|
||||||
private handleFormFieldSubmitEvent = () => {
|
private handleFormFieldSubmitEvent = () => {
|
||||||
void this.sendExtensionMessage("formFieldSubmitted", this.getFormFieldDataForNotification());
|
void this.sendExtensionMessage("formFieldSubmitted", this.getFormFieldData());
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles capturing the form field data for a notification message. Is triggered from the
|
* Handles capturing the form field data for a notification message. Will not trigger this behavior
|
||||||
* background script when a POST request is encountered. Will not trigger this behavior
|
* in the case where the user is still typing in the field unless the focus is ignored.
|
||||||
* in the case where the user is still typing in the field.
|
|
||||||
*/
|
*/
|
||||||
private handleGetFormFieldDataForNotificationMessage = async () => {
|
private handleGetInlineMenuFormFieldDataMessage = async ({
|
||||||
if (await this.isFieldCurrentlyFocused()) {
|
ignoreFieldFocus,
|
||||||
|
}: AutofillExtensionMessage) => {
|
||||||
|
if (!ignoreFieldFocus && (await this.isFieldCurrentlyFocused())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getFormFieldDataForNotification();
|
return this.getFormFieldData();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the form field data used for add login and change password notifications.
|
* Returns the form field data used for add login and change password notifications.
|
||||||
*/
|
*/
|
||||||
private getFormFieldDataForNotification = (): NotificationFormFieldData => {
|
private getFormFieldData = (): InlineMenuFormFieldData => {
|
||||||
return {
|
return {
|
||||||
uri: globalThis.document.URL,
|
uri: globalThis.document.URL,
|
||||||
username: this.userFilledFields["username"]?.value || "",
|
username: this.userFilledFields["username"]?.value || "",
|
||||||
@ -681,9 +667,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* is currently focused.
|
* is currently focused.
|
||||||
*/
|
*/
|
||||||
private handleFormFieldBlurEvent = () => {
|
private handleFormFieldBlurEvent = () => {
|
||||||
void this.sendExtensionMessage("updateIsFieldCurrentlyFocused", {
|
void this.updateIsFieldCurrentlyFocused(false);
|
||||||
isFieldCurrentlyFocused: false,
|
|
||||||
});
|
|
||||||
void this.sendExtensionMessage("checkAutofillInlineMenuFocused");
|
void this.sendExtensionMessage("checkAutofillInlineMenuFocused");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -726,7 +710,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) {
|
if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) {
|
||||||
this.clearFocusInlineMenuListTimeout();
|
this.clearFocusInlineMenuListTimeout();
|
||||||
await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
|
await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField);
|
||||||
this.openInlineMenu({ isOpeningFullInlineMenu: true });
|
await this.sendExtensionMessage("openAutofillInlineMenu", { isOpeningFullInlineMenu: true });
|
||||||
this.focusInlineMenuListTimeout = globalThis.setTimeout(
|
this.focusInlineMenuListTimeout = globalThis.setTimeout(
|
||||||
() => this.sendExtensionMessage("focusAutofillInlineMenuList"),
|
() => this.sendExtensionMessage("focusAutofillInlineMenuList"),
|
||||||
125,
|
125,
|
||||||
@ -744,7 +728,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
*/
|
*/
|
||||||
private handleFormFieldInputEvent = (formFieldElement: ElementWithOpId<FormFieldElement>) => {
|
private handleFormFieldInputEvent = (formFieldElement: ElementWithOpId<FormFieldElement>) => {
|
||||||
return this.useEventHandlersMemo(
|
return this.useEventHandlersMemo(
|
||||||
() => this.triggerFormFieldInput(formFieldElement),
|
debounce(() => this.triggerFormFieldInput(formFieldElement), 100, true),
|
||||||
this.getFormFieldHandlerMemoIndex(formFieldElement, EVENTS.INPUT),
|
this.getFormFieldHandlerMemoIndex(formFieldElement, EVENTS.INPUT),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -766,15 +750,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.hideInlineMenuListOnFilledField(formFieldElement)) {
|
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
overlayElement: AutofillOverlayElement.List,
|
||||||
overlayElement: AutofillOverlayElement.List,
|
forceCloseInlineMenu: true,
|
||||||
forceCloseInlineMenu: true,
|
});
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.openInlineMenu();
|
if (!formFieldElement?.value) {
|
||||||
|
await this.sendExtensionMessage("openAutofillInlineMenu");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -796,15 +779,20 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!autofillFieldData.fieldQualifier) {
|
if (!autofillFieldData.fieldQualifier) {
|
||||||
switch (autofillFieldData.filledByCipherType) {
|
switch (autofillFieldData.inlineMenuFillType) {
|
||||||
case CipherType.Login:
|
case CipherType.Login:
|
||||||
this.qualifyUserFilledLoginField(autofillFieldData);
|
case InlineMenuFillType.CurrentPasswordUpdate:
|
||||||
|
this.qualifyUserFilledField(autofillFieldData, this.loginFieldQualifiers);
|
||||||
|
break;
|
||||||
|
case InlineMenuFillType.AccountCreationUsername:
|
||||||
|
case InlineMenuFillType.PasswordGeneration:
|
||||||
|
this.qualifyUserFilledField(autofillFieldData, this.accountCreationFieldQualifiers);
|
||||||
break;
|
break;
|
||||||
case CipherType.Card:
|
case CipherType.Card:
|
||||||
this.qualifyUserFilledCardField(autofillFieldData);
|
this.qualifyUserFilledField(autofillFieldData, this.cardFieldQualifiers);
|
||||||
break;
|
break;
|
||||||
case CipherType.Identity:
|
case CipherType.Identity:
|
||||||
this.qualifyUserFilledIdentityField(autofillFieldData);
|
this.qualifyUserFilledField(autofillFieldData, this.identityFieldQualifiers);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -813,52 +801,22 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles qualifying the user field login field to be used when adding a new vault item.
|
* Handles qualification of the user filled field based on the field qualifiers provided.
|
||||||
*
|
*
|
||||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
|
* @param qualifiers - The field qualifiers to use when qualifying the user filled field.
|
||||||
*/
|
*/
|
||||||
private qualifyUserFilledLoginField(autofillFieldData: AutofillField) {
|
private qualifyUserFilledField = (
|
||||||
for (const [fieldQualifier, fieldQualifierFunction] of Object.entries(
|
autofillFieldData: AutofillField,
|
||||||
this.loginFieldQualifiers,
|
qualifiers: Record<string, CallableFunction>,
|
||||||
)) {
|
) => {
|
||||||
|
for (const [fieldQualifier, fieldQualifierFunction] of Object.entries(qualifiers)) {
|
||||||
if (fieldQualifierFunction(autofillFieldData)) {
|
if (fieldQualifierFunction(autofillFieldData)) {
|
||||||
autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType;
|
autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles qualifying the user field card field to be used when adding a new vault item.
|
|
||||||
*
|
|
||||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
|
||||||
*/
|
|
||||||
private qualifyUserFilledCardField(autofillFieldData: AutofillField) {
|
|
||||||
for (const [fieldQualifier, fieldQualifierFunction] of Object.entries(
|
|
||||||
this.cardFieldQualifiers,
|
|
||||||
)) {
|
|
||||||
if (fieldQualifierFunction(autofillFieldData)) {
|
|
||||||
autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles qualifying the user field identity field to be used when adding a new vault item.
|
|
||||||
*
|
|
||||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
|
||||||
*/
|
|
||||||
private qualifyUserFilledIdentityField(autofillFieldData: AutofillField) {
|
|
||||||
for (const [fieldQualifier, fieldQualifierFunction] of Object.entries(
|
|
||||||
this.identityFieldQualifiers,
|
|
||||||
)) {
|
|
||||||
if (fieldQualifierFunction(autofillFieldData)) {
|
|
||||||
autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the qualified user filled filed to allow for referencing its value when adding a new vault item.
|
* Stores the qualified user filled filed to allow for referencing its value when adding a new vault item.
|
||||||
@ -936,6 +894,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.pageDetailsUpdateRequired) {
|
||||||
|
await this.sendExtensionMessage("bgCollectPageDetails", {
|
||||||
|
sender: "autofillOverlayContentService",
|
||||||
|
});
|
||||||
|
this.pageDetailsUpdateRequired = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (elementIsSelectElement(formFieldElement)) {
|
if (elementIsSelectElement(formFieldElement)) {
|
||||||
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
forceCloseInlineMenu: true,
|
forceCloseInlineMenu: true,
|
||||||
@ -943,75 +908,19 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", {
|
await this.updateIsFieldCurrentlyFocused(true);
|
||||||
isFieldCurrentlyFocused: true,
|
|
||||||
});
|
|
||||||
const initiallyFocusedField = this.mostRecentlyFocusedField;
|
|
||||||
await this.updateMostRecentlyFocusedField(formFieldElement);
|
await this.updateMostRecentlyFocusedField(formFieldElement);
|
||||||
|
await this.sendExtensionMessage("openAutofillInlineMenu");
|
||||||
const hideInlineMenuListOnFilledField = await this.hideInlineMenuListOnFilledField(
|
|
||||||
formFieldElement as FillableFormFieldElement,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick ||
|
|
||||||
(initiallyFocusedField !== this.mostRecentlyFocusedField && hideInlineMenuListOnFilledField)
|
|
||||||
) {
|
|
||||||
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
forceCloseInlineMenu: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hideInlineMenuListOnFilledField) {
|
|
||||||
this.updateInlineMenuButtonPosition();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void this.sendExtensionMessage("openAutofillInlineMenu");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates whether the user is currently authenticated.
|
* Triggers an update in the background script focused status of the form field element.
|
||||||
|
*
|
||||||
|
* @param isFieldCurrentlyFocused - The focused status of the form field element.
|
||||||
*/
|
*/
|
||||||
private isUserAuthed() {
|
private updateIsFieldCurrentlyFocused = async (isFieldCurrentlyFocused: boolean) => {
|
||||||
return this.authStatus === AuthenticationStatus.Unlocked;
|
await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { isFieldCurrentlyFocused });
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that the most recently focused field is currently
|
|
||||||
* focused within the root node relative to the field.
|
|
||||||
*/
|
|
||||||
private recentlyFocusedFieldIsCurrentlyFocused() {
|
|
||||||
return (
|
|
||||||
this.getRootNodeActiveElement(this.mostRecentlyFocusedField) === this.mostRecentlyFocusedField
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the position of both the inline menu button and list.
|
|
||||||
*/
|
|
||||||
private updateInlineMenuElementsPosition() {
|
|
||||||
this.updateInlineMenuButtonPosition();
|
|
||||||
this.updateInlineMenuListPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the position of the inline menu button.
|
|
||||||
*/
|
|
||||||
private updateInlineMenuButtonPosition() {
|
|
||||||
void this.sendExtensionMessage("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.Button,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the position of the inline menu list.
|
|
||||||
*/
|
|
||||||
private updateInlineMenuListPosition() {
|
|
||||||
void this.sendExtensionMessage("updateAutofillInlineMenuPosition", {
|
|
||||||
overlayElement: AutofillOverlayElement.List,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the data used to position the inline menu elements in relation
|
* Updates the data used to position the inline menu elements in relation
|
||||||
@ -1036,31 +945,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
|
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
|
||||||
const autofillFieldData = this.formFieldElements.get(formFieldElement);
|
const autofillFieldData = this.formFieldElements.get(formFieldElement);
|
||||||
|
|
||||||
let accountCreationFieldType = null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
// user setting allows display of identities in inline menu
|
|
||||||
this.showInlineMenuIdentities &&
|
|
||||||
// `showInlineMenuAccountCreation` has been set or field is filled by Login cipher
|
|
||||||
(autofillFieldData?.showInlineMenuAccountCreation ||
|
|
||||||
autofillFieldData?.filledByCipherType === CipherType.Login) &&
|
|
||||||
// field is a username field, which is relevant to both Identity and Login ciphers
|
|
||||||
this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData)
|
|
||||||
) {
|
|
||||||
accountCreationFieldType = this.inlineMenuFieldQualificationService.isEmailField(
|
|
||||||
autofillFieldData,
|
|
||||||
)
|
|
||||||
? "email"
|
|
||||||
: autofillFieldData.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusedFieldData = {
|
this.focusedFieldData = {
|
||||||
focusedFieldStyles: { paddingRight, paddingLeft },
|
focusedFieldStyles: { paddingRight, paddingLeft },
|
||||||
focusedFieldRects: { width, height, top, left },
|
focusedFieldRects: { width, height, top, left },
|
||||||
filledByCipherType: autofillFieldData?.filledByCipherType,
|
inlineMenuFillType: autofillFieldData?.inlineMenuFillType,
|
||||||
showInlineMenuAccountCreation: autofillFieldData?.showInlineMenuAccountCreation,
|
|
||||||
showPasskeys: !!autofillFieldData?.showPasskeys,
|
showPasskeys: !!autofillFieldData?.showPasskeys,
|
||||||
accountCreationFieldType,
|
accountCreationFieldType: autofillFieldData?.accountCreationFieldType,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.sendExtensionMessage("updateFocusedFieldData", {
|
await this.sendExtensionMessage("updateFocusedFieldData", {
|
||||||
@ -1141,8 +1031,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
if (
|
if (
|
||||||
this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails)
|
this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails)
|
||||||
) {
|
) {
|
||||||
autofillFieldData.filledByCipherType = CipherType.Login;
|
void this.setQualifiedLoginFillType(autofillFieldData);
|
||||||
autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1153,7 +1042,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
pageDetails,
|
pageDetails,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
autofillFieldData.filledByCipherType = CipherType.Card;
|
autofillFieldData.inlineMenuFillType = CipherType.Card;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1163,8 +1052,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
pageDetails,
|
pageDetails,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
autofillFieldData.filledByCipherType = CipherType.Identity;
|
this.setQualifiedAccountCreationFillType(autofillFieldData);
|
||||||
autofillFieldData.showInlineMenuAccountCreation = true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1175,13 +1063,71 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
pageDetails,
|
pageDetails,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
autofillFieldData.filledByCipherType = CipherType.Identity;
|
autofillFieldData.inlineMenuFillType = CipherType.Identity;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the autofill field data that indicates this field is part of a login form
|
||||||
|
*
|
||||||
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
|
*/
|
||||||
|
private async setQualifiedLoginFillType(autofillFieldData: AutofillField) {
|
||||||
|
autofillFieldData.inlineMenuFillType = CipherType.Login;
|
||||||
|
autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn");
|
||||||
|
|
||||||
|
this.qualifyAccountCreationFieldType(autofillFieldData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the autofill field data that indicates this field is part of an account creation or update form.
|
||||||
|
*
|
||||||
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
|
*/
|
||||||
|
private setQualifiedAccountCreationFillType(autofillFieldData: AutofillField) {
|
||||||
|
if (this.inlineMenuFieldQualificationService.isNewPasswordField(autofillFieldData)) {
|
||||||
|
autofillFieldData.inlineMenuFillType = InlineMenuFillType.PasswordGeneration;
|
||||||
|
this.qualifyAccountCreationFieldType(autofillFieldData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isUpdateCurrentPasswordField(autofillFieldData)) {
|
||||||
|
autofillFieldData.inlineMenuFillType = InlineMenuFillType.CurrentPasswordUpdate;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData)) {
|
||||||
|
autofillFieldData.inlineMenuFillType = InlineMenuFillType.AccountCreationUsername;
|
||||||
|
this.qualifyAccountCreationFieldType(autofillFieldData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the account creation field type for the autofill field data based on the field's attributes.
|
||||||
|
*
|
||||||
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
|
*/
|
||||||
|
private qualifyAccountCreationFieldType(autofillFieldData: AutofillField) {
|
||||||
|
if (!this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData)) {
|
||||||
|
autofillFieldData.accountCreationFieldType = InlineMenuAccountCreationFieldType.Password;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.showInlineMenuIdentities) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isEmailField(autofillFieldData)) {
|
||||||
|
autofillFieldData.accountCreationFieldType = InlineMenuAccountCreationFieldType.Email;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autofillFieldData.accountCreationFieldType = InlineMenuAccountCreationFieldType.Text;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates whether a field is considered to be "hidden" based on the field's attributes.
|
* Validates whether a field is considered to be "hidden" based on the field's attributes.
|
||||||
* If the field is hidden, a fallback listener will be set up to ensure that the
|
* If the field is hidden, a fallback listener will be set up to ensure that the
|
||||||
@ -1287,12 +1233,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
) {
|
) {
|
||||||
this.formFieldElements.set(formFieldElement, autofillFieldData);
|
this.formFieldElements.set(formFieldElement, autofillFieldData);
|
||||||
|
|
||||||
if (!this.mostRecentlyFocusedField) {
|
if (elementIsFillableFormField(formFieldElement) && !!formFieldElement.value) {
|
||||||
await this.updateMostRecentlyFocusedField(formFieldElement);
|
this.storeModifiedFormElement(formFieldElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupFormFieldElementEventListeners(formFieldElement);
|
this.setupFormFieldElementEventListeners(formFieldElement);
|
||||||
this.setupFormSubmissionEventListeners(formFieldElement, autofillFieldData);
|
await this.setupFormSubmissionEventListeners(formFieldElement, autofillFieldData);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
globalThis.document.hasFocus() &&
|
globalThis.document.hasFocus() &&
|
||||||
@ -1302,16 +1248,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the background script for the autofill inline menu visibility setting.
|
|
||||||
* If the setting is not found, a default value of OnFieldFocus will be used
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async getInlineMenuVisibility() {
|
|
||||||
const inlineMenuVisibility = await this.sendExtensionMessage("getAutofillInlineMenuVisibility");
|
|
||||||
this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries the background script for the autofill inline menu's Cards visibility setting.
|
* Queries the background script for the autofill inline menu's Cards visibility setting.
|
||||||
* If the setting is not found, a default value of true will be used
|
* If the setting is not found, a default value of true will be used
|
||||||
@ -1336,20 +1272,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
this.showInlineMenuIdentities = inlineMenuIdentitiesVisibility ?? true;
|
this.showInlineMenuIdentities = inlineMenuIdentitiesVisibility ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
|
|
||||||
*
|
|
||||||
* @param formFieldElement - The form field element that triggered the focus event.
|
|
||||||
*/
|
|
||||||
private async hideInlineMenuListOnFilledField(
|
|
||||||
formFieldElement?: FillableFormFieldElement,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
formFieldElement?.value &&
|
|
||||||
((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether the most recently focused field has a value.
|
* Indicates whether the most recently focused field has a value.
|
||||||
*/
|
*/
|
||||||
@ -1357,19 +1279,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
|
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the local reference to the inline menu visibility setting.
|
|
||||||
*
|
|
||||||
* @param data - The data object from the extension message.
|
|
||||||
*/
|
|
||||||
private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) {
|
|
||||||
const newSettingValue = data?.newSettingValue;
|
|
||||||
|
|
||||||
if (!isNaN(newSettingValue)) {
|
|
||||||
this.inlineMenuVisibility = newSettingValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a field is currently filling within an frame in the tab.
|
* Checks if a field is currently filling within an frame in the tab.
|
||||||
*/
|
*/
|
||||||
@ -1398,13 +1307,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFocused")) === true;
|
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFocused")) === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the current tab contains ciphers that can be used to populate the inline menu.
|
|
||||||
*/
|
|
||||||
private async isInlineMenuCiphersPopulated() {
|
|
||||||
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the root node of the passed element and returns the active element within that root node.
|
* Gets the root node of the passed element and returns the active element within that root node.
|
||||||
*
|
*
|
||||||
@ -1611,7 +1513,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
private setupGlobalEventListeners = () => {
|
private setupGlobalEventListeners = () => {
|
||||||
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
||||||
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
|
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
|
||||||
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleWindowFocusOutEvent);
|
||||||
this.setOverlayRepositionEventListeners();
|
this.setOverlayRepositionEventListeners();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1627,19 +1529,36 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the window focus out event, triggering a focus check on the
|
||||||
|
* inline menu if the document has focus and a closure of the inline
|
||||||
|
* menu if it does not have focus.
|
||||||
|
*/
|
||||||
|
private handleWindowFocusOutEvent = () => {
|
||||||
|
if (document.hasFocus()) {
|
||||||
|
this.handleFormFieldBlurEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the visibility change event. This method will remove the
|
* Handles the visibility change event. This method will remove the
|
||||||
* autofill overlay if the document is not visible.
|
* autofill overlay if the document is not visible.
|
||||||
*/
|
*/
|
||||||
private handleVisibilityChangeEvent = () => {
|
private handleVisibilityChangeEvent = () => {
|
||||||
if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
|
if (globalThis.document.visibilityState === "hidden") {
|
||||||
return;
|
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unsetMostRecentlyFocusedField();
|
if (this.mostRecentlyFocusedField) {
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
this.unsetMostRecentlyFocusedField();
|
||||||
forceCloseInlineMenu: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1811,11 +1730,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||||
this.formFieldElements.delete(formFieldElement);
|
this.formFieldElements.delete(formFieldElement);
|
||||||
});
|
});
|
||||||
Object.keys(this.userFilledFields).forEach((key) => {
|
this.clearUserFilledFields();
|
||||||
if (this.userFilledFields[key]) {
|
|
||||||
delete this.userFilledFields[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.userFilledFields = null;
|
this.userFilledFields = null;
|
||||||
globalThis.removeEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
globalThis.removeEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
||||||
globalThis.document.removeEventListener(
|
globalThis.document.removeEventListener(
|
||||||
|
@ -289,41 +289,6 @@ describe("AutofillService", () => {
|
|||||||
expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled();
|
expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("updates the inline menu visibility setting", () => {
|
|
||||||
it("when changing the inline menu from on focus of field to on button click", async () => {
|
|
||||||
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
|
||||||
tab1,
|
|
||||||
"updateAutofillInlineMenuVisibility",
|
|
||||||
{ newSettingValue: AutofillOverlayVisibility.OnButtonClick },
|
|
||||||
);
|
|
||||||
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
|
||||||
tab2,
|
|
||||||
"updateAutofillInlineMenuVisibility",
|
|
||||||
{ newSettingValue: AutofillOverlayVisibility.OnButtonClick },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when changing the inline menu from button click to field focus", async () => {
|
|
||||||
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
|
|
||||||
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnFieldFocus);
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
|
||||||
tab1,
|
|
||||||
"updateAutofillInlineMenuVisibility",
|
|
||||||
{ newSettingValue: AutofillOverlayVisibility.OnFieldFocus },
|
|
||||||
);
|
|
||||||
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
|
||||||
tab2,
|
|
||||||
"updateAutofillInlineMenuVisibility",
|
|
||||||
{ newSettingValue: AutofillOverlayVisibility.OnFieldFocus },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("reloads the autofill scripts", () => {
|
describe("reloads the autofill scripts", () => {
|
||||||
it("when changing the inline menu from a disabled setting to an enabled setting", async () => {
|
it("when changing the inline menu from a disabled setting to an enabled setting", async () => {
|
||||||
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.Off);
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.Off);
|
||||||
@ -3292,10 +3257,6 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
|
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
|
||||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
|
|
||||||
excludedField,
|
|
||||||
AutoFillConstants.ExcludedAutofillTypes,
|
|
||||||
);
|
|
||||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||||
expect(value.script).toStrictEqual([]);
|
expect(value.script).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
@ -4725,8 +4686,6 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
const result = AutofillService["fieldIsFuzzyMatch"](field, ["some-value"]);
|
const result = AutofillService["fieldIsFuzzyMatch"](field, ["some-value"]);
|
||||||
|
|
||||||
expect(AutofillService.hasValue).toHaveBeenCalledTimes(7);
|
|
||||||
expect(AutofillService["fuzzyMatch"]).not.toHaveBeenCalled();
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1882,7 +1882,10 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
*/
|
*/
|
||||||
private excludeFieldFromIdentityFill(field: AutofillField): boolean {
|
private excludeFieldFromIdentityFill(field: AutofillField): boolean {
|
||||||
return (
|
return (
|
||||||
AutofillService.isExcludedFieldType(field, AutoFillConstants.ExcludedAutofillTypes) ||
|
AutofillService.isExcludedFieldType(field, [
|
||||||
|
"password",
|
||||||
|
...AutoFillConstants.ExcludedAutofillTypes,
|
||||||
|
]) ||
|
||||||
AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) ||
|
AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) ||
|
||||||
!field.viewable
|
!field.viewable
|
||||||
);
|
);
|
||||||
@ -2887,6 +2890,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
AutofillService.hasValue(field.dataSetValues) &&
|
||||||
|
this.fuzzyMatch(names, field.dataSetValues)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3062,13 +3071,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
*
|
*
|
||||||
* @param oldSettingValue - The previous setting value
|
* @param oldSettingValue - The previous setting value
|
||||||
* @param newSettingValue - The current setting value
|
* @param newSettingValue - The current setting value
|
||||||
* @param cipherType - The cipher type of the changed inline menu setting
|
|
||||||
*/
|
*/
|
||||||
private async handleInlineMenuVisibilitySettingsChange(
|
private async handleInlineMenuVisibilitySettingsChange(
|
||||||
oldSettingValue: InlineMenuVisibilitySetting | boolean,
|
oldSettingValue: InlineMenuVisibilitySetting | boolean,
|
||||||
newSettingValue: InlineMenuVisibilitySetting | boolean,
|
newSettingValue: InlineMenuVisibilitySetting | boolean,
|
||||||
) {
|
) {
|
||||||
if (oldSettingValue === undefined || oldSettingValue === newSettingValue) {
|
if (oldSettingValue == null || oldSettingValue === newSettingValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3076,18 +3084,11 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
typeof oldSettingValue === "boolean" || typeof newSettingValue === "boolean";
|
typeof oldSettingValue === "boolean" || typeof newSettingValue === "boolean";
|
||||||
const inlineMenuPreviouslyDisabled = oldSettingValue === AutofillOverlayVisibility.Off;
|
const inlineMenuPreviouslyDisabled = oldSettingValue === AutofillOverlayVisibility.Off;
|
||||||
const inlineMenuCurrentlyDisabled = newSettingValue === AutofillOverlayVisibility.Off;
|
const inlineMenuCurrentlyDisabled = newSettingValue === AutofillOverlayVisibility.Off;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isInlineMenuVisibilitySubSetting &&
|
!isInlineMenuVisibilitySubSetting &&
|
||||||
!inlineMenuPreviouslyDisabled &&
|
!inlineMenuPreviouslyDisabled &&
|
||||||
!inlineMenuCurrentlyDisabled
|
!inlineMenuCurrentlyDisabled
|
||||||
) {
|
) {
|
||||||
const tabs = await BrowserApi.tabsQuery({});
|
|
||||||
tabs.forEach((tab) =>
|
|
||||||
BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", {
|
|
||||||
newSettingValue,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
const domQueryService = new DomQueryService();
|
const domQueryService = new DomQueryService();
|
||||||
const autofillOverlayContentService = new AutofillOverlayContentService(
|
const autofillOverlayContentService = new AutofillOverlayContentService(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
inlineMenuFieldQualificationService,
|
inlineMenuFieldQualificationService,
|
||||||
);
|
);
|
||||||
let collectAutofillContentService: CollectAutofillContentService;
|
let collectAutofillContentService: CollectAutofillContentService;
|
||||||
@ -262,8 +263,8 @@ describe("CollectAutofillContentService", () => {
|
|||||||
collectAutofillContentService["autofillFieldElements"] = new Map([
|
collectAutofillContentService["autofillFieldElements"] = new Map([
|
||||||
[fieldElement, autofillField],
|
[fieldElement, autofillField],
|
||||||
]);
|
]);
|
||||||
const isFormFieldViewableSpy = jest
|
const isElementViewableSpy = jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
|
const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
|
||||||
collectAutofillContentService["autofillOverlayContentService"],
|
collectAutofillContentService["autofillOverlayContentService"],
|
||||||
@ -273,7 +274,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
await collectAutofillContentService.getPageDetails();
|
await collectAutofillContentService.getPageDetails();
|
||||||
|
|
||||||
expect(autofillField.viewable).toBe(true);
|
expect(autofillField.viewable).toBe(true);
|
||||||
expect(isFormFieldViewableSpy).toHaveBeenCalledWith(fieldElement);
|
expect(isElementViewableSpy).toHaveBeenCalledWith(fieldElement);
|
||||||
expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalled();
|
expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -301,7 +302,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
||||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
|
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
|
||||||
jest
|
jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
const pageDetails = await collectAutofillContentService.getPageDetails();
|
const pageDetails = await collectAutofillContentService.getPageDetails();
|
||||||
@ -353,6 +354,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
"aria-disabled": false,
|
"aria-disabled": false,
|
||||||
"aria-haspopup": false,
|
"aria-haspopup": false,
|
||||||
"data-stripe": null,
|
"data-stripe": null,
|
||||||
|
dataSetValues: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
opid: "__1",
|
opid: "__1",
|
||||||
@ -385,6 +387,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
"aria-disabled": false,
|
"aria-disabled": false,
|
||||||
"aria-haspopup": false,
|
"aria-haspopup": false,
|
||||||
"data-stripe": null,
|
"data-stripe": null,
|
||||||
|
dataSetValues: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
collectedTimestamp: expect.any(Number),
|
collectedTimestamp: expect.any(Number),
|
||||||
@ -561,7 +564,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldElements");
|
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldElements");
|
||||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem");
|
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem");
|
||||||
jest
|
jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
const { formFieldElements } =
|
const { formFieldElements } =
|
||||||
@ -609,6 +612,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
value: "",
|
value: "",
|
||||||
viewable: true,
|
viewable: true,
|
||||||
|
dataSetValues: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aria-disabled": false,
|
"aria-disabled": false,
|
||||||
@ -641,6 +645,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
type: "password",
|
type: "password",
|
||||||
value: "",
|
value: "",
|
||||||
viewable: true,
|
viewable: true,
|
||||||
|
dataSetValues: "",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -929,7 +934,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
collectAutofillContentService["autofillFieldElements"].set(usernameInput, existingFieldData);
|
collectAutofillContentService["autofillFieldElements"].set(usernameInput, existingFieldData);
|
||||||
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
||||||
jest
|
jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
||||||
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
||||||
@ -941,7 +946,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
|
|
||||||
expect(collectAutofillContentService["getAutofillFieldMaxLength"]).not.toHaveBeenCalled();
|
expect(collectAutofillContentService["getAutofillFieldMaxLength"]).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable,
|
collectAutofillContentService["domElementVisibilityService"].isElementViewable,
|
||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
|
expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
|
||||||
expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled();
|
expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled();
|
||||||
@ -962,7 +967,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
) as ElementWithOpId<FormFieldElement>;
|
) as ElementWithOpId<FormFieldElement>;
|
||||||
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
||||||
jest
|
jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
||||||
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
||||||
@ -976,7 +981,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
spanElement,
|
spanElement,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable,
|
collectAutofillContentService["domElementVisibilityService"].isElementViewable,
|
||||||
).toHaveBeenCalledWith(spanElement);
|
).toHaveBeenCalledWith(spanElement);
|
||||||
expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
|
expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
@ -1020,6 +1025,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
tagName: spanElement.tagName.toLowerCase(),
|
tagName: spanElement.tagName.toLowerCase(),
|
||||||
title: spanElementTitle,
|
title: spanElementTitle,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
|
dataSetValues: "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1070,7 +1076,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
) as ElementWithOpId<FillableFormFieldElement>;
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
||||||
jest
|
jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
||||||
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
||||||
@ -1111,6 +1117,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
type: usernameField.type,
|
type: usernameField.type,
|
||||||
value: usernameField.value,
|
value: usernameField.value,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
|
dataSetValues: "label: username-data-label, stripe: data-stripe, ",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1155,7 +1162,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
) as ElementWithOpId<FillableFormFieldElement>;
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
|
||||||
jest
|
jest
|
||||||
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
|
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isElementViewable")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
|
||||||
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
jest.spyOn(collectAutofillContentService as any, "getElementValue");
|
||||||
@ -1189,6 +1196,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
type: hiddenField.type,
|
type: hiddenField.type,
|
||||||
value: hiddenField.value,
|
value: hiddenField.value,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
|
dataSetValues: "stripe: data-stripe, ",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -2499,13 +2507,13 @@ describe("CollectAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("handleFormElementIntersection", () => {
|
describe("handleFormElementIntersection", () => {
|
||||||
let isFormFieldViewableSpy: jest.SpyInstance;
|
let isElementViewableSpy: jest.SpyInstance;
|
||||||
let setupAutofillOverlayListenerOnFieldSpy: jest.SpyInstance;
|
let setupAutofillOverlayListenerOnFieldSpy: jest.SpyInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
isFormFieldViewableSpy = jest.spyOn(
|
isElementViewableSpy = jest.spyOn(
|
||||||
collectAutofillContentService["domElementVisibilityService"],
|
collectAutofillContentService["domElementVisibilityService"],
|
||||||
"isFormFieldViewable",
|
"isElementViewable",
|
||||||
);
|
);
|
||||||
setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
|
setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
|
||||||
collectAutofillContentService["autofillOverlayContentService"],
|
collectAutofillContentService["autofillOverlayContentService"],
|
||||||
@ -2524,7 +2532,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
|
|
||||||
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
||||||
|
|
||||||
expect(isFormFieldViewableSpy).not.toHaveBeenCalled();
|
expect(isElementViewableSpy).not.toHaveBeenCalled();
|
||||||
expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
|
expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2535,11 +2543,11 @@ describe("CollectAutofillContentService", () => {
|
|||||||
{ target: formFieldElement, isIntersecting: true },
|
{ target: formFieldElement, isIntersecting: true },
|
||||||
] as unknown as IntersectionObserverEntry[];
|
] as unknown as IntersectionObserverEntry[];
|
||||||
collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
|
collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
|
||||||
isFormFieldViewableSpy.mockReturnValueOnce(false);
|
isElementViewableSpy.mockReturnValueOnce(false);
|
||||||
|
|
||||||
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
||||||
|
|
||||||
expect(isFormFieldViewableSpy).toHaveBeenCalledWith(formFieldElement);
|
expect(isElementViewableSpy).toHaveBeenCalledWith(formFieldElement);
|
||||||
expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
|
expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2548,12 +2556,12 @@ describe("CollectAutofillContentService", () => {
|
|||||||
const entries = [
|
const entries = [
|
||||||
{ target: formFieldElement, isIntersecting: true },
|
{ target: formFieldElement, isIntersecting: true },
|
||||||
] as unknown as IntersectionObserverEntry[];
|
] as unknown as IntersectionObserverEntry[];
|
||||||
isFormFieldViewableSpy.mockReturnValueOnce(true);
|
isElementViewableSpy.mockReturnValueOnce(true);
|
||||||
collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver;
|
collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver;
|
||||||
|
|
||||||
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
||||||
|
|
||||||
expect(isFormFieldViewableSpy).not.toHaveBeenCalled();
|
expect(isElementViewableSpy).not.toHaveBeenCalled();
|
||||||
expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
|
expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2563,13 +2571,13 @@ describe("CollectAutofillContentService", () => {
|
|||||||
const entries = [
|
const entries = [
|
||||||
{ target: formFieldElement, isIntersecting: true },
|
{ target: formFieldElement, isIntersecting: true },
|
||||||
] as unknown as IntersectionObserverEntry[];
|
] as unknown as IntersectionObserverEntry[];
|
||||||
isFormFieldViewableSpy.mockReturnValueOnce(true);
|
isElementViewableSpy.mockReturnValueOnce(true);
|
||||||
collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
|
collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField);
|
||||||
collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver;
|
collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver;
|
||||||
|
|
||||||
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
await collectAutofillContentService["handleFormElementIntersection"](entries);
|
||||||
|
|
||||||
expect(isFormFieldViewableSpy).toHaveBeenCalledWith(formFieldElement);
|
expect(isElementViewableSpy).toHaveBeenCalledWith(formFieldElement);
|
||||||
expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledWith(
|
expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledWith(
|
||||||
formFieldElement,
|
formFieldElement,
|
||||||
autofillField,
|
autofillField,
|
||||||
|
@ -196,7 +196,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
private updateCachedAutofillFieldVisibility() {
|
private updateCachedAutofillFieldVisibility() {
|
||||||
this.autofillFieldElements.forEach(async (autofillField, element) => {
|
this.autofillFieldElements.forEach(async (autofillField, element) => {
|
||||||
const previouslyViewable = autofillField.viewable;
|
const previouslyViewable = autofillField.viewable;
|
||||||
autofillField.viewable = await this.domElementVisibilityService.isFormFieldViewable(element);
|
autofillField.viewable = await this.domElementVisibilityService.isElementViewable(element);
|
||||||
|
|
||||||
if (!previouslyViewable && autofillField.viewable) {
|
if (!previouslyViewable && autofillField.viewable) {
|
||||||
this.setupOverlayOnField(element, autofillField);
|
this.setupOverlayOnField(element, autofillField);
|
||||||
@ -360,13 +360,14 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
opid: element.opid,
|
opid: element.opid,
|
||||||
elementNumber: index,
|
elementNumber: index,
|
||||||
maxLength: this.getAutofillFieldMaxLength(element),
|
maxLength: this.getAutofillFieldMaxLength(element),
|
||||||
viewable: await this.domElementVisibilityService.isFormFieldViewable(element),
|
viewable: await this.domElementVisibilityService.isElementViewable(element),
|
||||||
htmlID: this.getPropertyOrAttribute(element, "id"),
|
htmlID: this.getPropertyOrAttribute(element, "id"),
|
||||||
htmlName: this.getPropertyOrAttribute(element, "name"),
|
htmlName: this.getPropertyOrAttribute(element, "name"),
|
||||||
htmlClass: this.getPropertyOrAttribute(element, "class"),
|
htmlClass: this.getPropertyOrAttribute(element, "class"),
|
||||||
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
|
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
|
||||||
title: this.getPropertyOrAttribute(element, "title"),
|
title: this.getPropertyOrAttribute(element, "title"),
|
||||||
tagName: this.getAttributeLowerCase(element, "tagName"),
|
tagName: this.getAttributeLowerCase(element, "tagName"),
|
||||||
|
dataSetValues: this.getDataSetValues(element),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!autofillFieldBase.viewable) {
|
if (!autofillFieldBase.viewable) {
|
||||||
@ -800,6 +801,21 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
return elementValue;
|
return elementValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the `data-*` attribute metadata to help with validating the autofill data.
|
||||||
|
*
|
||||||
|
* @param element - The form field element to capture the `data-*` attribute metadata from
|
||||||
|
*/
|
||||||
|
private getDataSetValues(element: ElementWithOpId<FormFieldElement>): string {
|
||||||
|
let datasetValues = "";
|
||||||
|
const dataset = element.dataset;
|
||||||
|
for (const key in dataset) {
|
||||||
|
datasetValues += `${key}: ${dataset[key]}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
return datasetValues;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the options from a select element and return them as an array
|
* Get the options from a select element and return them as an array
|
||||||
* of arrays indicating the select element option text and value.
|
* of arrays indicating the select element option text and value.
|
||||||
@ -945,6 +961,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
this.domRecentlyMutated = true;
|
this.domRecentlyMutated = true;
|
||||||
if (this.autofillOverlayContentService) {
|
if (this.autofillOverlayContentService) {
|
||||||
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
||||||
|
this.autofillOverlayContentService.clearUserFilledFields();
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true });
|
void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true });
|
||||||
}
|
}
|
||||||
this.noFieldsFound = false;
|
this.noFieldsFound = false;
|
||||||
@ -1315,8 +1332,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isViewable =
|
const isViewable = await this.domElementVisibilityService.isElementViewable(formFieldElement);
|
||||||
await this.domElementVisibilityService.isFormFieldViewable(formFieldElement);
|
|
||||||
if (!isViewable) {
|
if (!isViewable) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ describe("DomElementVisibilityService", () => {
|
|||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isFormFieldViewable", () => {
|
describe("isElementViewable", () => {
|
||||||
it("returns false if the element is outside viewport bounds", async () => {
|
it("returns false if the element is outside viewport bounds", async () => {
|
||||||
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
const usernameElement = document.querySelector("input[name='username']") as FormFieldElement;
|
||||||
jest.spyOn(usernameElement, "getBoundingClientRect");
|
jest.spyOn(usernameElement, "getBoundingClientRect");
|
||||||
@ -47,10 +47,10 @@ describe("DomElementVisibilityService", () => {
|
|||||||
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss");
|
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss");
|
||||||
jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement");
|
jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement");
|
||||||
|
|
||||||
const isFormFieldViewable =
|
const isElementViewable =
|
||||||
await domElementVisibilityService.isFormFieldViewable(usernameElement);
|
await domElementVisibilityService.isElementViewable(usernameElement);
|
||||||
|
|
||||||
expect(isFormFieldViewable).toEqual(false);
|
expect(isElementViewable).toEqual(false);
|
||||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||||
usernameElement,
|
usernameElement,
|
||||||
@ -71,10 +71,10 @@ describe("DomElementVisibilityService", () => {
|
|||||||
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(true);
|
jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(true);
|
||||||
jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement");
|
jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement");
|
||||||
|
|
||||||
const isFormFieldViewable =
|
const isElementViewable =
|
||||||
await domElementVisibilityService.isFormFieldViewable(usernameElement);
|
await domElementVisibilityService.isElementViewable(usernameElement);
|
||||||
|
|
||||||
expect(isFormFieldViewable).toEqual(false);
|
expect(isElementViewable).toEqual(false);
|
||||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||||
usernameElement,
|
usernameElement,
|
||||||
@ -99,10 +99,10 @@ describe("DomElementVisibilityService", () => {
|
|||||||
.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement")
|
.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement")
|
||||||
.mockReturnValueOnce(false);
|
.mockReturnValueOnce(false);
|
||||||
|
|
||||||
const isFormFieldViewable =
|
const isElementViewable =
|
||||||
await domElementVisibilityService.isFormFieldViewable(usernameElement);
|
await domElementVisibilityService.isElementViewable(usernameElement);
|
||||||
|
|
||||||
expect(isFormFieldViewable).toEqual(false);
|
expect(isElementViewable).toEqual(false);
|
||||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||||
usernameElement,
|
usernameElement,
|
||||||
@ -127,10 +127,10 @@ describe("DomElementVisibilityService", () => {
|
|||||||
.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement")
|
.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement")
|
||||||
.mockReturnValueOnce(true);
|
.mockReturnValueOnce(true);
|
||||||
|
|
||||||
const isFormFieldViewable =
|
const isElementViewable =
|
||||||
await domElementVisibilityService.isFormFieldViewable(usernameElement);
|
await domElementVisibilityService.isElementViewable(usernameElement);
|
||||||
|
|
||||||
expect(isFormFieldViewable).toEqual(true);
|
expect(isElementViewable).toEqual(true);
|
||||||
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
expect(usernameElement.getBoundingClientRect).toHaveBeenCalled();
|
||||||
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith(
|
||||||
usernameElement,
|
usernameElement,
|
||||||
|
@ -6,15 +6,14 @@ import { DomElementVisibilityService as DomElementVisibilityServiceInterface } f
|
|||||||
class DomElementVisibilityService implements DomElementVisibilityServiceInterface {
|
class DomElementVisibilityService implements DomElementVisibilityServiceInterface {
|
||||||
private cachedComputedStyle: CSSStyleDeclaration | null = null;
|
private cachedComputedStyle: CSSStyleDeclaration | null = null;
|
||||||
|
|
||||||
constructor(private inlineMenuElements?: AutofillInlineMenuContentService) {}
|
constructor(private inlineMenuContentService?: AutofillInlineMenuContentService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a form field is viewable. This is done by checking if the element is within the
|
* Checks if an element is viewable. This is done by checking if the element is within the
|
||||||
* viewport bounds, not hidden by CSS, and not hidden behind another element.
|
* viewport bounds, not hidden by CSS, and not hidden behind another element.
|
||||||
* @param {FormFieldElement} element
|
* @param element
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
*/
|
||||||
async isFormFieldViewable(element: FormFieldElement): Promise<boolean> {
|
async isElementViewable(element: HTMLElement): Promise<boolean> {
|
||||||
const elementBoundingClientRect = element.getBoundingClientRect();
|
const elementBoundingClientRect = element.getBoundingClientRect();
|
||||||
if (
|
if (
|
||||||
this.isElementOutsideViewportBounds(element, elementBoundingClientRect) ||
|
this.isElementOutsideViewportBounds(element, elementBoundingClientRect) ||
|
||||||
@ -190,7 +189,7 @@ class DomElementVisibilityService implements DomElementVisibilityServiceInterfac
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inlineMenuElements?.isElementInlineMenu(elementAtCenterPoint as HTMLElement)) {
|
if (this.inlineMenuContentService?.isElementInlineMenu(elementAtCenterPoint as HTMLElement)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,6 +192,10 @@ export class DomQueryService implements DomQueryServiceInterface {
|
|||||||
root: Document | ShadowRoot | Element,
|
root: Document | ShadowRoot | Element,
|
||||||
returnSingleShadowRoot = false,
|
returnSingleShadowRoot = false,
|
||||||
): ShadowRoot[] {
|
): ShadowRoot[] {
|
||||||
|
if (!root) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const shadowRoots: ShadowRoot[] = [];
|
const shadowRoots: ShadowRoot[] = [];
|
||||||
const potentialShadowRoots = root.querySelectorAll(":defined");
|
const potentialShadowRoots = root.querySelectorAll(":defined");
|
||||||
for (let index = 0; index < potentialShadowRoots.length; index++) {
|
for (let index = 0; index < potentialShadowRoots.length; index++) {
|
||||||
|
@ -408,7 +408,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
|||||||
autoCompleteType: "new-password",
|
autoCompleteType: "new-password",
|
||||||
htmlID: "user-password",
|
htmlID: "user-password",
|
||||||
htmlName: "user-password",
|
htmlName: "user-password",
|
||||||
placeholder: "user-password",
|
placeholder: "new password",
|
||||||
});
|
});
|
||||||
pageDetails.fields = [field, passwordField];
|
pageDetails.fields = [field, passwordField];
|
||||||
|
|
||||||
|
@ -35,9 +35,31 @@ export class InlineMenuFieldQualificationService
|
|||||||
private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap();
|
private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap();
|
||||||
private submitButtonKeywordsMap: SubmitButtonKeywordsMap = new WeakMap();
|
private submitButtonKeywordsMap: SubmitButtonKeywordsMap = new WeakMap();
|
||||||
private autocompleteDisabledValues = new Set(["off", "false"]);
|
private autocompleteDisabledValues = new Set(["off", "false"]);
|
||||||
private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
|
|
||||||
private accountCreationFieldKeywords = [
|
private accountCreationFieldKeywords = [
|
||||||
...new Set(["register", "registration", "create", "confirm", ...this.newFieldKeywords]),
|
"register",
|
||||||
|
"registration",
|
||||||
|
"create password",
|
||||||
|
"create a password",
|
||||||
|
"create an account",
|
||||||
|
"create account password",
|
||||||
|
"create user password",
|
||||||
|
"confirm password",
|
||||||
|
"confirm account password",
|
||||||
|
"confirm user password",
|
||||||
|
"new user",
|
||||||
|
"new email",
|
||||||
|
"new e-mail",
|
||||||
|
"new password",
|
||||||
|
"new-password",
|
||||||
|
"neuer benutzer",
|
||||||
|
"neues passwort",
|
||||||
|
"neue e-mail",
|
||||||
|
];
|
||||||
|
private updatePasswordFieldKeywords = [
|
||||||
|
"update password",
|
||||||
|
"change password",
|
||||||
|
"current password",
|
||||||
|
"kennwort ändern",
|
||||||
];
|
];
|
||||||
private creditCardFieldKeywords = [
|
private creditCardFieldKeywords = [
|
||||||
...new Set([
|
...new Set([
|
||||||
@ -145,8 +167,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
return this.isFieldForLoginFormFallback(field);
|
return this.isFieldForLoginFormFallback(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTotpField = this.isTotpField(field);
|
if (this.isTotpField(field)) {
|
||||||
if (isTotpField) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,12 +197,6 @@ export class InlineMenuFieldQualificationService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the field contains any keywords indicating this is for a "new" or "changed" credit card
|
|
||||||
// field, we should assume that the field is not going to be autofilled.
|
|
||||||
if (this.keywordsFoundInFieldData(field, [...this.newFieldKeywords])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentForm = pageDetails.forms[field.form];
|
const parentForm = pageDetails.forms[field.form];
|
||||||
|
|
||||||
// If the field does not have a parent form
|
// If the field does not have a parent form
|
||||||
@ -229,7 +244,10 @@ export class InlineMenuFieldQualificationService
|
|||||||
* @param pageDetails - The details of the page that the field is on.
|
* @param pageDetails - The details of the page that the field is on.
|
||||||
*/
|
*/
|
||||||
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
||||||
if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
|
if (
|
||||||
|
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
|
||||||
|
this.isTotpField(field)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,10 +304,22 @@ export class InlineMenuFieldQualificationService
|
|||||||
field: AutofillField,
|
field: AutofillField,
|
||||||
pageDetails: AutofillPageDetails,
|
pageDetails: AutofillPageDetails,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
const parentForm = pageDetails.forms[field.form];
|
||||||
|
|
||||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
// 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.
|
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||||
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
||||||
return true;
|
if (!parentForm) {
|
||||||
|
return (
|
||||||
|
pageDetails.fields.filter(this.isNewPasswordField).filter((f) => f.viewable).length === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
pageDetails.fields
|
||||||
|
.filter(this.isNewPasswordField)
|
||||||
|
.filter((f) => f.viewable && f.form === field.form).length === 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameFieldsInPageDetails = pageDetails.fields.filter(this.isUsernameField);
|
const usernameFieldsInPageDetails = pageDetails.fields.filter(this.isUsernameField);
|
||||||
@ -303,7 +333,6 @@ export class InlineMenuFieldQualificationService
|
|||||||
|
|
||||||
// If the field is not structured within a form, we need to identify if the field is present on
|
// If the field is not structured within a form, we need to identify if the field is present on
|
||||||
// a page with multiple password fields. If that isn't the case, we can assume this is a login form field.
|
// a page with multiple password fields. If that isn't the case, we can assume this is a login form field.
|
||||||
const parentForm = pageDetails.forms[field.form];
|
|
||||||
if (!parentForm) {
|
if (!parentForm) {
|
||||||
// If no parent form is found, and multiple password fields are present, we should assume that
|
// If no parent form is found, and multiple password fields are present, we should assume that
|
||||||
// the passed field belongs to a user account creation form.
|
// the passed field belongs to a user account creation form.
|
||||||
@ -368,7 +397,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
|
|
||||||
// If any keywords in the field's data indicates that this is a field for a "new" or "changed"
|
// If any keywords in the field's data indicates that this is a field for a "new" or "changed"
|
||||||
// username, we should assume that this field is not for a login form.
|
// username, we should assume that this field is not for a login form.
|
||||||
if (this.keywordsFoundInFieldData(field, [...this.newFieldKeywords])) {
|
if (this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,12 +445,18 @@ export class InlineMenuFieldQualificationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the form that contains the field has more than one visible field, we should assume
|
// If the form that contains a single field, we should assume that it is part
|
||||||
// that the field is part of an account creation form.
|
// of a multistep login form.
|
||||||
const fieldsWithinForm = pageDetails.fields.filter(
|
const fieldsWithinForm = pageDetails.fields.filter(
|
||||||
(pageDetailsField) => pageDetailsField.form === field.form,
|
(pageDetailsField) => pageDetailsField.form === field.form,
|
||||||
);
|
);
|
||||||
return fieldsWithinForm.length === 1;
|
if (fieldsWithinForm.length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If multiple fields exist within the form, we should check if a single visible field exists.
|
||||||
|
// If so, we should assume that the field is part of a login form.
|
||||||
|
return fieldsWithinForm.filter((field) => field.viewable).length === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a single password field exists within the page details, and that password field is part of
|
// If a single password field exists within the page details, and that password field is part of
|
||||||
@ -439,8 +474,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no visible fields are found on the page, but we have a single password
|
// If we have a single password field we should assume that the field is part of a login form.
|
||||||
// field we should assume that the field is part of a login form.
|
|
||||||
if (passwordFieldsInPageDetails.length === 1) {
|
if (passwordFieldsInPageDetails.length === 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -814,7 +848,8 @@ export class InlineMenuFieldQualificationService
|
|||||||
isUsernameField = (field: AutofillField): boolean => {
|
isUsernameField = (field: AutofillField): boolean => {
|
||||||
if (
|
if (
|
||||||
!this.usernameFieldTypes.has(field.type) ||
|
!this.usernameFieldTypes.has(field.type) ||
|
||||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)
|
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
|
||||||
|
this.fieldHasDisqualifyingAttributeValue(field)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -854,6 +889,22 @@ export class InlineMenuFieldQualificationService
|
|||||||
return this.isPasswordField(field);
|
return this.isPasswordField(field);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a current password field for an update password form.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isUpdateCurrentPasswordField = (field: AutofillField): boolean => {
|
||||||
|
if (this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.isPasswordField(field) &&
|
||||||
|
this.keywordsFoundInFieldData(field, this.updatePasswordFieldKeywords)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the provided field as a new password field.
|
* Validates the provided field as a new password field.
|
||||||
*
|
*
|
||||||
@ -1084,6 +1135,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
autofillFieldData.title,
|
autofillFieldData.title,
|
||||||
autofillFieldData.placeholder,
|
autofillFieldData.placeholder,
|
||||||
autofillFieldData.autoCompleteType,
|
autofillFieldData.autoCompleteType,
|
||||||
|
autofillFieldData.dataSetValues,
|
||||||
autofillFieldData["label-data"],
|
autofillFieldData["label-data"],
|
||||||
autofillFieldData["label-aria"],
|
autofillFieldData["label-aria"],
|
||||||
autofillFieldData["label-left"],
|
autofillFieldData["label-left"],
|
||||||
@ -1101,7 +1153,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
keywordEl = keywordEl.replace(/-/g, "");
|
keywordEl = keywordEl.replace(/-/g, "");
|
||||||
|
|
||||||
// Split the keyword by non-alphanumeric characters to get the keywords without treating a space as a separator.
|
// Split the keyword by non-alphanumeric characters to get the keywords without treating a space as a separator.
|
||||||
keywordEl.split(/[^\p{L}\d]+/gu).forEach((keyword) => {
|
keywordEl.split(/[^\p{L}\d]+/gu).forEach((keyword: string) => {
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
keywordsSet.add(keyword);
|
keywordsSet.add(keyword);
|
||||||
}
|
}
|
||||||
@ -1111,7 +1163,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
keywordEl
|
keywordEl
|
||||||
.replace(/\s/g, "")
|
.replace(/\s/g, "")
|
||||||
.split(/[^\p{L}\d]+/gu)
|
.split(/[^\p{L}\d]+/gu)
|
||||||
.forEach((keyword) => {
|
.forEach((keyword: string) => {
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
keywordsSet.add(keyword);
|
keywordsSet.add(keyword);
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ describe("InsertAutofillContentService", () => {
|
|||||||
const domElementVisibilityService = new DomElementVisibilityService();
|
const domElementVisibilityService = new DomElementVisibilityService();
|
||||||
const autofillOverlayContentService = new AutofillOverlayContentService(
|
const autofillOverlayContentService = new AutofillOverlayContentService(
|
||||||
domQueryService,
|
domQueryService,
|
||||||
|
domElementVisibilityService,
|
||||||
inlineMenuFieldQualificationService,
|
inlineMenuFieldQualificationService,
|
||||||
);
|
);
|
||||||
const collectAutofillContentService = new CollectAutofillContentService(
|
const collectAutofillContentService = new CollectAutofillContentService(
|
||||||
@ -122,16 +123,25 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("fillForm", () => {
|
describe("fillForm", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
Object.defineProperty(globalThis, "window", {
|
||||||
|
value: { frameElement: null },
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "frameElement", {
|
||||||
|
value: { hasAttribute: jest.fn(() => false) },
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("returns early if the passed fill script does not have a script property", async () => {
|
it("returns early if the passed fill script does not have a script property", async () => {
|
||||||
fillScript.script = [];
|
fillScript.script = [];
|
||||||
jest.spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe");
|
|
||||||
jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill");
|
jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill");
|
||||||
jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill");
|
jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill");
|
||||||
jest.spyOn(insertAutofillContentService as any, "runFillScriptAction");
|
jest.spyOn(insertAutofillContentService as any, "runFillScriptAction");
|
||||||
|
|
||||||
await insertAutofillContentService.fillForm(fillScript);
|
await insertAutofillContentService.fillForm(fillScript);
|
||||||
|
|
||||||
expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).not.toHaveBeenCalled();
|
|
||||||
expect(
|
expect(
|
||||||
insertAutofillContentService["userCancelledInsecureUrlAutofill"],
|
insertAutofillContentService["userCancelledInsecureUrlAutofill"],
|
||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
@ -142,16 +152,16 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns early if the script is filling within a sand boxed iframe", async () => {
|
it("returns early if the script is filling within a sand boxed iframe", async () => {
|
||||||
jest
|
Object.defineProperty(globalThis, "frameElement", {
|
||||||
.spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe")
|
value: { hasAttribute: jest.fn(() => true) },
|
||||||
.mockReturnValue(true);
|
writable: true,
|
||||||
|
});
|
||||||
jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill");
|
jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill");
|
||||||
jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill");
|
jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill");
|
||||||
jest.spyOn(insertAutofillContentService as any, "runFillScriptAction");
|
jest.spyOn(insertAutofillContentService as any, "runFillScriptAction");
|
||||||
|
|
||||||
await insertAutofillContentService.fillForm(fillScript);
|
await insertAutofillContentService.fillForm(fillScript);
|
||||||
|
|
||||||
expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled();
|
|
||||||
expect(
|
expect(
|
||||||
insertAutofillContentService["userCancelledInsecureUrlAutofill"],
|
insertAutofillContentService["userCancelledInsecureUrlAutofill"],
|
||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
@ -162,9 +172,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns early if the autofill is occurring on an insecure url and the user cancels the autofill", async () => {
|
it("returns early if the autofill is occurring on an insecure url and the user cancels the autofill", async () => {
|
||||||
jest
|
|
||||||
.spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe")
|
|
||||||
.mockReturnValue(false);
|
|
||||||
jest
|
jest
|
||||||
.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill")
|
.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill")
|
||||||
.mockReturnValue(true);
|
.mockReturnValue(true);
|
||||||
@ -173,7 +180,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
|
|
||||||
await insertAutofillContentService.fillForm(fillScript);
|
await insertAutofillContentService.fillForm(fillScript);
|
||||||
|
|
||||||
expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled();
|
|
||||||
expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled();
|
expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
insertAutofillContentService["userCancelledUntrustedIframeAutofill"],
|
insertAutofillContentService["userCancelledUntrustedIframeAutofill"],
|
||||||
@ -182,9 +188,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns early if the iframe is untrusted and the user cancelled the autofill", async () => {
|
it("returns early if the iframe is untrusted and the user cancelled the autofill", async () => {
|
||||||
jest
|
|
||||||
.spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe")
|
|
||||||
.mockReturnValue(false);
|
|
||||||
jest
|
jest
|
||||||
.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill")
|
.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill")
|
||||||
.mockReturnValue(false);
|
.mockReturnValue(false);
|
||||||
@ -195,7 +198,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
|
|
||||||
await insertAutofillContentService.fillForm(fillScript);
|
await insertAutofillContentService.fillForm(fillScript);
|
||||||
|
|
||||||
expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled();
|
|
||||||
expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled();
|
expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
insertAutofillContentService["userCancelledUntrustedIframeAutofill"],
|
insertAutofillContentService["userCancelledUntrustedIframeAutofill"],
|
||||||
@ -204,9 +206,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs the fill script action for all scripts found within the fill script", async () => {
|
it("runs the fill script action for all scripts found within the fill script", async () => {
|
||||||
jest
|
|
||||||
.spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe")
|
|
||||||
.mockReturnValue(false);
|
|
||||||
jest
|
jest
|
||||||
.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill")
|
.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill")
|
||||||
.mockReturnValue(false);
|
.mockReturnValue(false);
|
||||||
@ -217,7 +216,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
|
|
||||||
await insertAutofillContentService.fillForm(fillScript);
|
await insertAutofillContentService.fillForm(fillScript);
|
||||||
|
|
||||||
expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled();
|
|
||||||
expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled();
|
expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
insertAutofillContentService["userCancelledUntrustedIframeAutofill"],
|
insertAutofillContentService["userCancelledUntrustedIframeAutofill"],
|
||||||
@ -244,41 +242,6 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fillingWithinSandboxedIframe", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
Object.defineProperty(globalThis, "window", {
|
|
||||||
value: { frameElement: null },
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false if the `self.origin` value is not null", () => {
|
|
||||||
const result = insertAutofillContentService["fillingWithinSandboxedIframe"]();
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
expect(self.origin).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true if the frameElement has a sandbox attribute", () => {
|
|
||||||
Object.defineProperty(globalThis, "frameElement", {
|
|
||||||
value: { hasAttribute: jest.fn(() => true) },
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = insertAutofillContentService["fillingWithinSandboxedIframe"]();
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true if the window location hostname is empty", () => {
|
|
||||||
setMockWindowLocation({ protocol: "http:", hostname: "" });
|
|
||||||
|
|
||||||
const result = insertAutofillContentService["fillingWithinSandboxedIframe"]();
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("userCancelledInsecureUrlAutofill", () => {
|
describe("userCancelledInsecureUrlAutofill", () => {
|
||||||
const currentHostname = "bitwarden.com";
|
const currentHostname = "bitwarden.com";
|
||||||
|
|
||||||
|
@ -3,15 +3,16 @@ import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants";
|
|||||||
import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script";
|
import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script";
|
||||||
import { FormFieldElement } from "../types";
|
import { FormFieldElement } from "../types";
|
||||||
import {
|
import {
|
||||||
|
currentlyInSandboxedIframe,
|
||||||
elementIsFillableFormField,
|
elementIsFillableFormField,
|
||||||
elementIsInputElement,
|
elementIsInputElement,
|
||||||
elementIsSelectElement,
|
elementIsSelectElement,
|
||||||
elementIsTextAreaElement,
|
elementIsTextAreaElement,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
|
||||||
import { InsertAutofillContentService as InsertAutofillContentServiceInterface } from "./abstractions/insert-autofill-content.service";
|
import { InsertAutofillContentService as InsertAutofillContentServiceInterface } from "./abstractions/insert-autofill-content.service";
|
||||||
import { CollectAutofillContentService } from "./collect-autofill-content.service";
|
import { CollectAutofillContentService } from "./collect-autofill-content.service";
|
||||||
import DomElementVisibilityService from "./dom-element-visibility.service";
|
|
||||||
|
|
||||||
class InsertAutofillContentService implements InsertAutofillContentServiceInterface {
|
class InsertAutofillContentService implements InsertAutofillContentServiceInterface {
|
||||||
private readonly autofillInsertActions: AutofillInsertActions = {
|
private readonly autofillInsertActions: AutofillInsertActions = {
|
||||||
@ -39,7 +40,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
async fillForm(fillScript: AutofillScript) {
|
async fillForm(fillScript: AutofillScript) {
|
||||||
if (
|
if (
|
||||||
!fillScript.script?.length ||
|
!fillScript.script?.length ||
|
||||||
this.fillingWithinSandboxedIframe() ||
|
currentlyInSandboxedIframe() ||
|
||||||
this.userCancelledInsecureUrlAutofill(fillScript.savedUrls) ||
|
this.userCancelledInsecureUrlAutofill(fillScript.savedUrls) ||
|
||||||
this.userCancelledUntrustedIframeAutofill(fillScript)
|
this.userCancelledUntrustedIframeAutofill(fillScript)
|
||||||
) {
|
) {
|
||||||
@ -50,20 +51,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
await Promise.all(fillActionPromises);
|
await Promise.all(fillActionPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifies if the execution of this script is happening
|
|
||||||
* within a sandboxed iframe.
|
|
||||||
* @returns {boolean}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private fillingWithinSandboxedIframe() {
|
|
||||||
return (
|
|
||||||
String(self.origin).toLowerCase() === "null" ||
|
|
||||||
globalThis.frameElement?.hasAttribute("sandbox") ||
|
|
||||||
globalThis.location.hostname === ""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the autofill is occurring on a page that can be considered secure. If the page is not secure,
|
* Checks if the autofill is occurring on a page that can be considered secure. If the page is not secure,
|
||||||
* the user is prompted to confirm that they want to autofill on the page.
|
* the user is prompted to confirm that they want to autofill on the page.
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
$dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord";
|
$dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord";
|
||||||
|
|
||||||
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
$font-family-source-code-pro: "Source Code Pro", monospace;
|
||||||
$font-size-base: 14px;
|
$font-size-base: 14px;
|
||||||
$text-color: #212529;
|
$text-color: #212529;
|
||||||
$muted-text-color: #6c747c;
|
$muted-text-color: #6c747c;
|
||||||
@ -11,6 +12,8 @@ $border-color-dark: #ddd;
|
|||||||
$border-radius: 3px;
|
$border-radius: 3px;
|
||||||
$focus-outline-color: #1252a3;
|
$focus-outline-color: #1252a3;
|
||||||
$muted-blue: #5a6d91;
|
$muted-blue: #5a6d91;
|
||||||
|
$password-special-color: #b80017;
|
||||||
|
$password-number-color: #1452c1;
|
||||||
|
|
||||||
$brand-primary: #175ddc;
|
$brand-primary: #175ddc;
|
||||||
|
|
||||||
@ -47,6 +50,8 @@ $themes: (
|
|||||||
successColor: $success-color-light,
|
successColor: $success-color-light,
|
||||||
errorColor: $error-color-light,
|
errorColor: $error-color-light,
|
||||||
passkeysAuthenticating: $muted-blue,
|
passkeysAuthenticating: $muted-blue,
|
||||||
|
passwordSpecialColor: $password-special-color,
|
||||||
|
passwordNumberColor: $password-number-color,
|
||||||
),
|
),
|
||||||
dark: (
|
dark: (
|
||||||
textColor: #ffffff,
|
textColor: #ffffff,
|
||||||
@ -63,6 +68,8 @@ $themes: (
|
|||||||
successColor: $success-color-dark,
|
successColor: $success-color-dark,
|
||||||
errorColor: $error-color-dark,
|
errorColor: $error-color-dark,
|
||||||
passkeysAuthenticating: #bac0ce,
|
passkeysAuthenticating: #bac0ce,
|
||||||
|
passwordSpecialColor: #ff8d85,
|
||||||
|
passwordNumberColor: #6f9df1,
|
||||||
),
|
),
|
||||||
nord: (
|
nord: (
|
||||||
textColor: $nord5,
|
textColor: $nord5,
|
||||||
@ -78,6 +85,8 @@ $themes: (
|
|||||||
focusOutlineColor: lighten($focus-outline-color, 25%),
|
focusOutlineColor: lighten($focus-outline-color, 25%),
|
||||||
successColor: $success-color-dark,
|
successColor: $success-color-dark,
|
||||||
passkeysAuthenticating: $nord4,
|
passkeysAuthenticating: $nord4,
|
||||||
|
passwordSpecialColor: $nord12,
|
||||||
|
passwordNumberColor: $nord8,
|
||||||
),
|
),
|
||||||
solarizedDark: (
|
solarizedDark: (
|
||||||
textColor: $solarizedDarkBase2,
|
textColor: $solarizedDarkBase2,
|
||||||
@ -94,6 +103,8 @@ $themes: (
|
|||||||
focusOutlineColor: lighten($focus-outline-color, 15%),
|
focusOutlineColor: lighten($focus-outline-color, 15%),
|
||||||
successColor: $success-color-dark,
|
successColor: $success-color-dark,
|
||||||
passkeysAuthenticating: $solarizedDarkBase2,
|
passkeysAuthenticating: $solarizedDarkBase2,
|
||||||
|
passwordSpecialColor: #b58900,
|
||||||
|
passwordNumberColor: $solarizedDarkCyan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -156,11 +156,9 @@ export function createAutofillScriptMock(
|
|||||||
|
|
||||||
const overlayPagesTranslations = {
|
const overlayPagesTranslations = {
|
||||||
locale: "en",
|
locale: "en",
|
||||||
buttonPageTitle: "buttonPageTitle",
|
|
||||||
listPageTitle: "listPageTitle",
|
|
||||||
opensInANewWindow: "opensInANewWindow",
|
opensInANewWindow: "opensInANewWindow",
|
||||||
toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay",
|
toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay",
|
||||||
unlockYourAccount: "unlockYourAccount",
|
unlockYourAccountToViewAutofillSuggestions: "unlockYourAccountToViewAutofillSuggestions",
|
||||||
unlockAccount: "unlockAccount",
|
unlockAccount: "unlockAccount",
|
||||||
fillCredentialsFor: "fillCredentialsFor",
|
fillCredentialsFor: "fillCredentialsFor",
|
||||||
username: "username",
|
username: "username",
|
||||||
@ -215,7 +213,7 @@ export function createInitAutofillInlineMenuListMessageMock(
|
|||||||
theme: ThemeType.Light,
|
theme: ThemeType.Light,
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
portKey: "portKey",
|
portKey: "portKey",
|
||||||
filledByCipherType: CipherType.Login,
|
inlineMenuFillType: CipherType.Login,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
createAutofillOverlayCipherDataMock(1, {
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
icon: {
|
icon: {
|
||||||
@ -264,7 +262,7 @@ export function createFocusedFieldDataMock(
|
|||||||
paddingRight: "6px",
|
paddingRight: "6px",
|
||||||
paddingLeft: "6px",
|
paddingLeft: "6px",
|
||||||
},
|
},
|
||||||
filledByCipherType: CipherType.Login,
|
inlineMenuFillType: CipherType.Login,
|
||||||
tabId: 1,
|
tabId: 1,
|
||||||
frameId: 2,
|
frameId: 2,
|
||||||
...customFields,
|
...customFields,
|
||||||
|
@ -165,7 +165,7 @@ export function triggerWebRequestOnBeforeRedirectEvent(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function triggerWebRequestOnCompletedEvent(details: chrome.webRequest.WebRequestDetails) {
|
export function triggerWebRequestOnCompletedEvent(details: chrome.webRequest.WebResponseDetails) {
|
||||||
(chrome.webRequest.onCompleted.addListener as unknown as jest.SpyInstance).mock.calls.forEach(
|
(chrome.webRequest.onCompleted.addListener as unknown as jest.SpyInstance).mock.calls.forEach(
|
||||||
(call) => {
|
(call) => {
|
||||||
const callback = call[0];
|
const callback = call[0];
|
||||||
|
@ -323,6 +323,10 @@ export function nodeIsButtonElement(node: Node): node is HTMLButtonElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function nodeIsAnchorElement(node: Node): node is HTMLAnchorElement {
|
||||||
|
return nodeIsElement(node) && elementIsInstanceOf<HTMLAnchorElement>(node, "a");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a boolean representing the attribute value of an element.
|
* Returns a boolean representing the attribute value of an element.
|
||||||
*
|
*
|
||||||
@ -378,12 +382,26 @@ export function throttle(callback: (_args: any) => any, limit: number) {
|
|||||||
*
|
*
|
||||||
* @param callback - The callback function to debounce.
|
* @param callback - The callback function to debounce.
|
||||||
* @param delay - The time in milliseconds to debounce the callback.
|
* @param delay - The time in milliseconds to debounce the callback.
|
||||||
|
* @param immediate - Determines whether the callback should run immediately.
|
||||||
*/
|
*/
|
||||||
export function debounce(callback: (_args: any) => any, delay: number) {
|
export function debounce(callback: (_args: any) => any, delay: number, immediate?: boolean) {
|
||||||
let timeout: NodeJS.Timeout;
|
let timeout: NodeJS.Timeout;
|
||||||
return function (...args: unknown[]) {
|
return function (...args: unknown[]) {
|
||||||
globalThis.clearTimeout(timeout);
|
const callImmediately = !!immediate && !timeout;
|
||||||
timeout = globalThis.setTimeout(() => callback.apply(this, args), delay);
|
|
||||||
|
if (timeout) {
|
||||||
|
globalThis.clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
timeout = globalThis.setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
if (!callImmediately) {
|
||||||
|
callback.apply(this, args);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
if (callImmediately) {
|
||||||
|
callback.apply(this, args);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,3 +491,54 @@ export function generateDomainMatchPatterns(url: string): string[] {
|
|||||||
export function isInvalidResponseStatusCode(statusCode: number) {
|
export function isInvalidResponseStatusCode(statusCode: number) {
|
||||||
return statusCode < 200 || statusCode >= 300;
|
return statusCode < 200 || statusCode >= 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current context is within a sandboxed iframe.
|
||||||
|
*/
|
||||||
|
export function currentlyInSandboxedIframe(): boolean {
|
||||||
|
return (
|
||||||
|
String(self.origin).toLowerCase() === "null" ||
|
||||||
|
globalThis.frameElement?.hasAttribute("sandbox") ||
|
||||||
|
globalThis.location.hostname === ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object allows us to map a special character to a key name. The key name is used
|
||||||
|
* in gathering the i18n translation of the written version of the special character.
|
||||||
|
*/
|
||||||
|
export const specialCharacterToKeyMap: Record<string, string> = {
|
||||||
|
" ": "spaceCharacterDescriptor",
|
||||||
|
"~": "tildeCharacterDescriptor",
|
||||||
|
"`": "backtickCharacterDescriptor",
|
||||||
|
"!": "exclamationCharacterDescriptor",
|
||||||
|
"@": "atSignCharacterDescriptor",
|
||||||
|
"#": "hashSignCharacterDescriptor",
|
||||||
|
$: "dollarSignCharacterDescriptor",
|
||||||
|
"%": "percentSignCharacterDescriptor",
|
||||||
|
"^": "caretCharacterDescriptor",
|
||||||
|
"&": "ampersandCharacterDescriptor",
|
||||||
|
"*": "asteriskCharacterDescriptor",
|
||||||
|
"(": "parenLeftCharacterDescriptor",
|
||||||
|
")": "parenRightCharacterDescriptor",
|
||||||
|
"-": "hyphenCharacterDescriptor",
|
||||||
|
_: "underscoreCharacterDescriptor",
|
||||||
|
"+": "plusCharacterDescriptor",
|
||||||
|
"=": "equalsCharacterDescriptor",
|
||||||
|
"{": "braceLeftCharacterDescriptor",
|
||||||
|
"}": "braceRightCharacterDescriptor",
|
||||||
|
"[": "bracketLeftCharacterDescriptor",
|
||||||
|
"]": "bracketRightCharacterDescriptor",
|
||||||
|
"|": "pipeCharacterDescriptor",
|
||||||
|
"\\": "backSlashCharacterDescriptor",
|
||||||
|
":": "colonCharacterDescriptor",
|
||||||
|
";": "semicolonCharacterDescriptor",
|
||||||
|
'"': "doubleQuoteCharacterDescriptor",
|
||||||
|
"'": "singleQuoteCharacterDescriptor",
|
||||||
|
"<": "lessThanCharacterDescriptor",
|
||||||
|
">": "greaterThanCharacterDescriptor",
|
||||||
|
",": "commaCharacterDescriptor",
|
||||||
|
".": "periodCharacterDescriptor",
|
||||||
|
"?": "questionCharacterDescriptor",
|
||||||
|
"/": "forwardSlashCharacterDescriptor",
|
||||||
|
};
|
||||||
|
@ -30,3 +30,9 @@ export const circleCheckIcon =
|
|||||||
|
|
||||||
export const spinnerIcon =
|
export const spinnerIcon =
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><g clip-path="url(#a)"><path fill="#5A6D91" d="M4.869 15.015a.588.588 0 1 0 0-1.177.588.588 0 0 0 0 1.177ZM8.252 16a.588.588 0 1 0 0-1.176.588.588 0 0 0 0 1.176Zm3.683-.911a.589.589 0 1 0 0-1.177.589.589 0 0 0 0 1.177ZM2.43 12.882a.693.693 0 1 0 0-1.387.693.693 0 0 0 0 1.387ZM1.318 9.738a.82.82 0 1 0 0-1.64.82.82 0 0 0 0 1.64Zm.69-3.578a.968.968 0 1 0 0-1.937.968.968 0 0 0 0 1.937ZM4.81 3.337a1.175 1.175 0 1 0 0-2.35 1.175 1.175 0 0 0 0 2.35Zm4.597-.676a1.33 1.33 0 1 0 0-2.661 1.33 1.33 0 0 0 0 2.66Zm4.543 2.954a1.553 1.553 0 1 0 0-3.105 1.553 1.553 0 0 0 0 3.105Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>';
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><g clip-path="url(#a)"><path fill="#5A6D91" d="M4.869 15.015a.588.588 0 1 0 0-1.177.588.588 0 0 0 0 1.177ZM8.252 16a.588.588 0 1 0 0-1.176.588.588 0 0 0 0 1.176Zm3.683-.911a.589.589 0 1 0 0-1.177.589.589 0 0 0 0 1.177ZM2.43 12.882a.693.693 0 1 0 0-1.387.693.693 0 0 0 0 1.387ZM1.318 9.738a.82.82 0 1 0 0-1.64.82.82 0 0 0 0 1.64Zm.69-3.578a.968.968 0 1 0 0-1.937.968.968 0 0 0 0 1.937ZM4.81 3.337a1.175 1.175 0 1 0 0-2.35 1.175 1.175 0 0 0 0 2.35Zm4.597-.676a1.33 1.33 0 1 0 0-2.661 1.33 1.33 0 0 0 0 2.66Zm4.543 2.954a1.553 1.553 0 1 0 0-3.105 1.553 1.553 0 0 0 0 3.105Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>';
|
||||||
|
|
||||||
|
export const keyIcon =
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M21.803 3.035a7.453 7.453 0 0 0-2.427-1.567 7.763 7.763 0 0 0-2.877-.551c-.988 0-1.967.187-2.878.55a7.455 7.455 0 0 0-2.427 1.568A7.193 7.193 0 0 0 9.283 6.23a6.936 6.936 0 0 0-.023 3.675.556.556 0 0 1-.16.549L.656 18.61a.77.77 0 0 0-.233.468l-.415 3.756a.722.722 0 0 0 .04.354.773.773 0 0 0 .203.3.85.85 0 0 0 .697.201l5.141-.855a.832.832 0 0 0 .461-.241.757.757 0 0 0 .211-.458l.108-1.162a.554.554 0 0 1 .17-.35.62.62 0 0 1 .365-.167l1.2-.105a.832.832 0 0 0 .503-.23.756.756 0 0 0 .23-.482l.124-1.326a.361.361 0 0 1 .111-.23.4.4 0 0 1 .24-.108l1.381-.113a.815.815 0 0 0 .501-.225l2.473-2.386a.506.506 0 0 1 .48-.126 7.904 7.904 0 0 0 1.912.235 7.68 7.68 0 0 0 2.846-.539 7.344 7.344 0 0 0 2.402-1.546C23.213 11.905 24 10.069 24 8.155c0-1.914-.787-3.752-2.194-5.122l-.003.002Zm-10.81 7.148a5.496 5.496 0 0 1-.25-3.208 5.677 5.677 0 0 1 1.6-2.835 5.828 5.828 0 0 1 1.902-1.233 6.075 6.075 0 0 1 4.515 0 5.829 5.829 0 0 1 1.902 1.233c1.107 1.073 1.726 2.514 1.726 4.016 0 1.501-.62 2.943-1.726 4.016a5.925 5.925 0 0 1-2.93 1.537 6.135 6.135 0 0 1-3.339-.245.844.844 0 0 0-.85.182l-2.498 2.409a1.124 1.124 0 0 1-.682.308l-1.687.142a.839.839 0 0 0-.503.23.754.754 0 0 0-.23.482l-.105 1.13a.594.594 0 0 1-.181.374.653.653 0 0 1-.39.178l-1.171.1a.832.832 0 0 0-.503.23.755.755 0 0 0-.23.483l-.122 1.313a.474.474 0 0 1-.13.287.518.518 0 0 1-.288.151l-2.66.439a.36.36 0 0 1-.286-.084.314.314 0 0 1-.102-.266l.182-1.758a.724.724 0 0 1 .222-.449l8.636-8.333a.778.778 0 0 0 .215-.39.756.756 0 0 0-.036-.439h-.001Zm6.976-1.226c-.474 0-.938-.134-1.332-.384a2.31 2.31 0 0 1-.884-1.022 2.17 2.17 0 0 1-.137-1.317c.093-.442.321-.848.657-1.166a2.441 2.441 0 0 1 1.228-.624 2.516 2.516 0 0 1 1.386.13 2.37 2.37 0 0 1 1.077.84c.263.374.404.814.404 1.265 0 .605-.253 1.184-.703 1.611-.45.428-1.06.667-1.696.667Zm0-3.56c-.266 0-.527.075-.75.216-.221.14-.394.34-.496.575a1.22 1.22 0 0 0-.077.74c.053.249.18.477.37.657.189.18.43.3.691.35.262.05.533.025.78-.072.246-.097.457-.261.606-.472a1.235 1.235 0 0 0-.168-1.619 1.369 1.369 0 0 0-.954-.376v.002l-.002-.001Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .308h24v24H0z"/></clipPath></defs></svg>';
|
||||||
|
|
||||||
|
export const refreshIcon =
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M18.383 11.37a.678.678 0 0 0-.496.086.65.65 0 0 0-.291.402 7.457 7.457 0 0 1-2.451 3.912 7.754 7.754 0 0 1-4.328 1.78 7.761 7.761 0 0 1-4.554-.901 7.502 7.502 0 0 1-3.167-3.318c-.025-.064.03-.159.165-.14l1.039.417a.687.687 0 0 0 .51.005.662.662 0 0 0 .365-.346.62.62 0 0 0-.142-.694.64.64 0 0 0-.214-.136l-2.656-1.061a.686.686 0 0 0-.854.31L.065 14.139a.621.621 0 0 0 .31.847.69.69 0 0 0 .639-.033.653.653 0 0 0 .247-.261l.4-.792a.167.167 0 0 1 .124-.077.173.173 0 0 1 .075.01.16.16 0 0 1 .063.04 8.813 8.813 0 0 0 3.29 3.627 9.109 9.109 0 0 0 4.764 1.358c.312 0 .632-.015.961-.044a9.223 9.223 0 0 0 5.065-2.116 8.871 8.871 0 0 0 2.89-4.578.628.628 0 0 0-.274-.656.655.655 0 0 0-.236-.095v.001Zm1.25-5.735a.693.693 0 0 0-.64.033.659.659 0 0 0-.247.262l-.4.79a.166.166 0 0 1-.261.028 8.809 8.809 0 0 0-3.29-3.63 9.113 9.113 0 0 0-4.764-1.36c-.311 0-.631.014-.961.045A9.224 9.224 0 0 0 4.004 3.92a8.863 8.863 0 0 0-2.89 4.58.622.622 0 0 0 .276.658.657.657 0 0 0 .237.094c.17.036.349.005.496-.086a.65.65 0 0 0 .29-.402 7.452 7.452 0 0 1 2.452-3.911 7.764 7.764 0 0 1 4.328-1.781 7.761 7.761 0 0 1 4.553.902 7.508 7.508 0 0 1 3.168 3.317c.023.063-.03.16-.165.138l-1.042-.42a.688.688 0 0 0-.509-.004.666.666 0 0 0-.367.345.622.622 0 0 0 .357.83l2.65 1.06c.156.064.33.067.489.01a.665.665 0 0 0 .365-.318l1.243-2.454a.622.622 0 0 0-.302-.843Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .421h20v19.773H0z"/></clipPath></defs></svg>';
|
||||||
|
@ -3,7 +3,6 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { ExtensionCommand, ExtensionCommandType } from "@bitwarden/common/autofill/constants";
|
import { ExtensionCommand, ExtensionCommandType } from "@bitwarden/common/autofill/constants";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
|
||||||
|
|
||||||
import { openUnlockPopout } from "../auth/popup/utils/auth-popout-window";
|
import { openUnlockPopout } from "../auth/popup/utils/auth-popout-window";
|
||||||
import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
|
import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
|
||||||
@ -17,10 +16,10 @@ export default class CommandsBackground {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private main: MainBackground,
|
private main: MainBackground,
|
||||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private generatePasswordToClipboard: () => Promise<void>,
|
||||||
) {
|
) {
|
||||||
this.isSafari = this.platformUtilsService.isSafari();
|
this.isSafari = this.platformUtilsService.isSafari();
|
||||||
this.isVivaldi = this.platformUtilsService.isVivaldi();
|
this.isVivaldi = this.platformUtilsService.isVivaldi();
|
||||||
@ -77,13 +76,6 @@ export default class CommandsBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generatePasswordToClipboard() {
|
|
||||||
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
|
||||||
const password = await this.passwordGenerationService.generatePassword(options);
|
|
||||||
this.platformUtilsService.copyToClipboard(password);
|
|
||||||
await this.passwordGenerationService.addHistory(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async triggerAutofillCommand(
|
private async triggerAutofillCommand(
|
||||||
tab?: chrome.tabs.Tab,
|
tab?: chrome.tabs.Tab,
|
||||||
commandSender?: ExtensionCommandType,
|
commandSender?: ExtensionCommandType,
|
||||||
|
@ -233,6 +233,7 @@ import { Fido2Background } from "../autofill/fido2/background/fido2.background";
|
|||||||
import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service";
|
import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||||
import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service";
|
import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service";
|
||||||
import AutofillService from "../autofill/services/autofill.service";
|
import AutofillService from "../autofill/services/autofill.service";
|
||||||
|
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
|
||||||
import { SafariApp } from "../browser/safariApp";
|
import { SafariApp } from "../browser/safariApp";
|
||||||
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
||||||
import { BrowserKeyService } from "../key-management/browser-key.service";
|
import { BrowserKeyService } from "../key-management/browser-key.service";
|
||||||
@ -1153,10 +1154,10 @@ export default class MainBackground {
|
|||||||
);
|
);
|
||||||
this.commandsBackground = new CommandsBackground(
|
this.commandsBackground = new CommandsBackground(
|
||||||
this,
|
this,
|
||||||
this.passwordGenerationService,
|
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.vaultTimeoutService,
|
this.vaultTimeoutService,
|
||||||
this.authService,
|
this.authService,
|
||||||
|
() => this.generatePasswordToClipboard(),
|
||||||
);
|
);
|
||||||
this.notificationBackground = new NotificationBackground(
|
this.notificationBackground = new NotificationBackground(
|
||||||
this.autofillService,
|
this.autofillService,
|
||||||
@ -1201,14 +1202,7 @@ export default class MainBackground {
|
|||||||
|
|
||||||
const contextMenuClickedHandler = new ContextMenuClickedHandler(
|
const contextMenuClickedHandler = new ContextMenuClickedHandler(
|
||||||
(options) => this.platformUtilsService.copyToClipboard(options.text),
|
(options) => this.platformUtilsService.copyToClipboard(options.text),
|
||||||
async (_tab) => {
|
async () => this.generatePasswordToClipboard(),
|
||||||
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
|
||||||
const password = await this.passwordGenerationService.generatePassword(options);
|
|
||||||
this.platformUtilsService.copyToClipboard(password);
|
|
||||||
// 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.passwordGenerationService.addHistory(password);
|
|
||||||
},
|
|
||||||
async (tab, cipher) => {
|
async (tab, cipher) => {
|
||||||
this.loginToAutoFill = cipher;
|
this.loginToAutoFill = cipher;
|
||||||
if (tab == null) {
|
if (tab == null) {
|
||||||
@ -1665,6 +1659,7 @@ export default class MainBackground {
|
|||||||
this.themeStateService,
|
this.themeStateService,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
this.overlayBackground = new OverlayBackground(
|
this.overlayBackground = new OverlayBackground(
|
||||||
this.logService,
|
this.logService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
@ -1677,7 +1672,10 @@ export default class MainBackground {
|
|||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.vaultSettingsService,
|
this.vaultSettingsService,
|
||||||
this.fido2ActiveRequestManager,
|
this.fido2ActiveRequestManager,
|
||||||
|
inlineMenuFieldQualificationService,
|
||||||
this.themeStateService,
|
this.themeStateService,
|
||||||
|
() => this.generatePassword(),
|
||||||
|
(password) => this.addPasswordToHistory(password),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1690,4 +1688,19 @@ export default class MainBackground {
|
|||||||
await this.overlayBackground.init();
|
await this.overlayBackground.init();
|
||||||
await this.tabsBackground.init();
|
await this.tabsBackground.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generatePassword = async (): Promise<string> => {
|
||||||
|
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
||||||
|
return await this.passwordGenerationService.generatePassword(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
generatePasswordToClipboard = async () => {
|
||||||
|
const password = await this.generatePassword();
|
||||||
|
this.platformUtilsService.copyToClipboard(password);
|
||||||
|
await this.addPasswordToHistory(password);
|
||||||
|
};
|
||||||
|
|
||||||
|
addPasswordToHistory = async (password: string) => {
|
||||||
|
await this.passwordGenerationService.addHistory(password);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export const EVENTS = {
|
|||||||
MOUSEENTER: "mouseenter",
|
MOUSEENTER: "mouseenter",
|
||||||
MOUSELEAVE: "mouseleave",
|
MOUSELEAVE: "mouseleave",
|
||||||
MOUSEUP: "mouseup",
|
MOUSEUP: "mouseup",
|
||||||
|
MOUSEOUT: "mouseout",
|
||||||
SUBMIT: "submit",
|
SUBMIT: "submit",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user