| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** @fileoverview Runs the Polymer tests for the PasswordsDeviceSection page. */ |
| |
| import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| import {MultiStorePasswordUiEntry, PasswordManagerImpl, Router, routes, SyncBrowserProxyImpl} from 'chrome://settings/settings.js'; |
| import {createMultiStorePasswordEntry, createPasswordEntry, PasswordDeviceSectionElementFactory} from 'chrome://test/settings/passwords_and_autofill_fake_data.js'; |
| import {simulateStoredAccounts, simulateSyncStatus} from 'chrome://test/settings/sync_test_util.m.js'; |
| import {TestPasswordManagerProxy} from 'chrome://test/settings/test_password_manager_proxy.js'; |
| import {TestSyncBrowserProxy} from 'chrome://test/settings/test_sync_browser_proxy.m.js'; |
| import {eventToPromise} from 'chrome://test/test_util.m.js'; |
| import {assertEquals, assertTrue} from '../chai_assert.js'; |
| |
| /** |
| * Sets the fake password data, the appropriate route and creates the element. |
| * @param {!TestSyncBrowserProxy} syncBrowserProxy |
| * @param {!TestPasswordManagerProxy} passwordManager |
| * @param {!Array<!chrome.passwordsPrivate.PasswordUiEntry>} passwordList |
| * @return {!Object} |
| */ |
| async function createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, passwordList) { |
| passwordManager.data.passwords = passwordList; |
| Router.getInstance().setCurrentRoute( |
| routes.DEVICE_PASSWORDS, new URLSearchParams()); |
| const passwordsDeviceSection = |
| document.createElement('passwords-device-section'); |
| document.body.appendChild(passwordsDeviceSection); |
| flush(); |
| |
| // Wait for the initial state of sync and account storage opt in to be queried |
| // since this could cause a redirect. |
| await syncBrowserProxy.whenCalled('getSyncStatus'); |
| await syncBrowserProxy.whenCalled('getStoredAccounts'); |
| await passwordManager.whenCalled('isOptedInForAccountStorage'); |
| |
| return passwordsDeviceSection; |
| } |
| |
| /** |
| * @param {!Element} subsection The passwords subsection element that will be |
| * checked. |
| * @param {!Array<!MultiStorePasswordUiEntry>} expectedPasswords The |
| * expected passwords in this subsection. |
| * @private |
| */ |
| function validatePasswordsSubsection(subsection, expectedPasswords) { |
| assertDeepEquals(expectedPasswords, subsection.items); |
| const listItemElements = subsection.querySelectorAll('password-list-item'); |
| for (let index = 0; index < expectedPasswords.length; ++index) { |
| const expectedPassword = expectedPasswords[index]; |
| const listItemElement = listItemElements[index]; |
| assertTrue(!!listItemElement); |
| assertEquals( |
| expectedPassword.urls.shown, |
| listItemElement.$.originUrl.textContent.trim()); |
| assertEquals(expectedPassword.urls.link, listItemElement.$.originUrl.href); |
| assertEquals(expectedPassword.username, listItemElement.$.username.value); |
| } |
| } |
| |
| suite('PasswordsDeviceSection', function() { |
| /** @type {TestPasswordManagerProxy} */ |
| let passwordManager = null; |
| /** @type {TestSyncBrowserProxy} */ |
| let syncBrowserProxy = null; |
| /** @type {!settings.StoredAccount} */ |
| const SIGNED_IN_ACCOUNT = {email: 'john@gmail.com'}; |
| /** @type {PasswordDeviceSectionElementFactory} */ |
| let elementFactory = null; |
| |
| setup(function() { |
| PolymerTest.clearBody(); |
| passwordManager = new TestPasswordManagerProxy(); |
| PasswordManagerImpl.instance_ = passwordManager; |
| syncBrowserProxy = new TestSyncBrowserProxy(); |
| SyncBrowserProxyImpl.instance_ = syncBrowserProxy; |
| elementFactory = new PasswordDeviceSectionElementFactory(document); |
| |
| // The user only enters this page when they are eligible (signed-in but not |
| // syncing) and opted-in to account storage. |
| syncBrowserProxy.storedAccounts = [SIGNED_IN_ACCOUNT]; |
| simulateStoredAccounts(syncBrowserProxy.storedAccounts); |
| syncBrowserProxy.syncStatus = {signedIn: false}; |
| simulateSyncStatus(syncBrowserProxy.syncStatus); |
| passwordManager.setIsOptedInForAccountStorageAndNotify(true); |
| }); |
| |
| // Test verifies that the fallback text is displayed when passwords are not |
| // present. |
| test('verifyPasswordsEmptySubsections', async function() { |
| const passwordsDeviceSection = await createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, []); |
| assertFalse(passwordsDeviceSection.shadowRoot |
| .querySelector('#noDeviceOnlyPasswordsLabel') |
| .hidden); |
| assertFalse(passwordsDeviceSection.shadowRoot |
| .querySelector('#noDeviceAndAccountPasswordsLabel') |
| .hidden); |
| }); |
| |
| // Test verifies that account passwords are not displayed, whereas |
| // device-only and device-and-account ones end up in the correct subsection. |
| test('verifyPasswordsFilledSubsections', async function() { |
| const devicePassword = createPasswordEntry( |
| {username: 'device', id: 0, fromAccountStore: false}); |
| const accountPassword = createPasswordEntry( |
| {username: 'account', id: 1, fromAccountStore: true}); |
| // Create duplicate that gets merged. |
| const deviceCopyPassword = createPasswordEntry( |
| {username: 'both', frontendId: 42, id: 2, fromAccountStore: false}); |
| const accountCopyPassword = createPasswordEntry( |
| {username: 'both', frontendId: 42, id: 3, fromAccountStore: true}); |
| |
| // Shuffle entries a little. |
| const passwordsDeviceSection = |
| await createPasswordsDeviceSection(syncBrowserProxy, passwordManager, [ |
| devicePassword, |
| deviceCopyPassword, |
| accountPassword, |
| accountCopyPassword, |
| ]); |
| |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceOnlyPasswordList, [ |
| createMultiStorePasswordEntry({username: 'device', deviceId: 0}), |
| ]); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceAndAccountPasswordList, [ |
| createMultiStorePasswordEntry( |
| {username: 'both', deviceId: 2, accountId: 3}), |
| ]); |
| assertTrue(passwordsDeviceSection.shadowRoot |
| .querySelector('#noDeviceOnlyPasswordsLabel') |
| .hidden); |
| assertTrue(passwordsDeviceSection.shadowRoot |
| .querySelector('#noDeviceAndAccountPasswordsLabel') |
| .hidden); |
| }); |
| |
| // Test verifies that removing the device copy of a duplicated password |
| // removes it from both lists. |
| test('verifyPasswordListRemoveDeviceCopy', async function() { |
| const passwordList = [ |
| createPasswordEntry({frontendId: 42, id: 10, fromAccountStore: true}), |
| createPasswordEntry({frontendId: 42, id: 20, fromAccountStore: false}), |
| ]; |
| |
| const passwordsDeviceSection = await createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, passwordList); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceOnlyPasswordList, []); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceAndAccountPasswordList, |
| [createMultiStorePasswordEntry({accountId: 10, deviceId: 20})]); |
| |
| // Remove device copy. |
| passwordList.splice(1, 1); |
| passwordManager.lastCallback.addSavedPasswordListChangedListener( |
| passwordList); |
| flush(); |
| |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceOnlyPasswordList, []); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceAndAccountPasswordList, []); |
| }); |
| |
| // Test verifies that removing the account copy of a duplicated password |
| // moves it to the other subsection. |
| test('verifyPasswordListRemoveDeviceCopy', async function() { |
| const passwordList = [ |
| createPasswordEntry({frontendId: 42, id: 10, fromAccountStore: true}), |
| createPasswordEntry({frontendId: 42, id: 20, fromAccountStore: false}), |
| ]; |
| |
| const passwordsDeviceSection = await createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, passwordList); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceOnlyPasswordList, []); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceAndAccountPasswordList, |
| [createMultiStorePasswordEntry({accountId: 10, deviceId: 20})]); |
| |
| // Remove account copy. |
| passwordList.splice(0, 1); |
| passwordManager.lastCallback.addSavedPasswordListChangedListener( |
| passwordList); |
| flush(); |
| |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceOnlyPasswordList, |
| [createMultiStorePasswordEntry({deviceId: 20})]); |
| validatePasswordsSubsection( |
| passwordsDeviceSection.$.deviceAndAccountPasswordList, []); |
| }); |
| |
| // Test checks that when the overflow menu is opened for any password not |
| // corresponding to the first signed-in account, an option to move it to that |
| // account is shown. |
| test( |
| 'hasMoveToAccountOptionIfIsNotSignedInAccountPassword', async function() { |
| const nonGooglePasswordWithSameEmail = createPasswordEntry( |
| {username: SIGNED_IN_ACCOUNT.email, url: 'not-google.com'}); |
| const googlePasswordWithDifferentEmail = createPasswordEntry( |
| {username: 'another-user', url: 'accounts.google.com'}); |
| const passwordsDeviceSection = await createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, |
| [nonGooglePasswordWithSameEmail, googlePasswordWithDifferentEmail]); |
| const passwordElements = |
| passwordsDeviceSection.root.querySelectorAll('password-list-item'); |
| |
| passwordElements[0].$.moreActionsButton.click(); |
| flush(); |
| let moveToAccountOption = passwordsDeviceSection.$.passwordsListHandler |
| .$.menuMovePasswordToAccount; |
| assertFalse(moveToAccountOption.hidden); |
| |
| passwordsDeviceSection.$.passwordsListHandler.$.menu.close(); |
| |
| passwordElements[1].$.moreActionsButton.click(); |
| flush(); |
| moveToAccountOption = passwordsDeviceSection.$.passwordsListHandler.$ |
| .menuMovePasswordToAccount; |
| assertFalse(moveToAccountOption.hidden); |
| }); |
| |
| // Test checks that when the overflow menu is opened for the password |
| // corresponding to the first signed-in account, no option to move it to the |
| // same account is shown. |
| test('hasNoMoveToAccountOptionIfIsSignedInAccountPassword', async function() { |
| const signedInGoogleAccountPassword = createPasswordEntry( |
| {username: SIGNED_IN_ACCOUNT.email, url: 'accounts.google.com'}); |
| const passwordsDeviceSection = await createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, [signedInGoogleAccountPassword]); |
| const [password] = |
| passwordsDeviceSection.root.querySelectorAll('password-list-item'); |
| |
| password.$.moreActionsButton.click(); |
| flush(); |
| const moveToAccountOption = passwordsDeviceSection.$.passwordsListHandler.$ |
| .menuMovePasswordToAccount; |
| assertTrue(moveToAccountOption.hidden); |
| }); |
| |
| |
| // Test verifies that clicking the 'move to account' button displays the |
| // dialog and that clicking the "Move" button then moves the device copy. |
| test('verifyMovesCorrectIdToAccount', async function() { |
| // Create duplicated password that will be merged in the UI. |
| const accountCopy = createPasswordEntry( |
| {user: 'both', id: 2, frontendId: 42, fromAccountStore: true}); |
| const deviceCopy = createPasswordEntry( |
| {user: 'both', id: 1, frontendId: 42, fromAccountStore: false}); |
| const passwordsDeviceSection = await createPasswordsDeviceSection( |
| syncBrowserProxy, passwordManager, [deviceCopy, accountCopy]); |
| |
| // At first the dialog is not shown. |
| assertFalse(!!passwordsDeviceSection.$.passwordsListHandler.$$( |
| '#passwordMoveToAccountDialog')); |
| |
| // Click the option in the overflow menu to move the password. Verify the |
| // dialog is now open. |
| const [password] = |
| passwordsDeviceSection.root.querySelectorAll('password-list-item'); |
| password.$.moreActionsButton.click(); |
| passwordsDeviceSection.$.passwordsListHandler.$.menuMovePasswordToAccount |
| .click(); |
| flush(); |
| const moveToAccountDialog = |
| passwordsDeviceSection.$.passwordsListHandler.$$( |
| '#passwordMoveToAccountDialog'); |
| assertTrue(!!moveToAccountDialog); |
| |
| // Click the Move button in the dialog. The API should be called with the id |
| // for the device copy. Verify the dialog disappears. |
| moveToAccountDialog.$.moveButton.click(); |
| const movedId = await passwordManager.whenCalled('movePasswordsToAccount'); |
| assertEquals(deviceCopy.id, movedId[0]); |
| }); |
| |
| // Test verifies that Chrome navigates to the standard passwords page if the |
| // user enables sync. |
| test('leavesPageIfSyncIsEnabled', async function() { |
| await createPasswordsDeviceSection(syncBrowserProxy, passwordManager, []); |
| assertEquals(Router.getInstance().currentRoute, routes.DEVICE_PASSWORDS); |
| simulateSyncStatus({signedIn: true}); |
| flush(); |
| assertEquals(Router.getInstance().currentRoute, routes.PASSWORDS); |
| }); |
| |
| // Test verifies that Chrome navigates to the standard passwords page if the |
| // user signs out. |
| test('leavesPageIfUserSignsOut', async function() { |
| await createPasswordsDeviceSection(syncBrowserProxy, passwordManager, []); |
| assertEquals(Router.getInstance().currentRoute, routes.DEVICE_PASSWORDS); |
| simulateStoredAccounts([]); |
| flush(); |
| assertEquals(Router.getInstance().currentRoute, routes.PASSWORDS); |
| }); |
| |
| // Test verifies that Chrome navigates to the standard passwords page if the |
| // user opts out of the account-scoped password storage. |
| test('leavesPageIfUserOptsOut', async function() { |
| await createPasswordsDeviceSection(syncBrowserProxy, passwordManager, []); |
| assertEquals(Router.getInstance().currentRoute, routes.DEVICE_PASSWORDS); |
| passwordManager.setIsOptedInForAccountStorageAndNotify(false); |
| flush(); |
| assertEquals(Router.getInstance().currentRoute, routes.PASSWORDS); |
| }); |
| |
| // The move multiple password dialog is dismissable. |
| test('moveMultiplePasswordsDialogDismissable', function() { |
| const deviceEntry = createMultiStorePasswordEntry( |
| {url: 'goo.gl', username: 'bart', deviceId: 42}); |
| const moveMultipleDialog = |
| elementFactory.createMoveMultiplePasswordsDialog([deviceEntry]); |
| assertTrue(moveMultipleDialog.$.dialog.open); |
| moveMultipleDialog.$.cancelButton.click(); |
| flush(); |
| assertFalse(moveMultipleDialog.$.dialog.open); |
| }); |
| |
| test('moveMultiplePasswordsDialogFiresCloseEventWhenCanceled', function() { |
| const deviceEntry = createMultiStorePasswordEntry( |
| {url: 'goo.gl', username: 'bart', deviceId: 42}); |
| const moveMultipleDialog = |
| elementFactory.createMoveMultiplePasswordsDialog([deviceEntry]); |
| moveMultipleDialog.$.cancelButton.click(); |
| return eventToPromise('close', moveMultipleDialog); |
| }); |
| |
| // Testing moving multiple password dialog Move button. |
| test('moveMultiplePasswordsDialogMoveButton', async function() { |
| const deviceEntry1 = createMultiStorePasswordEntry( |
| {url: 'goo.gl', username: 'bart1', deviceId: 41}); |
| const deviceEntry2 = createMultiStorePasswordEntry( |
| {url: 'goo.gl', username: 'bart2', deviceId: 54}); |
| const moveMultipleDialog = elementFactory.createMoveMultiplePasswordsDialog( |
| [deviceEntry1, deviceEntry2]); |
| // Uncheck the first entry. |
| const firstPasswordItem = moveMultipleDialog.$$('password-list-item'); |
| firstPasswordItem.querySelector('cr-checkbox').click(); |
| // Press the Move button |
| moveMultipleDialog.$.moveButton.click(); |
| flush(); |
| // Only the 2nd entry should be moved |
| const movedIds = await passwordManager.whenCalled('movePasswordsToAccount'); |
| assertEquals(1, movedIds.length); |
| assertEquals(deviceEntry2.deviceId, movedIds[0]); |
| // The dialog should be closed. |
| assertFalse(moveMultipleDialog.$.dialog.open); |
| }); |
| |
| // Testing moving multiple password dialog doesn't have more actions menu |
| // button next to each password row.. |
| test('moveMultiplePasswordsDialogNoMoreActionButton', function() { |
| const deviceEntry = createMultiStorePasswordEntry( |
| {url: 'goo.gl', username: 'bart', deviceId: 42}); |
| const moveMultipleDialog = |
| elementFactory.createMoveMultiplePasswordsDialog([deviceEntry]); |
| const firstPasswordItem = moveMultipleDialog.$$('password-list-item'); |
| assertTrue(firstPasswordItem.$.moreActionsButton.hidden); |
| }); |
| }); |