From 1b76ce5b7c19703429de6ec0b6f8d831c688d9df Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:31:08 -0800 Subject: [PATCH] [PM-30264] - fix exact match dialog show logic (#18216) * fix exact match dialog show logic * fix logic for uri matching * simplify exact match dialog show logic --- .../item-more-options.component.spec.ts | 187 ++++++++++-------- .../item-more-options.component.ts | 11 +- 2 files changed, 111 insertions(+), 87 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts index bd9ce108522..6728249b788 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts @@ -158,14 +158,6 @@ describe("ItemMoreOptionsComponent", () => { expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); }); - it("does not show the exact match dialog when the default match strategy is Exact and autofill confirmation is not to be shown", async () => { - uriMatchStrategy$.next(UriMatchStrategy.Exact); - autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); - }); - describe("autofill confirmation dialog", () => { beforeEach(() => { uriMatchStrategy$.next(UriMatchStrategy.Domain); @@ -236,22 +228,30 @@ describe("ItemMoreOptionsComponent", () => { }); describe("URI match strategy handling", () => { + it("calls the passwordService to passwordRepromptCheck", async () => { + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); + + await component.doAutofill(); + + expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher); + }); + describe("when the default URI match strategy is Exact", () => { beforeEach(() => { uriMatchStrategy$.next(UriMatchStrategy.Exact); }); - it("calls the passwordService to passwordRepromptCheck", async () => { - autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); - mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); - - await component.doAutofill(); - - expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher); - }); - - it("shows the exact match dialog", async () => { + it("shows the exact match dialog when the cipher has no saved URIs", async () => { autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [], + }, + })); await component.doAutofill(); @@ -266,6 +266,53 @@ describe("ItemMoreOptionsComponent", () => { expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); }); + + it("does not show the exact match dialog when the cipher has at least one non-exact match uri", async () => { + mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [ + { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, + { uri: "https://two.example.com", match: UriMatchStrategy.Domain }, + ], + }, + })); + + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows the exact match dialog when the cipher uris all have a match strategy of Exact", async () => { + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [ + { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, + { uri: "https://two.example.com/a", match: UriMatchStrategy.Exact }, + ], + }, + })); + + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.objectContaining({ key: "cannotAutofill" }), + content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), + type: "info", + }), + ); + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); }); describe("when the default URI match strategy is not Exact", () => { @@ -273,7 +320,45 @@ describe("ItemMoreOptionsComponent", () => { mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); uriMatchStrategy$.next(UriMatchStrategy.Domain); }); - it("does not show the exact match dialog", async () => { + + it("does not show the exact match dialog when the cipher has no saved URIs", async () => { + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows the exact match dialog when the cipher has only exact match saved URIs", async () => { + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [ + { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, + { uri: "https://two.example.com/a", match: UriMatchStrategy.Exact }, + ], + }, + })); + + autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); + + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.objectContaining({ key: "cannotAutofill" }), + content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), + type: "info", + }), + ); + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("does not show the exact match dialog when the cipher has at least one uri without a match strategy of Exact", async () => { + mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); cipherService.getFullCipherView.mockImplementation(async (c) => ({ ...baseCipher, ...c, @@ -292,70 +377,6 @@ describe("ItemMoreOptionsComponent", () => { expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); }); - - it("shows the exact match dialog when the cipher has a single uri with a match strategy of Exact", async () => { - cipherService.getFullCipherView.mockImplementation(async (c) => ({ - ...baseCipher, - ...c, - login: { - ...baseCipher.login, - uris: [{ uri: "https://one.example.com", match: UriMatchStrategy.Exact }], - }, - })); - - autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); - - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( - expect.objectContaining({ - title: expect.objectContaining({ key: "cannotAutofill" }), - content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), - type: "info", - }), - ); - expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); - expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); - }); - }); - - it("does not show the exact match dialog when the cipher has no uris", async () => { - mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); - cipherService.getFullCipherView.mockImplementation(async (c) => ({ - ...baseCipher, - ...c, - login: { - ...baseCipher.login, - uris: [], - }, - })); - - autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); - - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); - }); - - it("does not show the exact match dialog when the cipher has a uri with a match strategy of Exact and a uri with a match strategy of Domain", async () => { - mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); - cipherService.getFullCipherView.mockImplementation(async (c) => ({ - ...baseCipher, - ...c, - login: { - ...baseCipher.login, - uris: [ - { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, - { uri: "https://page.example.com", match: UriMatchStrategy.Domain }, - ], - }, - })); - - autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); - - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index c4353e17bef..ce797d9755e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -204,12 +204,15 @@ export class ItemMoreOptionsComponent { } const uris = cipher.login?.uris ?? []; - const cipherHasAllExactMatchLoginUris = - uris.length > 0 && uris.every((u) => u.uri && u.match === UriMatchStrategy.Exact); - const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$); - if (cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact) { + const showExactMatchDialog = + uris.length === 0 + ? uriMatchStrategy === UriMatchStrategy.Exact + : // all saved URIs are exact match + uris.every((u) => (u.match ?? uriMatchStrategy) === UriMatchStrategy.Exact); + + if (showExactMatchDialog) { await this.dialogService.openSimpleDialog({ title: { key: "cannotAutofill" }, content: { key: "cannotAutofillExactMatch" },