mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[PM-934] Autofill not working until page has been refreshed (#6826)
* [PM-934] Autofill not working until page has been refreshed * [PM-934] Adjusting cleanup of the messages_handler script * [PM-934] Fixing small issue found within collection of page details * [PM-934] Addressing concenrs brought up during code review * [PM-934] Addressing concenrs brought up during code review * [PM-934] Addressing concenrs brought up during code review * [PM-934] Addressing concenrs brought up during code review * [PM-934] Applying re-set changes to the autofill overlay implementation on reset of the extension * [PM-934] Applying jest tests to added logic within AutofillOverlayContent service * [PM-934] Fixing typo present in tabs background listener * [PM-934] Finishing up jest tests for updated implementation * [PM-934] Incorporating methodology for ensuring the autofill overlay updates to reflect user settings within existing tabs * [PM-934] Refining implementation to ensure we do not unnecessarily re-inject content scripts when the autofill overlay settings change * [PM-934] Working through jest tests for added implementation details * [PM-934] Working through jest tests for added implementation details * [PM-934] Finalizing jest tests for implemented logic * [PM-5035] Refactoring method structure
This commit is contained in:
parent
7051f255ed
commit
bf60711efe
@ -673,7 +673,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
*/
|
||||
private setupExtensionMessageListeners() {
|
||||
BrowserApi.messageListener("overlay.background", this.handleExtensionMessage);
|
||||
chrome.runtime.onConnect.addListener(this.handlePortOnConnect);
|
||||
BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,6 +10,10 @@ import {
|
||||
settingsServiceFactory,
|
||||
SettingsServiceInitOptions,
|
||||
} from "../../../background/service-factories/settings-service.factory";
|
||||
import {
|
||||
configServiceFactory,
|
||||
ConfigServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/config-service.factory";
|
||||
import {
|
||||
CachedServices,
|
||||
factory,
|
||||
@ -43,7 +47,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
|
||||
EventCollectionServiceInitOptions &
|
||||
LogServiceInitOptions &
|
||||
SettingsServiceInitOptions &
|
||||
UserVerificationServiceInitOptions;
|
||||
UserVerificationServiceInitOptions &
|
||||
ConfigServiceInitOptions;
|
||||
|
||||
export function autofillServiceFactory(
|
||||
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
|
||||
@ -62,6 +67,7 @@ export function autofillServiceFactory(
|
||||
await logServiceFactory(cache, opts),
|
||||
await settingsServiceFactory(cache, opts),
|
||||
await userVerificationServiceFactory(cache, opts),
|
||||
await configServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import OverlayBackground from "./overlay.background";
|
||||
import TabsBackground from "./tabs.background";
|
||||
|
||||
describe("TabsBackground", () => {
|
||||
let tabsBackgorund: TabsBackground;
|
||||
let tabsBackground: TabsBackground;
|
||||
const mainBackground = mock<MainBackground>({
|
||||
messagingService: {
|
||||
send: jest.fn(),
|
||||
@ -25,7 +25,7 @@ describe("TabsBackground", () => {
|
||||
const overlayBackground = mock<OverlayBackground>();
|
||||
|
||||
beforeEach(() => {
|
||||
tabsBackgorund = new TabsBackground(mainBackground, notificationBackground, overlayBackground);
|
||||
tabsBackground = new TabsBackground(mainBackground, notificationBackground, overlayBackground);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -35,11 +35,11 @@ describe("TabsBackground", () => {
|
||||
describe("init", () => {
|
||||
it("sets up a window on focusChanged listener", () => {
|
||||
const handleWindowOnFocusChangedSpy = jest.spyOn(
|
||||
tabsBackgorund as any,
|
||||
tabsBackground as any,
|
||||
"handleWindowOnFocusChanged",
|
||||
);
|
||||
|
||||
tabsBackgorund.init();
|
||||
tabsBackground.init();
|
||||
|
||||
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
|
||||
handleWindowOnFocusChangedSpy,
|
||||
@ -49,7 +49,7 @@ describe("TabsBackground", () => {
|
||||
|
||||
describe("tab event listeners", () => {
|
||||
beforeEach(() => {
|
||||
tabsBackgorund.init();
|
||||
tabsBackground["setupTabEventListeners"]();
|
||||
});
|
||||
|
||||
describe("window onFocusChanged event", () => {
|
||||
@ -64,7 +64,7 @@ describe("TabsBackground", () => {
|
||||
triggerWindowOnFocusedChangedEvent(10);
|
||||
await flushPromises();
|
||||
|
||||
expect(tabsBackgorund["focusedWindowId"]).toBe(10);
|
||||
expect(tabsBackground["focusedWindowId"]).toBe(10);
|
||||
});
|
||||
|
||||
it("updates the current tab data", async () => {
|
||||
@ -144,7 +144,7 @@ describe("TabsBackground", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mainBackground.onUpdatedRan = false;
|
||||
tabsBackgorund["focusedWindowId"] = focusedWindowId;
|
||||
tabsBackground["focusedWindowId"] = focusedWindowId;
|
||||
tab = mock<chrome.tabs.Tab>({
|
||||
windowId: focusedWindowId,
|
||||
active: true,
|
||||
|
@ -20,6 +20,14 @@ export default class TabsBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateCurrentTabData();
|
||||
this.setupTabEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the tab and window event listeners.
|
||||
*/
|
||||
private setupTabEventListeners() {
|
||||
chrome.windows.onFocusChanged.addListener(this.handleWindowOnFocusChanged);
|
||||
chrome.tabs.onActivated.addListener(this.handleTabOnActivated);
|
||||
chrome.tabs.onReplaced.addListener(this.handleTabOnReplaced);
|
||||
@ -33,7 +41,7 @@ export default class TabsBackground {
|
||||
* @param windowId - The ID of the window that was focused.
|
||||
*/
|
||||
private handleWindowOnFocusChanged = async (windowId: number) => {
|
||||
if (!windowId) {
|
||||
if (windowId == null || windowId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -116,8 +124,10 @@ export default class TabsBackground {
|
||||
* for the current tab. Also updates the overlay ciphers.
|
||||
*/
|
||||
private updateCurrentTabData = async () => {
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu();
|
||||
await this.overlayBackground.updateOverlayCiphers();
|
||||
await Promise.all([
|
||||
this.main.refreshBadge(),
|
||||
this.main.refreshMenu(),
|
||||
this.overlayBackground.updateOverlayCiphers(),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ type AutofillExtensionMessage = {
|
||||
direction?: "previous" | "next";
|
||||
isOpeningFullOverlay?: boolean;
|
||||
forceCloseOverlay?: boolean;
|
||||
autofillOverlayVisibility?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@ -34,10 +35,12 @@ type AutofillExtensionMessageHandlers = {
|
||||
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
|
||||
bgUnlockPopoutOpened: () => void;
|
||||
bgVaultItemRepromptPopoutOpened: () => void;
|
||||
updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void;
|
||||
};
|
||||
|
||||
interface AutofillInit {
|
||||
init(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit };
|
||||
|
@ -6,7 +6,7 @@ import { flushPromises, sendExtensionRuntimeMessage } from "../jest/testing-util
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import AutofillScript from "../models/autofill-script";
|
||||
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
||||
import { RedirectFocusDirection } from "../utils/autofill-overlay.enum";
|
||||
import { AutofillOverlayVisibility, RedirectFocusDirection } from "../utils/autofill-overlay.enum";
|
||||
|
||||
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
||||
import AutofillInit from "./autofill-init";
|
||||
@ -16,6 +16,11 @@ describe("AutofillInit", () => {
|
||||
const autofillOverlayContentService = mock<AutofillOverlayContentService>();
|
||||
|
||||
beforeEach(() => {
|
||||
chrome.runtime.connect = jest.fn().mockReturnValue({
|
||||
onDisconnect: {
|
||||
addListener: jest.fn(),
|
||||
},
|
||||
});
|
||||
autofillInit = new AutofillInit(autofillOverlayContentService);
|
||||
});
|
||||
|
||||
@ -477,6 +482,57 @@ describe("AutofillInit", () => {
|
||||
expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateAutofillOverlayVisibility", () => {
|
||||
beforeEach(() => {
|
||||
autofillInit["autofillOverlayContentService"].autofillOverlayVisibility =
|
||||
AutofillOverlayVisibility.OnButtonClick;
|
||||
});
|
||||
|
||||
it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => {
|
||||
sendExtensionRuntimeMessage({
|
||||
command: "updateAutofillOverlayVisibility",
|
||||
data: {},
|
||||
});
|
||||
|
||||
expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
|
||||
AutofillOverlayVisibility.OnButtonClick,
|
||||
);
|
||||
});
|
||||
|
||||
it("updates the overlay visibility value", () => {
|
||||
const message = {
|
||||
command: "updateAutofillOverlayVisibility",
|
||||
data: {
|
||||
autofillOverlayVisibility: AutofillOverlayVisibility.Off,
|
||||
},
|
||||
};
|
||||
|
||||
sendExtensionRuntimeMessage(message);
|
||||
|
||||
expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
|
||||
message.data.autofillOverlayVisibility,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
it("removes the extension message listeners", () => {
|
||||
autofillInit.destroy();
|
||||
|
||||
expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith(
|
||||
autofillInit["handleExtensionMessage"],
|
||||
);
|
||||
});
|
||||
|
||||
it("destroys the collectAutofillContentService", () => {
|
||||
jest.spyOn(autofillInit["collectAutofillContentService"], "destroy");
|
||||
|
||||
autofillInit.destroy();
|
||||
|
||||
expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -26,6 +26,7 @@ class AutofillInit implements AutofillInitInterface {
|
||||
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
|
||||
bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(),
|
||||
bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(),
|
||||
updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message),
|
||||
};
|
||||
|
||||
/**
|
||||
@ -214,6 +215,19 @@ class AutofillInit implements AutofillInitInterface {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the autofill overlay visibility.
|
||||
*
|
||||
* @param data - Contains the autoFillOverlayVisibility value
|
||||
*/
|
||||
private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) {
|
||||
if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the extension message listeners for the content script.
|
||||
*/
|
||||
@ -247,6 +261,16 @@ class AutofillInit implements AutofillInitInterface {
|
||||
Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles destroying the autofill init content script. Removes all
|
||||
* listeners, timeouts, and object instances to prevent memory leaks.
|
||||
*/
|
||||
destroy() {
|
||||
chrome.runtime.onMessage.removeListener(this.handleExtensionMessage);
|
||||
this.collectAutofillContentService.destroy();
|
||||
this.autofillOverlayContentService?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export default AutofillInit;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils";
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", loadAutofiller);
|
||||
} else {
|
||||
@ -8,27 +10,30 @@ function loadAutofiller() {
|
||||
let pageHref: string = null;
|
||||
let filledThisHref = false;
|
||||
let delayFillTimeout: number;
|
||||
|
||||
const activeUserIdKey = "activeUserId";
|
||||
let activeUserId: string;
|
||||
|
||||
chrome.storage.local.get(activeUserIdKey, (obj: any) => {
|
||||
if (obj == null || obj[activeUserIdKey] == null) {
|
||||
return;
|
||||
}
|
||||
activeUserId = obj[activeUserIdKey];
|
||||
});
|
||||
|
||||
chrome.storage.local.get(activeUserId, (obj: any) => {
|
||||
if (obj?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) {
|
||||
setInterval(() => doFillIfNeeded(), 500);
|
||||
}
|
||||
});
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.command === "fillForm" && pageHref === msg.url) {
|
||||
let doFillInterval: NodeJS.Timeout;
|
||||
const handleExtensionDisconnect = () => {
|
||||
clearDoFillInterval();
|
||||
clearDelayFillTimeout();
|
||||
};
|
||||
const handleExtensionMessage = (message: any) => {
|
||||
if (message.command === "fillForm" && pageHref === message.url) {
|
||||
filledThisHref = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setupExtensionEventListeners();
|
||||
triggerUserFillOnLoad();
|
||||
|
||||
async function triggerUserFillOnLoad() {
|
||||
const activeUserIdKey = "activeUserId";
|
||||
const userKeyStorage = await getFromLocalStorage(activeUserIdKey);
|
||||
const activeUserId = userKeyStorage[activeUserIdKey];
|
||||
const activeUserStorage = await getFromLocalStorage(activeUserId);
|
||||
if (activeUserStorage?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) {
|
||||
clearDoFillInterval();
|
||||
doFillInterval = setInterval(() => doFillIfNeeded(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
function doFillIfNeeded(force = false) {
|
||||
if (force || pageHref !== window.location.href) {
|
||||
@ -36,9 +41,7 @@ function loadAutofiller() {
|
||||
// Some websites are slow and rendering all page content. Try to fill again later
|
||||
// if we haven't already.
|
||||
filledThisHref = false;
|
||||
if (delayFillTimeout != null) {
|
||||
window.clearTimeout(delayFillTimeout);
|
||||
}
|
||||
clearDelayFillTimeout();
|
||||
delayFillTimeout = window.setTimeout(() => {
|
||||
if (!filledThisHref) {
|
||||
doFillIfNeeded(true);
|
||||
@ -55,4 +58,21 @@ function loadAutofiller() {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function clearDoFillInterval() {
|
||||
if (doFillInterval) {
|
||||
window.clearInterval(doFillInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function clearDelayFillTimeout() {
|
||||
if (delayFillTimeout) {
|
||||
window.clearTimeout(delayFillTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
function setupExtensionEventListeners() {
|
||||
setupExtensionDisconnectAction(handleExtensionDisconnect);
|
||||
chrome.runtime.onMessage.addListener(handleExtensionMessage);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
||||
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||
|
||||
import AutofillInit from "./autofill-init";
|
||||
|
||||
@ -6,6 +7,8 @@ import AutofillInit from "./autofill-init";
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
const autofillOverlayContentService = new AutofillOverlayContentService();
|
||||
windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService);
|
||||
setupAutofillInitDisconnectAction(windowContext);
|
||||
|
||||
windowContext.bitwardenAutofillInit.init();
|
||||
}
|
||||
})(window);
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||
|
||||
import AutofillInit from "./autofill-init";
|
||||
|
||||
(function (windowContext) {
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
windowContext.bitwardenAutofillInit = new AutofillInit();
|
||||
setupAutofillInitDisconnectAction(windowContext);
|
||||
|
||||
windowContext.bitwardenAutofillInit.init();
|
||||
}
|
||||
})(window);
|
||||
|
@ -1,31 +1,4 @@
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.command && event.data.command === "authResult") {
|
||||
chrome.runtime.sendMessage({
|
||||
command: event.data.command,
|
||||
code: event.data.code,
|
||||
state: event.data.state,
|
||||
lastpass: event.data.lastpass,
|
||||
referrer: event.source.location.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.command && event.data.command === "webAuthnResult") {
|
||||
chrome.runtime.sendMessage({
|
||||
command: event.data.command,
|
||||
data: event.data.data,
|
||||
remember: event.data.remember,
|
||||
referrer: event.source.location.hostname,
|
||||
});
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
import { setupExtensionDisconnectAction } from "../utils";
|
||||
|
||||
const forwardCommands = [
|
||||
"bgUnlockPopoutOpened",
|
||||
@ -34,8 +7,59 @@ const forwardCommands = [
|
||||
"addedCipher",
|
||||
];
|
||||
|
||||
chrome.runtime.onMessage.addListener((event) => {
|
||||
if (forwardCommands.includes(event.command)) {
|
||||
chrome.runtime.sendMessage(event);
|
||||
/**
|
||||
* Handles sending extension messages to the background
|
||||
* script based on window messages from the page.
|
||||
*
|
||||
* @param event - Window message event
|
||||
*/
|
||||
const handleWindowMessage = (event: MessageEvent) => {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (event.data.command && event.data.command === "authResult") {
|
||||
chrome.runtime.sendMessage({
|
||||
command: event.data.command,
|
||||
code: event.data.code,
|
||||
state: event.data.state,
|
||||
lastpass: event.data.lastpass,
|
||||
referrer: event.source.location.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.command && event.data.command === "webAuthnResult") {
|
||||
chrome.runtime.sendMessage({
|
||||
command: event.data.command,
|
||||
data: event.data.data,
|
||||
remember: event.data.remember,
|
||||
referrer: event.source.location.hostname,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles forwarding any commands that need to trigger
|
||||
* an action from one service of the extension background
|
||||
* to another.
|
||||
*
|
||||
* @param message - Message from the extension
|
||||
*/
|
||||
const handleExtensionMessage = (message: any) => {
|
||||
if (forwardCommands.includes(message.command)) {
|
||||
chrome.runtime.sendMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles cleaning up any event listeners that were
|
||||
* added to the window or extension.
|
||||
*/
|
||||
const handleExtensionDisconnect = () => {
|
||||
window.removeEventListener("message", handleWindowMessage);
|
||||
chrome.runtime.onMessage.removeListener(handleExtensionMessage);
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleWindowMessage, false);
|
||||
chrome.runtime.onMessage.addListener(handleExtensionMessage);
|
||||
setupExtensionDisconnectAction(handleExtensionDisconnect);
|
||||
|
@ -4,6 +4,7 @@ import AddLoginRuntimeMessage from "../notification/models/add-login-runtime-mes
|
||||
import ChangePasswordRuntimeMessage from "../notification/models/change-password-runtime-message";
|
||||
import { FormData } from "../services/abstractions/autofill.service";
|
||||
import { GlobalSettings, UserSettings } from "../types";
|
||||
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils";
|
||||
|
||||
interface HTMLElementWithFormOpId extends HTMLElement {
|
||||
formOpId: string;
|
||||
@ -122,6 +123,8 @@ async function loadNotificationBar() {
|
||||
}
|
||||
}
|
||||
|
||||
setupExtensionDisconnectAction(handleExtensionDisconnection);
|
||||
|
||||
if (!showNotificationBar) {
|
||||
return;
|
||||
}
|
||||
@ -999,11 +1002,23 @@ async function loadNotificationBar() {
|
||||
return theEl === document;
|
||||
}
|
||||
|
||||
function handleExtensionDisconnection(port: chrome.runtime.Port) {
|
||||
closeBar(false);
|
||||
clearTimeout(domObservationCollectTimeoutId);
|
||||
clearTimeout(collectPageDetailsTimeoutId);
|
||||
clearTimeout(handlePageChangeTimeoutId);
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
watchedForms.forEach((wf: WatchedForm) => {
|
||||
const form = wf.formEl;
|
||||
form.removeEventListener("submit", formSubmitted, false);
|
||||
const submitButton = getSubmitButton(
|
||||
form,
|
||||
unionSets(logInButtonNames, changePasswordButtonNames),
|
||||
);
|
||||
submitButton?.removeEventListener("click", formSubmitted, false);
|
||||
});
|
||||
}
|
||||
|
||||
// End Helper Functions
|
||||
}
|
||||
|
||||
async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage));
|
||||
});
|
||||
}
|
||||
|
5
apps/browser/src/autofill/enums/autofill-port.enums.ts
Normal file
5
apps/browser/src/autofill/enums/autofill-port.enums.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const AutofillPort = {
|
||||
InjectedScript: "autofill-injected-script-port",
|
||||
} as const;
|
||||
|
||||
export { AutofillPort };
|
@ -1,5 +1,5 @@
|
||||
import { EVENTS } from "../../constants";
|
||||
import { setElementStyles } from "../../utils/utils";
|
||||
import { setElementStyles } from "../../utils";
|
||||
import {
|
||||
BackgroundPortMessageHandlers,
|
||||
AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface,
|
||||
@ -166,9 +166,10 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
|
||||
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" });
|
||||
globalThis.removeEventListener("message", this.handleWindowMessage);
|
||||
this.port.onMessage.removeListener(this.handlePortMessage);
|
||||
this.port.onDisconnect.removeListener(this.handlePortDisconnect);
|
||||
this.port.disconnect();
|
||||
this.unobserveIframe();
|
||||
this.port?.onMessage.removeListener(this.handlePortMessage);
|
||||
this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
|
||||
this.port?.disconnect();
|
||||
this.port = null;
|
||||
};
|
||||
|
||||
@ -369,7 +370,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
* Unobserves the iframe element for mutations to its style attribute.
|
||||
*/
|
||||
private unobserveIframe() {
|
||||
this.iframeMutationObserver.disconnect();
|
||||
this.iframeMutationObserver?.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,8 +3,8 @@ import "lit/polyfill-support.js";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
import { EVENTS } from "../../../constants";
|
||||
import { buildSvgDomElement } from "../../../utils";
|
||||
import { logoIcon, logoLockedIcon } from "../../../utils/svg-icons";
|
||||
import { buildSvgDomElement } from "../../../utils/utils";
|
||||
import {
|
||||
InitAutofillOverlayButtonMessage,
|
||||
OverlayButtonWindowMessageHandlers,
|
||||
|
@ -4,8 +4,8 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
|
||||
import { OverlayCipherData } from "../../../background/abstractions/overlay.background";
|
||||
import { EVENTS } from "../../../constants";
|
||||
import { buildSvgDomElement } from "../../../utils";
|
||||
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../utils/svg-icons";
|
||||
import { buildSvgDomElement } from "../../../utils/utils";
|
||||
import {
|
||||
InitAutofillOverlayListMessage,
|
||||
OverlayListWindowMessageHandlers,
|
||||
|
@ -10,6 +10,7 @@ import { UriMatchType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../../../platform/flags";
|
||||
import { AutofillService } from "../../services/abstractions/autofill.service";
|
||||
import { AutofillOverlayVisibility } from "../../utils/autofill-overlay.enum";
|
||||
|
||||
@Component({
|
||||
@ -35,6 +36,7 @@ export class AutofillComponent implements OnInit {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private settingsService: SettingsService,
|
||||
private autofillService: AutofillService,
|
||||
) {
|
||||
this.autoFillOverlayVisibilityOptions = [
|
||||
{
|
||||
@ -86,7 +88,10 @@ export class AutofillComponent implements OnInit {
|
||||
}
|
||||
|
||||
async updateAutoFillOverlayVisibility() {
|
||||
const previousAutoFillOverlayVisibility =
|
||||
await this.settingsService.getAutoFillOverlayVisibility();
|
||||
await this.settingsService.setAutoFillOverlayVisibility(this.autoFillOverlayVisibility);
|
||||
await this.handleUpdatingAutofillOverlayContentScripts(previousAutoFillOverlayVisibility);
|
||||
}
|
||||
|
||||
async updateAutoFillOnPageLoad() {
|
||||
@ -144,4 +149,25 @@ export class AutofillComponent implements OnInit {
|
||||
event.preventDefault();
|
||||
BrowserApi.createNewTab(this.disablePasswordManagerLink);
|
||||
}
|
||||
|
||||
private async handleUpdatingAutofillOverlayContentScripts(
|
||||
previousAutoFillOverlayVisibility: number,
|
||||
) {
|
||||
const autofillOverlayPreviouslyDisabled =
|
||||
previousAutoFillOverlayVisibility === AutofillOverlayVisibility.Off;
|
||||
const autofillOverlayCurrentlyDisabled =
|
||||
this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off;
|
||||
|
||||
if (!autofillOverlayPreviouslyDisabled && !autofillOverlayCurrentlyDisabled) {
|
||||
const tabs = await BrowserApi.tabsQuery({});
|
||||
tabs.forEach((tab) =>
|
||||
BrowserApi.tabSendMessageData(tab, "updateAutofillOverlayVisibility", {
|
||||
autofillOverlayVisibility: this.autoFillOverlayVisibility,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.autofillService.reloadAutofillScripts();
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ interface AutofillOverlayContentService {
|
||||
isCurrentlyFilling: boolean;
|
||||
isOverlayCiphersPopulated: boolean;
|
||||
pageDetailsUpdateRequired: boolean;
|
||||
autofillOverlayVisibility: number;
|
||||
init(): void;
|
||||
setupAutofillOverlayListenerOnField(
|
||||
autofillFieldElement: ElementWithOpId<FormFieldElement>,
|
||||
@ -27,6 +28,7 @@ interface AutofillOverlayContentService {
|
||||
redirectOverlayFocusOut(direction: "previous" | "next"): void;
|
||||
focusMostRecentOverlayField(): void;
|
||||
blurMostRecentOverlayField(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export { OpenAutofillOverlayOptions, AutofillOverlayContentService };
|
||||
|
@ -44,10 +44,12 @@ export interface GenerateFillScriptOptions {
|
||||
}
|
||||
|
||||
export abstract class AutofillService {
|
||||
loadAutofillScriptsOnInstall: () => Promise<void>;
|
||||
reloadAutofillScripts: () => Promise<void>;
|
||||
injectAutofillScripts: (
|
||||
sender: chrome.runtime.MessageSender,
|
||||
autofillV2?: boolean,
|
||||
autofillOverlay?: boolean,
|
||||
tab: chrome.tabs.Tab,
|
||||
frameId?: number,
|
||||
triggeringOnPageLoad?: boolean,
|
||||
) => Promise<void>;
|
||||
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
|
||||
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
|
||||
|
@ -22,6 +22,7 @@ interface CollectAutofillContentService {
|
||||
filterCallback: CallableFunction,
|
||||
isObservingShadowRoot?: boolean,
|
||||
): Node[];
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -1609,4 +1609,100 @@ describe("AutofillOverlayContentService", () => {
|
||||
expect(autofillOverlayContentService["removeAutofillOverlay"]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
|
||||
let autofillFieldData: AutofillField;
|
||||
|
||||
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";
|
||||
autofillFieldData = createAutofillFieldMock({
|
||||
opid: "username-field",
|
||||
form: "validFormId",
|
||||
placeholder: "username",
|
||||
elementNumber: 1,
|
||||
});
|
||||
autofillOverlayContentService.setupAutofillOverlayListenerOnField(
|
||||
autofillFieldElement,
|
||||
autofillFieldData,
|
||||
);
|
||||
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
||||
});
|
||||
|
||||
it("disconnects all mutation observers", () => {
|
||||
autofillOverlayContentService["setupMutationObserver"]();
|
||||
jest.spyOn(autofillOverlayContentService["bodyElementMutationObserver"], "disconnect");
|
||||
jest.spyOn(autofillOverlayContentService["documentElementMutationObserver"], "disconnect");
|
||||
|
||||
autofillOverlayContentService.destroy();
|
||||
|
||||
expect(
|
||||
autofillOverlayContentService["documentElementMutationObserver"].disconnect,
|
||||
).toHaveBeenCalled();
|
||||
expect(
|
||||
autofillOverlayContentService["bodyElementMutationObserver"].disconnect,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears the user interaction event timeout", () => {
|
||||
jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout");
|
||||
|
||||
autofillOverlayContentService.destroy();
|
||||
|
||||
expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("de-registers all global event listeners", () => {
|
||||
jest.spyOn(globalThis.document, "removeEventListener");
|
||||
jest.spyOn(globalThis, "removeEventListener");
|
||||
jest.spyOn(autofillOverlayContentService as any, "removeOverlayRepositionEventListeners");
|
||||
|
||||
autofillOverlayContentService.destroy();
|
||||
|
||||
expect(globalThis.document.removeEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.VISIBILITYCHANGE,
|
||||
autofillOverlayContentService["handleVisibilityChangeEvent"],
|
||||
);
|
||||
expect(globalThis.removeEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.FOCUSOUT,
|
||||
autofillOverlayContentService["handleFormFieldBlurEvent"],
|
||||
);
|
||||
expect(
|
||||
autofillOverlayContentService["removeOverlayRepositionEventListeners"],
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("de-registers any event listeners that are attached to the form field elements", () => {
|
||||
jest.spyOn(autofillOverlayContentService as any, "removeCachedFormFieldEventListeners");
|
||||
jest.spyOn(autofillFieldElement, "removeEventListener");
|
||||
jest.spyOn(autofillOverlayContentService["formFieldElements"], "delete");
|
||||
|
||||
autofillOverlayContentService.destroy();
|
||||
|
||||
expect(
|
||||
autofillOverlayContentService["removeCachedFormFieldEventListeners"],
|
||||
).toHaveBeenCalledWith(autofillFieldElement);
|
||||
expect(autofillFieldElement.removeEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.BLUR,
|
||||
autofillOverlayContentService["handleFormFieldBlurEvent"],
|
||||
);
|
||||
expect(autofillFieldElement.removeEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.KEYUP,
|
||||
autofillOverlayContentService["handleFormFieldKeyupEvent"],
|
||||
);
|
||||
expect(autofillOverlayContentService["formFieldElements"].delete).toHaveBeenCalledWith(
|
||||
autofillFieldElement,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,16 +10,12 @@ import AutofillField from "../models/autofill-field";
|
||||
import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe";
|
||||
import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe";
|
||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
import { generateRandomCustomElementName, sendExtensionMessage, setElementStyles } from "../utils";
|
||||
import {
|
||||
AutofillOverlayElement,
|
||||
RedirectFocusDirection,
|
||||
AutofillOverlayVisibility,
|
||||
} from "../utils/autofill-overlay.enum";
|
||||
import {
|
||||
generateRandomCustomElementName,
|
||||
sendExtensionMessage,
|
||||
setElementStyles,
|
||||
} from "../utils/utils";
|
||||
|
||||
import {
|
||||
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
||||
@ -32,9 +28,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
||||
isCurrentlyFilling = false;
|
||||
isOverlayCiphersPopulated = false;
|
||||
pageDetailsUpdateRequired = false;
|
||||
autofillOverlayVisibility: number;
|
||||
private readonly findTabs = tabbable;
|
||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||
private autofillOverlayVisibility: number;
|
||||
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
|
||||
private userFilledFields: Record<string, FillableFormFieldElement> = {};
|
||||
private authStatus: AuthenticationStatus;
|
||||
private focusableElements: FocusableElement[] = [];
|
||||
@ -47,6 +44,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
||||
private userInteractionEventTimeout: NodeJS.Timeout;
|
||||
private overlayElementsMutationObserver: MutationObserver;
|
||||
private bodyElementMutationObserver: MutationObserver;
|
||||
private documentElementMutationObserver: MutationObserver;
|
||||
private mutationObserverIterations = 0;
|
||||
private mutationObserverIterationsResetTimeout: NodeJS.Timeout;
|
||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
||||
@ -86,6 +84,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
||||
return;
|
||||
}
|
||||
|
||||
this.formFieldElements.add(formFieldElement);
|
||||
|
||||
if (!this.autofillOverlayVisibility) {
|
||||
await this.getAutofillOverlayVisibility();
|
||||
}
|
||||
@ -901,10 +901,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
||||
this.handleBodyElementMutationObserverUpdate,
|
||||
);
|
||||
|
||||
const documentElementMutationObserver = new MutationObserver(
|
||||
this.documentElementMutationObserver = new MutationObserver(
|
||||
this.handleDocumentElementMutationObserverUpdate,
|
||||
);
|
||||
documentElementMutationObserver.observe(globalThis.document.documentElement, {
|
||||
this.documentElementMutationObserver.observe(globalThis.document.documentElement, {
|
||||
childList: true,
|
||||
});
|
||||
};
|
||||
@ -1117,6 +1117,28 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
||||
const documentRoot = element.getRootNode() as ShadowRoot | Document;
|
||||
return documentRoot?.activeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the autofill overlay content service. This method will
|
||||
* disconnect the mutation observers and remove all event listeners.
|
||||
*/
|
||||
destroy() {
|
||||
this.documentElementMutationObserver?.disconnect();
|
||||
this.clearUserInteractionEventTimeout();
|
||||
this.formFieldElements.forEach((formFieldElement) => {
|
||||
this.removeCachedFormFieldEventListeners(formFieldElement);
|
||||
formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||
this.formFieldElements.delete(formFieldElement);
|
||||
});
|
||||
globalThis.document.removeEventListener(
|
||||
EVENTS.VISIBILITYCHANGE,
|
||||
this.handleVisibilityChangeEvent,
|
||||
);
|
||||
globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
||||
this.removeAutofillOverlay();
|
||||
this.removeOverlayRepositionEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
export default AutofillOverlayContentService;
|
||||
|
@ -2,7 +2,9 @@ import { mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||
import {
|
||||
@ -24,6 +26,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import {
|
||||
createAutofillFieldMock,
|
||||
createAutofillPageDetailsMock,
|
||||
@ -54,6 +57,7 @@ describe("AutofillService", () => {
|
||||
const logService = mock<LogService>();
|
||||
const settingsService = mock<SettingsService>();
|
||||
const userVerificationService = mock<UserVerificationService>();
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
beforeEach(() => {
|
||||
autofillService = new AutofillService(
|
||||
@ -64,6 +68,7 @@ describe("AutofillService", () => {
|
||||
logService,
|
||||
settingsService,
|
||||
userVerificationService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
@ -72,6 +77,72 @@ describe("AutofillService", () => {
|
||||
mockReset(cipherService);
|
||||
});
|
||||
|
||||
describe("loadAutofillScriptsOnInstall", () => {
|
||||
let tab1: chrome.tabs.Tab;
|
||||
let tab2: chrome.tabs.Tab;
|
||||
let tab3: chrome.tabs.Tab;
|
||||
|
||||
beforeEach(() => {
|
||||
tab1 = createChromeTabMock({ id: 1, url: "https://some-url.com" });
|
||||
tab2 = createChromeTabMock({ id: 2, url: "http://some-url.com" });
|
||||
tab3 = createChromeTabMock({ id: 3, url: "chrome-extension://some-extension-route" });
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([tab1, tab2]);
|
||||
});
|
||||
|
||||
it("queries all browser tabs and injects the autofill scripts into them", async () => {
|
||||
jest.spyOn(autofillService, "injectAutofillScripts");
|
||||
|
||||
await autofillService.loadAutofillScriptsOnInstall();
|
||||
|
||||
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({});
|
||||
expect(autofillService.injectAutofillScripts).toHaveBeenCalledWith(tab1, 0, false);
|
||||
expect(autofillService.injectAutofillScripts).toHaveBeenCalledWith(tab2, 0, false);
|
||||
});
|
||||
|
||||
it("skips injecting scripts into tabs that do not have an http(s) protocol", async () => {
|
||||
jest.spyOn(autofillService, "injectAutofillScripts");
|
||||
|
||||
await autofillService.loadAutofillScriptsOnInstall();
|
||||
|
||||
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({});
|
||||
expect(autofillService.injectAutofillScripts).not.toHaveBeenCalledWith(tab3);
|
||||
});
|
||||
|
||||
it("sets up an extension runtime onConnect listener", async () => {
|
||||
await autofillService.loadAutofillScriptsOnInstall();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("reloadAutofillScripts", () => {
|
||||
it("disconnects and removes all autofill script ports", () => {
|
||||
const port1 = mock<chrome.runtime.Port>({
|
||||
disconnect: jest.fn(),
|
||||
});
|
||||
const port2 = mock<chrome.runtime.Port>({
|
||||
disconnect: jest.fn(),
|
||||
});
|
||||
autofillService["autofillScriptPortsSet"] = new Set([port1, port2]);
|
||||
|
||||
autofillService.reloadAutofillScripts();
|
||||
|
||||
expect(port1.disconnect).toHaveBeenCalled();
|
||||
expect(port2.disconnect).toHaveBeenCalled();
|
||||
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||
});
|
||||
|
||||
it("re-injects the autofill scripts in all tabs", () => {
|
||||
autofillService["autofillScriptPortsSet"] = new Set([mock<chrome.runtime.Port>()]);
|
||||
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
||||
|
||||
autofillService.reloadAutofillScripts();
|
||||
|
||||
expect(autofillService["injectAutofillScriptsInAllTabs"]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("injectAutofillScripts", () => {
|
||||
const autofillV1Script = "autofill.js";
|
||||
const autofillV2BootstrapScript = "bootstrap-autofill.js";
|
||||
@ -83,12 +154,12 @@ describe("AutofillService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
tabMock = createChromeTabMock();
|
||||
sender = { tab: tabMock };
|
||||
sender = { tab: tabMock, frameId: 1 };
|
||||
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
||||
});
|
||||
|
||||
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
|
||||
await autofillService.injectAutofillScripts(sender);
|
||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
|
||||
|
||||
[autofillV1Script, ...defaultAutofillScripts].forEach((scriptName) => {
|
||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||
@ -105,7 +176,11 @@ describe("AutofillService", () => {
|
||||
});
|
||||
|
||||
it("will inject the bootstrap-autofill script if the enableAutofillV2 flag is set", async () => {
|
||||
await autofillService.injectAutofillScripts(sender, true);
|
||||
jest
|
||||
.spyOn(configService, "getFeatureFlag")
|
||||
.mockImplementation((flag) => Promise.resolve(flag === FeatureFlag.AutofillV2));
|
||||
|
||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||
|
||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillV2BootstrapScript}`,
|
||||
@ -120,11 +195,16 @@ describe("AutofillService", () => {
|
||||
});
|
||||
|
||||
it("will inject the bootstrap-autofill-overlay script if the enableAutofillOverlay flag is set and the user has the autofill overlay enabled", async () => {
|
||||
jest
|
||||
.spyOn(configService, "getFeatureFlag")
|
||||
.mockImplementation((flag) =>
|
||||
Promise.resolve(flag === FeatureFlag.AutofillOverlay || flag === FeatureFlag.AutofillV2),
|
||||
);
|
||||
jest
|
||||
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
|
||||
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
||||
|
||||
await autofillService.injectAutofillScripts(sender, true, true);
|
||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||
|
||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillOverlayBootstrapScript}`,
|
||||
@ -144,18 +224,25 @@ describe("AutofillService", () => {
|
||||
});
|
||||
|
||||
it("will inject the bootstrap-autofill script if the enableAutofillOverlay flag is set but the user does not have the autofill overlay enabled", async () => {
|
||||
jest
|
||||
.spyOn(configService, "getFeatureFlag")
|
||||
.mockImplementation((flag) =>
|
||||
Promise.resolve(flag === FeatureFlag.AutofillOverlay || flag === FeatureFlag.AutofillV2),
|
||||
);
|
||||
jest
|
||||
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
|
||||
.mockResolvedValue(AutofillOverlayVisibility.Off);
|
||||
|
||||
await autofillService.injectAutofillScripts(sender, true, true);
|
||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||
|
||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillV2BootstrapScript}`,
|
||||
frameId: sender.frameId,
|
||||
...defaultExecuteScriptOptions,
|
||||
});
|
||||
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillV1Script}`,
|
||||
frameId: sender.frameId,
|
||||
...defaultExecuteScriptOptions,
|
||||
});
|
||||
});
|
||||
@ -4436,4 +4523,58 @@ describe("AutofillService", () => {
|
||||
expect(autofillService["currentlyOpeningPasswordRepromptPopout"]).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleInjectedScriptPortConnection", () => {
|
||||
it("ignores port connections that do not have the correct port name", () => {
|
||||
const port = mock<chrome.runtime.Port>({
|
||||
name: "some-invalid-port-name",
|
||||
onDisconnect: { addListener: jest.fn() },
|
||||
}) as any;
|
||||
|
||||
autofillService["handleInjectedScriptPortConnection"](port);
|
||||
|
||||
expect(port.onDisconnect.addListener).not.toHaveBeenCalled();
|
||||
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||
});
|
||||
|
||||
it("adds the connect port to the set of injected script ports and sets up an onDisconnect listener", () => {
|
||||
const port = mock<chrome.runtime.Port>({
|
||||
name: AutofillPort.InjectedScript,
|
||||
onDisconnect: { addListener: jest.fn() },
|
||||
}) as any;
|
||||
jest.spyOn(autofillService as any, "handleInjectScriptPortOnDisconnect");
|
||||
|
||||
autofillService["handleInjectedScriptPortConnection"](port);
|
||||
|
||||
expect(port.onDisconnect.addListener).toHaveBeenCalledWith(
|
||||
autofillService["handleInjectScriptPortOnDisconnect"],
|
||||
);
|
||||
expect(autofillService["autofillScriptPortsSet"].size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleInjectScriptPortOnDisconnect", () => {
|
||||
it("ignores port disconnections that do not have the correct port name", () => {
|
||||
autofillService["autofillScriptPortsSet"].add(mock<chrome.runtime.Port>());
|
||||
|
||||
autofillService["handleInjectScriptPortOnDisconnect"](
|
||||
mock<chrome.runtime.Port>({
|
||||
name: "some-invalid-port-name",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(autofillService["autofillScriptPortsSet"].size).toBe(1);
|
||||
});
|
||||
|
||||
it("removes the port from the set of injected script ports", () => {
|
||||
const port = mock<chrome.runtime.Port>({
|
||||
name: AutofillPort.InjectedScript,
|
||||
}) as any;
|
||||
autofillService["autofillScriptPortsSet"].add(port);
|
||||
|
||||
autofillService["handleInjectScriptPortOnDisconnect"](port);
|
||||
|
||||
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,6 +2,8 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@ -13,6 +15,7 @@ import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import AutofillScript from "../models/autofill-script";
|
||||
@ -35,6 +38,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout;
|
||||
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
|
||||
private currentlyOpeningPasswordRepromptPopout = false;
|
||||
private autofillScriptPortsSet = new Set<chrome.runtime.Port>();
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
@ -44,23 +48,54 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
private logService: LogService,
|
||||
private settingsService: SettingsService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Triggers on installation of the extension Handles injecting
|
||||
* content scripts into all tabs that are currently open, and
|
||||
* sets up a listener to ensure content scripts can identify
|
||||
* if the extension context has been disconnected.
|
||||
*/
|
||||
async loadAutofillScriptsOnInstall() {
|
||||
BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection);
|
||||
|
||||
this.injectAutofillScriptsInAllTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a complete reload of all autofill scripts on tabs open within
|
||||
* the user's browsing session. This is done by first disconnecting all
|
||||
* existing autofill content script ports, which cleans up existing object
|
||||
* instances, and then re-injecting the autofill scripts into all tabs.
|
||||
*/
|
||||
async reloadAutofillScripts() {
|
||||
this.autofillScriptPortsSet.forEach((port) => {
|
||||
port.disconnect();
|
||||
this.autofillScriptPortsSet.delete(port);
|
||||
});
|
||||
|
||||
this.injectAutofillScriptsInAllTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the autofill scripts into the current tab and all frames
|
||||
* found within the tab. Temporarily, will conditionally inject
|
||||
* the refactor of the core autofill script if the feature flag
|
||||
* is enabled.
|
||||
* @param {chrome.runtime.MessageSender} sender
|
||||
* @param {boolean} autofillV2
|
||||
* @param {boolean} autofillOverlay
|
||||
* @returns {Promise<void>}
|
||||
* @param {chrome.tabs.Tab} tab
|
||||
* @param {number} frameId
|
||||
* @param {boolean} triggeringOnPageLoad
|
||||
*/
|
||||
async injectAutofillScripts(
|
||||
sender: chrome.runtime.MessageSender,
|
||||
autofillV2 = false,
|
||||
autofillOverlay = false,
|
||||
) {
|
||||
tab: chrome.tabs.Tab,
|
||||
frameId = 0,
|
||||
triggeringOnPageLoad = true,
|
||||
): Promise<void> {
|
||||
const autofillV2 = await this.configService.getFeatureFlag<boolean>(FeatureFlag.AutofillV2);
|
||||
const autofillOverlay = await this.configService.getFeatureFlag<boolean>(
|
||||
FeatureFlag.AutofillOverlay,
|
||||
);
|
||||
let mainAutofillScript = "autofill.js";
|
||||
|
||||
const isUsingAutofillOverlay =
|
||||
@ -73,20 +108,24 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
: "bootstrap-autofill.js";
|
||||
}
|
||||
|
||||
const injectedScripts = [
|
||||
mainAutofillScript,
|
||||
"autofiller.js",
|
||||
"notificationBar.js",
|
||||
"contextMenuHandler.js",
|
||||
];
|
||||
const injectedScripts = [mainAutofillScript];
|
||||
if (triggeringOnPageLoad) {
|
||||
injectedScripts.push("autofiller.js");
|
||||
}
|
||||
injectedScripts.push("notificationBar.js", "contextMenuHandler.js");
|
||||
|
||||
for (const injectedScript of injectedScripts) {
|
||||
await BrowserApi.executeScriptInTab(sender.tab.id, {
|
||||
await BrowserApi.executeScriptInTab(tab.id, {
|
||||
file: `content/${injectedScript}`,
|
||||
frameId: sender.frameId,
|
||||
frameId,
|
||||
runAt: "document_start",
|
||||
});
|
||||
}
|
||||
|
||||
await BrowserApi.executeScriptInTab(tab.id, {
|
||||
file: "content/message_handler.js",
|
||||
runAt: "document_start",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1877,4 +1916,47 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming long-lived connections from injected autofill scripts.
|
||||
* Stores the port in a set to facilitate disconnecting ports if the extension
|
||||
* needs to re-inject the autofill scripts.
|
||||
*
|
||||
* @param port - The port that was connected
|
||||
*/
|
||||
private handleInjectedScriptPortConnection = (port: chrome.runtime.Port) => {
|
||||
if (port.name !== AutofillPort.InjectedScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autofillScriptPortsSet.add(port);
|
||||
port.onDisconnect.addListener(this.handleInjectScriptPortOnDisconnect);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles disconnecting ports that relate to injected autofill scripts.
|
||||
|
||||
* @param port - The port that was disconnected
|
||||
*/
|
||||
private handleInjectScriptPortOnDisconnect = (port: chrome.runtime.Port) => {
|
||||
if (port.name !== AutofillPort.InjectedScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autofillScriptPortsSet.delete(port);
|
||||
};
|
||||
|
||||
/**
|
||||
* Queries all open tabs in the user's browsing session
|
||||
* and injects the autofill scripts into the page.
|
||||
*/
|
||||
private async injectAutofillScriptsInAllTabs() {
|
||||
const tabs = await BrowserApi.tabsQuery({});
|
||||
for (let index = 0; index < tabs.length; index++) {
|
||||
const tab = tabs[index];
|
||||
if (tab.url?.startsWith("http")) {
|
||||
this.injectAutofillScripts(tab, 0, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1249,6 +1249,17 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
||||
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the CollectAutofillContentService. Clears all
|
||||
* timeouts and disconnects the mutation observer.
|
||||
*/
|
||||
destroy() {
|
||||
if (this.updateAutofillElementsAfterMutationTimeout) {
|
||||
clearTimeout(this.updateAutofillElementsAfterMutationTimeout);
|
||||
}
|
||||
this.mutationObserver?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectAutofillContentService;
|
||||
|
@ -1,10 +1,17 @@
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import { triggerPortOnDisconnectEvent } from "../jest/testing-utils";
|
||||
|
||||
import { logoIcon, logoLockedIcon } from "./svg-icons";
|
||||
|
||||
import {
|
||||
buildSvgDomElement,
|
||||
generateRandomCustomElementName,
|
||||
sendExtensionMessage,
|
||||
setElementStyles,
|
||||
} from "./utils";
|
||||
getFromLocalStorage,
|
||||
setupExtensionDisconnectAction,
|
||||
setupAutofillInitDisconnectAction,
|
||||
} from "./index";
|
||||
|
||||
describe("buildSvgDomElement", () => {
|
||||
it("returns an SVG DOM element", () => {
|
||||
@ -116,3 +123,107 @@ describe("setElementStyles", () => {
|
||||
expect(testDiv.style.cssText).toEqual(expectedCSSRuleString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFromLocalStorage", () => {
|
||||
it("returns a promise with the storage object pulled from the extension storage api", async () => {
|
||||
const localStorage: Record<string, any> = {
|
||||
testValue: "test",
|
||||
another: "another",
|
||||
};
|
||||
jest.spyOn(chrome.storage.local, "get").mockImplementation((keys, callback) => {
|
||||
const localStorageObject: Record<string, string> = {};
|
||||
|
||||
if (typeof keys === "string") {
|
||||
localStorageObject[keys] = localStorage[keys];
|
||||
} else if (Array.isArray(keys)) {
|
||||
for (const key of keys) {
|
||||
localStorageObject[key] = localStorage[key];
|
||||
}
|
||||
}
|
||||
|
||||
callback(localStorageObject);
|
||||
});
|
||||
|
||||
const returnValue = await getFromLocalStorage("testValue");
|
||||
|
||||
expect(chrome.storage.local.get).toHaveBeenCalled();
|
||||
expect(returnValue).toEqual({ testValue: "test" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupExtensionDisconnectAction", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("connects a port to the extension background and sets up an onDisconnect listener", () => {
|
||||
const onDisconnectCallback = jest.fn();
|
||||
let port: chrome.runtime.Port;
|
||||
jest.spyOn(chrome.runtime, "connect").mockImplementation(() => {
|
||||
port = {
|
||||
onDisconnect: {
|
||||
addListener: onDisconnectCallback,
|
||||
removeListener: jest.fn(),
|
||||
},
|
||||
} as unknown as chrome.runtime.Port;
|
||||
|
||||
return port;
|
||||
});
|
||||
|
||||
setupExtensionDisconnectAction(onDisconnectCallback);
|
||||
|
||||
expect(chrome.runtime.connect).toHaveBeenCalledWith({
|
||||
name: AutofillPort.InjectedScript,
|
||||
});
|
||||
expect(port.onDisconnect.addListener).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupAutofillInitDisconnectAction", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("skips setting up the extension disconnect action if the bitwardenAutofillInit object is not populated", () => {
|
||||
const onDisconnectCallback = jest.fn();
|
||||
window.bitwardenAutofillInit = undefined;
|
||||
const portConnectSpy = jest.spyOn(chrome.runtime, "connect").mockImplementation(() => {
|
||||
return {
|
||||
onDisconnect: {
|
||||
addListener: onDisconnectCallback,
|
||||
removeListener: jest.fn(),
|
||||
},
|
||||
} as unknown as chrome.runtime.Port;
|
||||
});
|
||||
|
||||
setupAutofillInitDisconnectAction(window);
|
||||
|
||||
expect(portConnectSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("destroys the autofill init instance when the port is disconnected", () => {
|
||||
let port: chrome.runtime.Port;
|
||||
const autofillInitDestroy: CallableFunction = jest.fn();
|
||||
window.bitwardenAutofillInit = {
|
||||
destroy: autofillInitDestroy,
|
||||
} as any;
|
||||
jest.spyOn(chrome.runtime, "connect").mockImplementation(() => {
|
||||
port = {
|
||||
onDisconnect: {
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
},
|
||||
} as unknown as chrome.runtime.Port;
|
||||
|
||||
return port;
|
||||
});
|
||||
|
||||
setupAutofillInitDisconnectAction(window);
|
||||
triggerPortOnDisconnectEvent(port as chrome.runtime.Port);
|
||||
|
||||
expect(chrome.runtime.connect).toHaveBeenCalled();
|
||||
expect(port.onDisconnect.addListener).toHaveBeenCalled();
|
||||
expect(autofillInitDestroy).toHaveBeenCalled();
|
||||
expect(window.bitwardenAutofillInit).toBeUndefined();
|
||||
});
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
|
||||
/**
|
||||
* Generates a random string of characters that formatted as a custom element name.
|
||||
*/
|
||||
@ -103,9 +105,57 @@ function setElementStyles(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from local storage based on the keys provided.
|
||||
*
|
||||
* @param keys - String or array of strings of keys to get from local storage
|
||||
*/
|
||||
async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a long-lived connection with the extension background
|
||||
* and triggers an onDisconnect event if the extension context
|
||||
* is invalidated.
|
||||
*
|
||||
* @param callback - Callback function to run when the extension disconnects
|
||||
*/
|
||||
function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) {
|
||||
const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript });
|
||||
const onDisconnectCallback = (disconnectedPort: chrome.runtime.Port) => {
|
||||
callback(disconnectedPort);
|
||||
port.onDisconnect.removeListener(onDisconnectCallback);
|
||||
};
|
||||
port.onDisconnect.addListener(onDisconnectCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setup of the extension disconnect action for the autofill init class
|
||||
* in both instances where the overlay might or might not be initialized.
|
||||
*
|
||||
* @param windowContext - The global window context
|
||||
*/
|
||||
function setupAutofillInitDisconnectAction(windowContext: Window) {
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onDisconnectCallback = () => {
|
||||
windowContext.bitwardenAutofillInit.destroy();
|
||||
delete windowContext.bitwardenAutofillInit;
|
||||
};
|
||||
setupExtensionDisconnectAction(onDisconnectCallback);
|
||||
}
|
||||
|
||||
export {
|
||||
generateRandomCustomElementName,
|
||||
buildSvgDomElement,
|
||||
sendExtensionMessage,
|
||||
setElementStyles,
|
||||
getFromLocalStorage,
|
||||
setupExtensionDisconnectAction,
|
||||
setupAutofillInitDisconnectAction,
|
||||
};
|
@ -566,6 +566,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.settingsService,
|
||||
this.userVerificationService,
|
||||
this.configService,
|
||||
);
|
||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@ -97,9 +96,9 @@ export default class RuntimeBackground {
|
||||
await closeUnlockPopout();
|
||||
}
|
||||
|
||||
await this.notificationsService.updateConnection(msg.command === "loggedIn");
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu(false);
|
||||
this.notificationsService.updateConnection(msg.command === "unlocked");
|
||||
this.systemService.cancelProcessReload();
|
||||
|
||||
if (item) {
|
||||
@ -133,11 +132,7 @@ export default class RuntimeBackground {
|
||||
await this.main.openPopup();
|
||||
break;
|
||||
case "triggerAutofillScriptInjection":
|
||||
await this.autofillService.injectAutofillScripts(
|
||||
sender,
|
||||
await this.configService.getFeatureFlag<boolean>(FeatureFlag.AutofillV2),
|
||||
await this.configService.getFeatureFlag<boolean>(FeatureFlag.AutofillOverlay),
|
||||
);
|
||||
await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||
break;
|
||||
case "bgCollectPageDetails":
|
||||
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
|
||||
@ -325,6 +320,8 @@ export default class RuntimeBackground {
|
||||
|
||||
private async checkOnInstalled() {
|
||||
setTimeout(async () => {
|
||||
this.autofillService.loadAutofillScriptsOnInstall();
|
||||
|
||||
if (this.onInstalledReason != null) {
|
||||
if (this.onInstalledReason === "install") {
|
||||
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
|
||||
|
@ -24,12 +24,6 @@
|
||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
"all_frames": false,
|
||||
"js": ["content/message_handler.js"],
|
||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
"all_frames": true,
|
||||
"css": ["content/autofill.css"],
|
||||
|
@ -23,11 +23,12 @@ const runtime = {
|
||||
removeListener: jest.fn(),
|
||||
},
|
||||
sendMessage: jest.fn(),
|
||||
getManifest: jest.fn(),
|
||||
getManifest: jest.fn(() => ({ version: 2 })),
|
||||
getURL: jest.fn((path) => `chrome-extension://id/${path}`),
|
||||
connect: jest.fn(),
|
||||
onConnect: {
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user