| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'chrome://password-manager/password_manager.js'; |
| |
| import type {EditPasswordDialogElement, PasswordDetailsCardElement} from 'chrome://password-manager/password_manager.js'; |
| import {Page, PasswordManagerImpl, PasswordViewPageInteractions, Router, SyncBrowserProxyImpl} from 'chrome://password-manager/password_manager.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js'; |
| import {flushTasks} from 'chrome://webui-test/polymer_test_util.js'; |
| import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js'; |
| |
| import {TestPasswordManagerProxy} from './test_password_manager_proxy.js'; |
| import {TestSyncBrowserProxy} from './test_sync_browser_proxy.js'; |
| import {createAffiliatedDomain, createPasswordEntry, makePasswordManagerPrefs} from './test_util.js'; |
| |
| async function createCardElement( |
| password: chrome.passwordsPrivate.PasswordUiEntry|null = |
| null): Promise<PasswordDetailsCardElement> { |
| if (!password) { |
| password = createPasswordEntry( |
| {url: 'test.com', username: 'vik', password: 'password47'}); |
| } |
| |
| const card = document.createElement('password-details-card'); |
| card.password = password; |
| card.prefs = makePasswordManagerPrefs(); |
| document.body.appendChild(card); |
| await flushTasks(); |
| return card; |
| } |
| |
| suite('PasswordDetailsCardTest', function() { |
| let passwordManager: TestPasswordManagerProxy; |
| let syncProxy: TestSyncBrowserProxy; |
| |
| setup(function() { |
| document.body.innerHTML = window.trustedTypes!.emptyHTML; |
| passwordManager = new TestPasswordManagerProxy(); |
| PasswordManagerImpl.setInstance(passwordManager); |
| syncProxy = new TestSyncBrowserProxy(); |
| SyncBrowserProxyImpl.setInstance(syncProxy); |
| Router.getInstance().navigateTo(Page.PASSWORDS); |
| return flushTasks(); |
| }); |
| |
| test('Content displayed properly', async function() { |
| const password = createPasswordEntry({ |
| url: 'test.com', |
| username: 'vik', |
| password: 'password69', |
| note: 'note', |
| }); |
| |
| const card = await createCardElement(password); |
| |
| assertEquals(password.username, card.$.usernameValue.value); |
| assertEquals(password.password, card.$.passwordValue.value); |
| assertEquals('password', card.$.passwordValue.type); |
| assertTrue(isVisible(card.$.noteValue)); |
| assertEquals(password.note, card.$.noteValue.note); |
| assertTrue(isVisible(card.$.showPasswordButton)); |
| assertTrue(isVisible(card.$.copyPasswordButton)); |
| assertTrue(isVisible(card.$.editButton)); |
| assertTrue(isVisible(card.$.deleteButton)); |
| }); |
| |
| test('Content displayed properly for federated credential', async function() { |
| const password = createPasswordEntry( |
| {url: 'test.com', username: 'vik', federationText: 'federation.com'}); |
| |
| const card = await createCardElement(password); |
| |
| assertEquals(password.username, card.$.usernameValue.value); |
| assertEquals(password.federationText, card.$.passwordValue.value); |
| assertEquals('text', card.$.passwordValue.type); |
| assertFalse(isVisible(card.$.noteValue)); |
| assertFalse(isVisible(card.$.showPasswordButton)); |
| assertFalse(isVisible(card.$.copyPasswordButton)); |
| assertFalse(isVisible(card.$.editButton)); |
| assertTrue(isVisible(card.$.deleteButton)); |
| }); |
| |
| test('Copy password', async function() { |
| const password = createPasswordEntry( |
| {id: 1, url: 'test.com', username: 'vik', password: 'password69'}); |
| |
| const card = await createCardElement(password); |
| |
| assertTrue(isVisible(card.$.copyPasswordButton)); |
| |
| card.$.copyPasswordButton.click(); |
| await eventToPromise('value-copied', card); |
| await passwordManager.whenCalled('extendAuthValidity'); |
| const {id, reason} = |
| await passwordManager.whenCalled('requestPlaintextPassword'); |
| assertEquals(password.id, id); |
| assertEquals(chrome.passwordsPrivate.PlaintextReason.COPY, reason); |
| assertEquals( |
| PasswordViewPageInteractions.PASSWORD_COPY_BUTTON_CLICKED, |
| await passwordManager.whenCalled('recordPasswordViewInteraction')); |
| |
| await flushTasks(); |
| }); |
| |
| test('Links properly displayed', async function() { |
| const password = createPasswordEntry( |
| {url: 'test.com', username: 'vik', password: 'password69'}); |
| password.affiliatedDomains = [ |
| createAffiliatedDomain('test.com'), |
| createAffiliatedDomain('m.test.com'), |
| ]; |
| |
| const card = await createCardElement(password); |
| |
| const listItemElements = |
| card.shadowRoot!.querySelectorAll<HTMLAnchorElement>('a.site-link'); |
| assertEquals(listItemElements.length, password.affiliatedDomains.length); |
| |
| password.affiliatedDomains.forEach((expectedDomain, i) => { |
| const listItemElement = listItemElements[i]; |
| |
| assertTrue(!!listItemElement); |
| assertEquals(expectedDomain.name, listItemElement.textContent!.trim()); |
| assertEquals(expectedDomain.url, listItemElement.href); |
| }); |
| }); |
| |
| test('show/hide password', async function() { |
| const password = createPasswordEntry( |
| {id: 1, url: 'test.com', username: 'vik', password: 'password69'}); |
| |
| const card = await createCardElement(password); |
| |
| assertEquals( |
| loadTimeData.getString('showPassword'), |
| card.$.showPasswordButton.title); |
| assertEquals('password', card.$.passwordValue.type); |
| assertTrue(card.$.showPasswordButton.hasAttribute('class')); |
| assertEquals( |
| 'icon-visibility', card.$.showPasswordButton.getAttribute('class')); |
| |
| card.$.showPasswordButton.click(); |
| assertEquals( |
| PasswordViewPageInteractions.PASSWORD_SHOW_BUTTON_CLICKED, |
| await passwordManager.whenCalled('recordPasswordViewInteraction')); |
| |
| assertEquals( |
| loadTimeData.getString('hidePassword'), |
| card.$.showPasswordButton.title); |
| assertEquals('text', card.$.passwordValue.type); |
| assertTrue(card.$.showPasswordButton.hasAttribute('class')); |
| assertEquals( |
| 'icon-visibility-off', card.$.showPasswordButton.getAttribute('class')); |
| }); |
| |
| test('clicking edit button opens an edit dialog', async function() { |
| const password = createPasswordEntry( |
| {id: 1, url: 'test.com', username: 'vik', password: 'password69'}); |
| password.affiliatedDomains = [createAffiliatedDomain('test.com')]; |
| |
| const card = await createCardElement(password); |
| |
| card.$.editButton.click(); |
| await eventToPromise('cr-dialog-open', card); |
| await passwordManager.whenCalled('extendAuthValidity'); |
| assertEquals( |
| PasswordViewPageInteractions.PASSWORD_EDIT_BUTTON_CLICKED, |
| await passwordManager.whenCalled('recordPasswordViewInteraction')); |
| await flushTasks(); |
| |
| const editDialog = |
| card.shadowRoot!.querySelector<EditPasswordDialogElement>( |
| 'edit-password-dialog'); |
| assertTrue(!!editDialog); |
| assertTrue(editDialog.$.dialog.open); |
| }); |
| |
| test('delete password', async function() { |
| const password = createPasswordEntry({ |
| url: 'test.com', |
| username: 'vik', |
| id: 0, |
| }); |
| |
| const card = await createCardElement(password); |
| |
| assertTrue(isVisible(card.$.deleteButton)); |
| |
| card.$.deleteButton.click(); |
| assertEquals( |
| PasswordViewPageInteractions.PASSWORD_DELETE_BUTTON_CLICKED, |
| await passwordManager.whenCalled('recordPasswordViewInteraction')); |
| |
| const params = await passwordManager.whenCalled('removeCredential'); |
| assertEquals(params.id, password.id); |
| assertEquals(params.fromStores, password.storedIn); |
| }); |
| |
| [chrome.passwordsPrivate.PasswordStoreSet.DEVICE_AND_ACCOUNT, |
| chrome.passwordsPrivate.PasswordStoreSet.DEVICE, |
| chrome.passwordsPrivate.PasswordStoreSet.ACCOUNT] |
| .forEach( |
| store => test( |
| `delete multi store password from ${store} `, async function() { |
| const password = createPasswordEntry({ |
| url: 'test.com', |
| username: 'vik', |
| id: 0, |
| }); |
| password.affiliatedDomains = [ |
| createAffiliatedDomain('test.com'), |
| createAffiliatedDomain('m.test.com'), |
| ]; |
| password.storedIn = |
| chrome.passwordsPrivate.PasswordStoreSet.DEVICE_AND_ACCOUNT; |
| |
| const card = await createCardElement(password); |
| |
| assertTrue(isVisible(card.$.deleteButton)); |
| |
| card.$.deleteButton.click(); |
| await flushTasks(); |
| |
| // Verify that password was not deleted immediately. |
| assertEquals( |
| 0, passwordManager.getCallCount('removeCredential')); |
| |
| const deleteDialog = card.shadowRoot!.querySelector( |
| 'multi-store-delete-password-dialog'); |
| assertTrue(!!deleteDialog); |
| assertTrue(deleteDialog.$.dialog.open); |
| |
| assertTrue(deleteDialog.$.removeFromAccountCheckbox.checked); |
| assertTrue(deleteDialog.$.removeFromDeviceCheckbox.checked); |
| |
| if (store === chrome.passwordsPrivate.PasswordStoreSet.DEVICE) { |
| deleteDialog.$.removeFromAccountCheckbox.click(); |
| await deleteDialog.$.removeFromAccountCheckbox.updateComplete; |
| } else if ( |
| store === |
| chrome.passwordsPrivate.PasswordStoreSet.ACCOUNT) { |
| deleteDialog.$.removeFromDeviceCheckbox.click(); |
| await deleteDialog.$.removeFromDeviceCheckbox.updateComplete; |
| } |
| deleteDialog.$.removeButton.click(); |
| |
| const params = |
| await passwordManager.whenCalled('removeCredential'); |
| assertEquals(password.id, params.id); |
| assertEquals(store, params.fromStores); |
| })); |
| |
| test('delete disabled when no store selected', async function() { |
| const password = createPasswordEntry({ |
| url: 'test.com', |
| username: 'vik', |
| id: 0, |
| inAccountStore: true, |
| inProfileStore: true, |
| }); |
| password.affiliatedDomains = [ |
| createAffiliatedDomain('test.com'), |
| createAffiliatedDomain('m.test.com'), |
| ]; |
| |
| const card = await createCardElement(password); |
| |
| assertTrue(isVisible(card.$.deleteButton)); |
| |
| card.$.deleteButton.click(); |
| await flushTasks(); |
| |
| // Verify that password was not deleted immediately. |
| assertEquals(0, passwordManager.getCallCount('removeCredential')); |
| |
| const deleteDialog = |
| card.shadowRoot!.querySelector('multi-store-delete-password-dialog'); |
| assertTrue(!!deleteDialog); |
| assertTrue(deleteDialog.$.dialog.open); |
| deleteDialog.$.removeFromAccountCheckbox.click(); |
| await deleteDialog.$.removeFromAccountCheckbox.updateComplete; |
| deleteDialog.$.removeFromDeviceCheckbox.click(); |
| await deleteDialog.$.removeFromDeviceCheckbox.updateComplete; |
| |
| assertFalse(deleteDialog.$.removeFromAccountCheckbox.checked); |
| assertFalse(deleteDialog.$.removeFromDeviceCheckbox.checked); |
| |
| assertTrue(deleteDialog.$.removeButton.disabled); |
| }); |
| |
| test('Sites title', async function() { |
| const password = createPasswordEntry( |
| {url: 'test.com', username: 'vik', password: 'password69'}); |
| password.affiliatedDomains = [ |
| { |
| name: 'test.com', |
| url: 'https://test.com', |
| signonRealm: 'https://test.com/', |
| }, |
| ]; |
| |
| const card = await createCardElement(password); |
| |
| assertEquals( |
| card.$.domainLabel.textContent!.trim(), |
| loadTimeData.getString('sitesLabel')); |
| }); |
| |
| test('Apps title', async function() { |
| const password = createPasswordEntry( |
| {url: 'test.com', username: 'vik', password: 'password69'}); |
| password.affiliatedDomains = [ |
| { |
| name: 'test.com', |
| url: 'https://test.com', |
| signonRealm: 'android://someHash/', |
| }, |
| ]; |
| |
| const card = await createCardElement(password); |
| |
| assertEquals( |
| card.$.domainLabel.textContent!.trim(), |
| loadTimeData.getString('appsLabel')); |
| }); |
| |
| test('Apps and sites title', async function() { |
| const password = createPasswordEntry( |
| {url: 'test.com', username: 'vik', password: 'password69'}); |
| password.affiliatedDomains = [ |
| { |
| name: 'test.com', |
| url: 'https://test.com', |
| signonRealm: 'android://someHash/', |
| }, |
| { |
| name: 'test.com', |
| url: 'https://test.com', |
| signonRealm: 'https://test.com/', |
| }, |
| ]; |
| |
| const card = await createCardElement(password); |
| |
| assertEquals( |
| card.$.domainLabel.textContent!.trim(), |
| loadTimeData.getString('sitesAndAppsLabel')); |
| }); |
| |
| test('share button available when sync enabled', async function() { |
| loadTimeData.overrideValues({enableSendPasswords: true}); |
| |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: false, |
| isSyncingPasswords: true, |
| }; |
| |
| const card = await createCardElement(); |
| |
| assertTrue(isVisible(card.$.shareButton)); |
| assertEquals(card.$.shareButton.textContent!.trim(), card.i18n('share')); |
| |
| assertFalse(!!card.shadowRoot!.querySelector('share-password-flow')); |
| |
| // Share flow should become available after the button click. |
| card.$.shareButton.click(); |
| await passwordManager.whenCalled('fetchFamilyMembers'); |
| await flushTasks(); |
| |
| const shareFlow = card.shadowRoot!.querySelector('share-password-flow'); |
| assertTrue(!!shareFlow); |
| }); |
| |
| test('share button available for account store users', async function() { |
| loadTimeData.overrideValues({enableSendPasswords: true}); |
| |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: true, |
| isSyncingPasswords: false, |
| }; |
| |
| passwordManager.data.isOptedInAccountStorage = true; |
| |
| const card = await createCardElement(); |
| |
| assertFalse(card.$.shareButton.hidden); |
| assertTrue(isVisible(card.$.shareButton)); |
| assertFalse(card.$.shareButton.disabled); |
| assertEquals(card.$.shareButton.textContent!.trim(), card.i18n('share')); |
| }); |
| |
| test('sharing disabled by policy', async function() { |
| loadTimeData.overrideValues({enableSendPasswords: true}); |
| |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: false, |
| isSyncingPasswords: true, |
| }; |
| |
| const card = document.createElement('password-details-card'); |
| card.password = createPasswordEntry(); |
| card.prefs = makePasswordManagerPrefs(); |
| card.prefs.password_manager.password_sharing_enabled.value = false; |
| card.prefs.password_manager.password_sharing_enabled.enforcement = |
| chrome.settingsPrivate.Enforcement.ENFORCED; |
| document.body.appendChild(card); |
| await flushTasks(); |
| |
| assertTrue(isVisible(card.$.shareButton)); |
| assertTrue(card.$.shareButton.disabled); |
| }); |
| |
| test('sharing unavailable for federated credentials', async function() { |
| loadTimeData.overrideValues({enableSendPasswords: true}); |
| |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: false, |
| isSyncingPasswords: true, |
| }; |
| |
| const card = |
| await createCardElement(createPasswordEntry({federationText: 'text'})); |
| |
| assertFalse(isVisible(card.$.shareButton)); |
| |
| const sharePasswordFlow = |
| card.shadowRoot!.querySelector('share-password-flow'); |
| assertFalse(!!sharePasswordFlow); |
| }); |
| |
| test('sharing unavailable without enableSendPasswords', async function() { |
| loadTimeData.overrideValues({enableSendPasswords: false}); |
| |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: false, |
| isSyncingPasswords: true, |
| }; |
| |
| const card = await createCardElement(); |
| |
| assertFalse(isVisible(card.$.shareButton)); |
| |
| const sharePasswordFlow = |
| card.shadowRoot!.querySelector('share-password-flow'); |
| assertFalse(!!sharePasswordFlow); |
| }); |
| |
| test('share button unavailable when sync disabled', async function() { |
| loadTimeData.overrideValues({enableSendPasswords: true}); |
| |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: false, |
| isSyncingPasswords: false, |
| }; |
| |
| const card = await createCardElement(); |
| |
| assertFalse(isVisible(card.$.shareButton)); |
| |
| const sharePasswordFlow = |
| card.shadowRoot!.querySelector('share-password-flow'); |
| assertFalse(!!sharePasswordFlow); |
| }); |
| |
| test( |
| 'clicking save password in account opens move password dialog', |
| async function() { |
| loadTimeData.overrideValues({enableButterOnDesktopFollowup: true}); |
| passwordManager.data.isOptedInAccountStorage = true; |
| syncProxy.syncInfo = { |
| isEligibleForAccountStorage: true, |
| isSyncingPasswords: false, |
| }; |
| |
| const card = await createCardElement(); |
| card.isUsingAccountStore = true; |
| await flushTasks(); |
| |
| const movePasswordLabel = card!.shadowRoot!.querySelector<HTMLElement>( |
| '.move-password-container div'); |
| assertTrue(!!movePasswordLabel); |
| assertTrue(isVisible(movePasswordLabel)); |
| |
| movePasswordLabel!.click(); |
| await flushTasks(); |
| |
| const moveDialog = |
| card.shadowRoot!.querySelector('move-single-password-dialog'); |
| assertTrue(!!moveDialog); |
| const dialog = moveDialog!.shadowRoot!.querySelector('#dialog'); |
| assertTrue(!!dialog); |
| }); |
| |
| test('Password value is hidden if object was changed', async function() { |
| const password1 = createPasswordEntry( |
| {id: 1, url: 'test.com', username: 'vik', password: 'password69'}); |
| password1.affiliatedDomains = [createAffiliatedDomain('test.com')]; |
| |
| const password2 = createPasswordEntry( |
| {id: 1, url: 'test.com', username: 'viktor', password: 'password69'}); |
| password2.affiliatedDomains = [createAffiliatedDomain('test.com')]; |
| |
| const card = await createCardElement(password1); |
| card.isPasswordVisible = true; |
| |
| card.password = password2; |
| assertFalse(card.isPasswordVisible); |
| }); |
| }); |