1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-11 10:10:25 +01:00

[PM-2858] Inline menu identity autofill (#9900)

* [PM-2857] Introducing logic that handles adding a credit card from the inline menu

* [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details

* [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details

* [PM-9342] Changing logic for how we identify new password fields to reflect a more assertive qualification

* [PM-2857] Fixing an issue with how we identify ciphers in the inline menu

* [PM-2857] Working through issues when adding a cipher from the inline menu for credit card ciphers

* [PM-2857] Working through issues when adding a cipher from the inline menu for credit card ciphers

* [PM-2857] Fixing an issue encountered with updating credit card info within the add/edit view

* [PM-9342] Adding feedback from code review

* [PM-5189] Fixing an issue where the port key for an inline menu element could potentially be undefined if the window focus changes too quickly

* [PM-2857] Refactoring implementation for how we getCipherViews to ensure we only query card items when necessary

* [PM-2857] Refactoring implementation to simplify how we create cipherViews when adding a new item

* [PM-2857] Fixing an issue with how we store identity and card cipher views

* [PM-2857] Fixing an issue with how we store identity and card cipher views

* [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements

* [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements

* [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements

* [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements

* [PM-2857] Fixing an issue with how we store identity and card cipher views

* [PM-2857] Finalizing jest tests

* [PM-2857] Finalizing jest tests

* [PM-2857] Adjusting an aspect of the inline menu icon

* [PM-2857] Adjusting aspect of inline menu field qualification

* [PM-2858] Inline menu identities autofill

* [PM-2857] Adjusting aspect of inline menu field qualification

* [PM-2858] Inline menu identities autofill

* [PM-2858] Incorporating logic required to selectively show and fill identity ciphers

* [PM-2858] Updating copy for unlock state to be generic

* [PM-2857] Updating copy for unlock state to be generic

* [PM-2857] Updating copy for unlock state to be generic

* [PM-2858] Updating copy for unlock state to be generic

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* PM-4950 - Fix hint and verify delete components that had the data in the wrong place (#9877)

* PM-4661: Add passkey.username as item.username (#9756)

* Add incoming passkey.username as item.username

* Driveby fix, was sending wrong username

* added username to new-cipher too

* Guarded the if-block

* Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fixed broken test

* fixed username on existing ciphers

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* PM-4878: Add passkey information to items when signing in (#9835)

* Added username to subtitle

* Added subName to cipher

* Moved subName to component

* Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* Fixed double code and added comment

* Added changeDetection: ChangeDetectionStrategy.OnPush as per review

---------

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* [AC-2791] Members page - finish component library refactors (#9727)

* Replace PlatformUtilsService with ToastService

* Remove unneeded templates

* Implement table filtering function

* Move member-only methods from base class to subclass

* Move utility functions inside new MemberTableDataSource

* Rename PeopleComponent to MembersComponent

* [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-8789] Move desktop_native into subcrate (#9682)

* Move desktop_native into subcrate

* Add publish = false to crates

* [PM-6394] remove policy evaluator cache (#9807)

* [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875)

* Change the seat adjustment message

* Move changes from en_GB file to en file

* revert changes in en_GB file

* Add feature flag to the change

* use user verification as a part of key rotation (#9722)

* Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770)

* Introduce `verificationType`

* Update template to use `verificationType`

* Implement a path for `verificationType = 'custom'`

* Delete `clientSideOnlyVerification`

* Update `EnrollMasterPasswordResetComponent` to include a server-side hash check

* Better describe the custom scenerio through comments

* Add an example of the custom verficiation scenerio

* Move execution of verification function into try/catch

* Migrate existing uses of `clientSideOnlyVerification`

* Use generic type option instead of casting

* Change "given" to "determined" in a comment

* Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552)

* Document the `org-redirect` guard in code

* Make assertions about the way the `org-redirect` guard should behave

* Restructure the `org-redirect` guard to be Angular 17+ compliant

* Convert data parameter to function parameter

* Convert a data parameter to a function parameter that was missed

* Pass redirect function to default organization route

* don't initialize kdf with validators, do it on first set (#9754)

* add testids for attachments (#9892)

* Bug fix - error toast in 2fa (#9623)

* Bug fix - error toast in 2fa

* Bug fix - Yubikey code obscured

* 2FA error fix

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-2858] Fixing icon color

* [PM-2858] Adding subtitle for identity inline menu list items

* [PM-2858] Fixing jest tests

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Scaffolding add new identity logic

* [PM-2858] Implementing add new identity

* [PM-2858] Implementing add new identity

* [PM-2858] Scaffolding add new identity logic

* [PM-2858] Scaffolding add new identity logic

* [PM-2858] Scaffolding add new identity logic

* [PM-2857] Fixing an issue with how we parse the last digits for credit card aria description

* [PM-2857] Setting up logic to ensrue we use a set email address as a fallback for a username

* [PM-2857] Fixing an issue with how we parse the last digits for credit card aria description

* [PM-2858] Reverting forced email address in inline menu identity autofill

* Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598)

* Document that `is-paid-org` guard in code

* Remove unused `MessagingService` dependency

* Make assertions about the way the is-paid-org guard should behave

* Restructure the `is-paid-org` guard to be Angular 17+ compliant

* Random commit to get the build job moving

* Undo previous commit

* Bumped client version(s) (#9895)

* [PM-9344] Clarify accepted user state (#9861)

* Prefer `Needs confirmation` to `Accepted` display status

This emphasizes that action is still required to complete setup.

* Remove unused message

* Bumped client version(s) (#9906)

* Revert "Bumped client version(s) (#9906)" (#9907)

This reverts commit 78c2829793.

* fix duo subscriptions and org vs individual duo setup (#9859)

* [PM-5024] Migrate tax-info component (#9872)

* Changes for the tax info migration

* Return for invalid formgroup

* Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631)

* Document the `org-permissions` guard in code

* Restructure the `org-permissions` guard to be Angular 17+ compliant

* Update the `org-permissions` guard to use `ToastService`

* Simplify callback function sigantures

* Remove unused test object

* Fix updated route from merge

* Restructure the `provider-permissions` guard to be Angular 17+ compliant  (#9609)

* Document the `provider-permissions` guard in code

* Restructure the `provider-permissions` guard to be Angular 17+ compliant

* [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bumped client version(s) (#9914)

* [PM-7162] Cipher Form - Item Details (#9758)

* [PM-7162] Fix weird angular error regarding disabled component bit-select

* [PM-7162] Introduce CipherFormConfigService and related types

* [PM-7162] Introduce CipherFormService

* [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface

* [PM-7162] Introduce the CipherForm component

* [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component

* [PM-7162] Export CipherForm from Vault Lib

* [PM-7162] Use the CipherForm in Browser AddEditV2

* [PM-7162] Introduce CipherForm storybook

* [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component

* [PM-7162] Add support for content projection of attachment button

* [PM-7162] Fix typo

* [PM-7162] Cipher form service cleanup

* [PM-7162] Move readonly collection notice to bit-hint

* [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript

* [PM-7162] Fix storybook after config changes

* [PM-7162] Use new add-edit component for clone route

* [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Autosync the updated translations (#9922)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9923)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9924)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* [AC-2830] Unable to create a free organization (#9917)

* Resolve the issue free org creation

* Check that the taxForm is touched

* [PM-7162] Fix broken getter when original cipher is null (#9927)

* [PM-8525] Edit Card (#9901)

* initial add of card details section

* add card number

* update card brand when the card number changes

* add year and month fields

* add security code field

* hide number and security code by default

* add `id` for all form fields

* update select options to match existing options

* make year input numerical

* only display card details for card ciphers

* use style to set input height

* handle numerical values for year

* update heading when a brand is available

* remove unused ref

* use cardview types for the form

* fix numerical input type

* disable card details when in partial-edit mode

* remove hardcoded height

* update types for formBuilder

* [PM-9440] Fix: handle undefined value in migration 66 (#9908)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891)

* [PM-7972] Account switching integration with "remember email" functionality (#9750)

* add account switching logic to login email service

* enforce boolean and fix desktop account switcher order

* [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* feat: duplicate error behavior in fake storage service

* feat: fix all migrations that were setting undefined values

* feat: add test for disabled fingrint in migration 66

* fix: default single user state saving undefined value to state

* revert: awaiting floating promise

gonna fix this in a separate PR

* Revert "feat: fix all migrations that were setting undefined values"

This reverts commit 034713256c.

* feat: automatically convert save to remove

* Revert "fix: default single user state saving undefined value to state"

This reverts commit 6c36da6ba5.

* [AC-2805] Consolidated Billing UI Updates (#9893)

* Add empty state for invoices

* Make cards on create client dialog tabbable

* Add space in $ / month per member

* Mute text, remove (Monthly) and right align menu on clients table

* Made used seats account for all users and fixed column sort for used/remaining

* Resize pricing cards

* Rename assignedSeats to occupiedSeats

* [PM-9460][deps] Tools: Update electron to v31 (#9921)

* [deps] Tools: Update electron to v31

* Bump version in electron-builder

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483)

* Guard Organization Info route - Owners only

* Guard TwoFactor route - Owners only and Organization must be able to use 2FA

* Update guards to use function syntax

---------

Co-authored-by: Addison Beck <hello@addisonbeck.com>

* [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951)

* Add required env variables to desktop native build script (#9869)

* [AC-2676] Remove paging logic from GroupsComponent (#9705)

* remove infinite scroll, use virtual scroll instead
* use TableDataSource for search
* allow sorting by name
* replacing PlatformUtilsService.showToast with ToastService
* misc FIXMEs

* [PM-9441] Catch and log exceptions during migration (#9905)

* feat: catch and log exceptions during migration

* Revert "feat: catch and log exceptions during migration"

This reverts commit d68733b7e5.

* feat: use log service to log migration errors

* Autosync the updated translations (#9972)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9973)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Updated codeowners for new design system team (#9913)

* Updated codeowners for new design system team.

* Moved Angular and Bootstrap dependencies

* Moved additional dependencies.

* Updated ownership

Co-authored-by: Will Martin <contact@willmartian.com>

---------

Co-authored-by: Will Martin <contact@willmartian.com>

* [SM-1016] Fix new access token dialog (#9918)

* swap to bit-dialog title & subtitle

* remove dialogRef.disableClose & use toastService

* Add shared two-factor-options component (#9767)

* Communicate the upcoming client vault privacy changes to MSPs (#9994)

* Add a banner notification to the provider portal

* Feature flag the banner

* Move banner copy to messages.json

* Allow for dismissing the banner

* Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653)

* PM-7321 - Temp add input password

* PM-7321 - update input password based on latest PR changes to test.

* PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks

* PM-7321 - more progress on registration finish.

* PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method.

* PM-7321 - WIP Registration Finish - wiring up request building and API call on submit.

* PM-7321 - WIP registratin finish

* PM-7321 - WIP on creating registration-finish service + web override to add org invite handling

* PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service

* PM-7321 - Get CLI building

* PM-7321 - Move all finish registration service and content to registration-finish feature folder.

* PM-7321 - Fix RegistrationFinishService config

* PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP

* PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password)

* PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component.

* PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey

* PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic.

* PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled.

* PM-7321 - Registration Finish - use validation service for error handling.

* PM-7321 - All register routes must be dynamic and change if the feature flag changes.

* PM-7321 - Test registration finish services.

* PM-7321 - RegisterRouteService - Add comment documenting why the service exists.

* PM-7321 - Add missing input password translations to browser & desktop

* PM-7321 - WebRegistrationFinishSvc - apply PR feedback

* [deps] Autofill: Update rimraf to v5.0.8 (#10008)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-9318] Fix username on protonpass import (#9889)

* Fix username field used for ProtonPass import

ProtonPass has changed their export format and userName is not itemEmail

* Import additional field itemUsername

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926)

* create onload() for qrious as well as error messaging if QR code cannot be displayed

* button and message updates and formpromise removal

* load QR script async

* rename and reorder methods

* Delete Unused Bits of StateService (#9858)

* Delete Unused Bits of StateService

* Fix Tests

* remove getBgService for auth request service (#10020)

* [PM-2858] Fixing an issue found when the first or last names of an identity are not filled

* [PM-2858] Fixing an issue found where keyboard navigation can potentially close the inline menu

* [PM-2858] Fixing jest tests within inline menu list

* [PM-2858] Fixing jest tests within inline menu list

* [PM-2858] Setting up login items to be presented when an account creation form is shown to the user

* [PM-2858] Refactoring implementation used for creating the inline menu cipher data

* [PM-2858] Refactoring implementation used for creating the inline menu cipher data

* [PM-2858] Refactoring implementation used for creating the inline menu cipher data

* [PM-2858] Refactoring implementation

* [PM-2858] Refactoring implementation

* [PM-2858] Refactoring implementation

* [PM-2858] Refactoring implementation

* [PM-2858] Changing how we populate login ciphers within create account

* [PM-2858] Adding documentation

* [PM-2858] Working through jest tests for the OverlayBackground

* [PM-2858] Working through jest tests for the OverlayBackground

* [PM-2858] Working through jest tests for the AutofillInlineMenuList class

* [PM-2858] Adding documentation to inline menu list methods

* [PM-2857] Fixing a jest test

* [PM-2858] Fixing jest tests within inline menu list

* [PM-2858] Addressing jest tests within AutofillOverlayContentService

* [PM-2858] Addressing jest tests within AutofillOverlayContentService

* [PM-2858] Addressing jest tests within InlineMenuFieldQualificationService

* [PM-9267] Implement feature flag for inline menu re-architecture (#9845)

* [PM-9267] Implement Feature Flag for Inline Menu Re-Architecture

* [PM-9267] Incorporating legacy OverlayBackground implementation

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Adjusting naming convention for page files

* [PM-9267] Adjusting naming convention for page files

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* PM-4950 - Fix hint and verify delete components that had the data in the wrong place (#9877)

* PM-4661: Add passkey.username as item.username (#9756)

* Add incoming passkey.username as item.username

* Driveby fix, was sending wrong username

* added username to new-cipher too

* Guarded the if-block

* Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fixed broken test

* fixed username on existing ciphers

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* PM-4878: Add passkey information to items when signing in (#9835)

* Added username to subtitle

* Added subName to cipher

* Moved subName to component

* Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* Fixed double code and added comment

* Added changeDetection: ChangeDetectionStrategy.OnPush as per review

---------

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* [AC-2791] Members page - finish component library refactors (#9727)

* Replace PlatformUtilsService with ToastService

* Remove unneeded templates

* Implement table filtering function

* Move member-only methods from base class to subclass

* Move utility functions inside new MemberTableDataSource

* Rename PeopleComponent to MembersComponent

* [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-8789] Move desktop_native into subcrate (#9682)

* Move desktop_native into subcrate

* Add publish = false to crates

* [PM-6394] remove policy evaluator cache (#9807)

* [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875)

* Change the seat adjustment message

* Move changes from en_GB file to en file

* revert changes in en_GB file

* Add feature flag to the change

* use user verification as a part of key rotation (#9722)

* Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770)

* Introduce `verificationType`

* Update template to use `verificationType`

* Implement a path for `verificationType = 'custom'`

* Delete `clientSideOnlyVerification`

* Update `EnrollMasterPasswordResetComponent` to include a server-side hash check

* Better describe the custom scenerio through comments

* Add an example of the custom verficiation scenerio

* Move execution of verification function into try/catch

* Migrate existing uses of `clientSideOnlyVerification`

* Use generic type option instead of casting

* Change "given" to "determined" in a comment

* Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552)

* Document the `org-redirect` guard in code

* Make assertions about the way the `org-redirect` guard should behave

* Restructure the `org-redirect` guard to be Angular 17+ compliant

* Convert data parameter to function parameter

* Convert a data parameter to a function parameter that was missed

* Pass redirect function to default organization route

* don't initialize kdf with validators, do it on first set (#9754)

* add testids for attachments (#9892)

* Bug fix - error toast in 2fa (#9623)

* Bug fix - error toast in 2fa

* Bug fix - Yubikey code obscured

* 2FA error fix

* Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598)

* Document that `is-paid-org` guard in code

* Remove unused `MessagingService` dependency

* Make assertions about the way the is-paid-org guard should behave

* Restructure the `is-paid-org` guard to be Angular 17+ compliant

* Random commit to get the build job moving

* Undo previous commit

* Bumped client version(s) (#9895)

* [PM-9344] Clarify accepted user state (#9861)

* Prefer `Needs confirmation` to `Accepted` display status

This emphasizes that action is still required to complete setup.

* Remove unused message

* Bumped client version(s) (#9906)

* Revert "Bumped client version(s) (#9906)" (#9907)

This reverts commit 78c2829793.

* fix duo subscriptions and org vs individual duo setup (#9859)

* [PM-5024] Migrate tax-info component (#9872)

* Changes for the tax info migration

* Return for invalid formgroup

* Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631)

* Document the `org-permissions` guard in code

* Restructure the `org-permissions` guard to be Angular 17+ compliant

* Update the `org-permissions` guard to use `ToastService`

* Simplify callback function sigantures

* Remove unused test object

* Fix updated route from merge

* Restructure the `provider-permissions` guard to be Angular 17+ compliant  (#9609)

* Document the `provider-permissions` guard in code

* Restructure the `provider-permissions` guard to be Angular 17+ compliant

* [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bumped client version(s) (#9914)

* [PM-7162] Cipher Form - Item Details (#9758)

* [PM-7162] Fix weird angular error regarding disabled component bit-select

* [PM-7162] Introduce CipherFormConfigService and related types

* [PM-7162] Introduce CipherFormService

* [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface

* [PM-7162] Introduce the CipherForm component

* [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component

* [PM-7162] Export CipherForm from Vault Lib

* [PM-7162] Use the CipherForm in Browser AddEditV2

* [PM-7162] Introduce CipherForm storybook

* [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component

* [PM-7162] Add support for content projection of attachment button

* [PM-7162] Fix typo

* [PM-7162] Cipher form service cleanup

* [PM-7162] Move readonly collection notice to bit-hint

* [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript

* [PM-7162] Fix storybook after config changes

* [PM-7162] Use new add-edit component for clone route

* [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Autosync the updated translations (#9922)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9923)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9924)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* [AC-2830] Unable to create a free organization (#9917)

* Resolve the issue free org creation

* Check that the taxForm is touched

* [PM-7162] Fix broken getter when original cipher is null (#9927)

* [PM-8525] Edit Card (#9901)

* initial add of card details section

* add card number

* update card brand when the card number changes

* add year and month fields

* add security code field

* hide number and security code by default

* add `id` for all form fields

* update select options to match existing options

* make year input numerical

* only display card details for card ciphers

* use style to set input height

* handle numerical values for year

* update heading when a brand is available

* remove unused ref

* use cardview types for the form

* fix numerical input type

* disable card details when in partial-edit mode

* remove hardcoded height

* update types for formBuilder

* [PM-9440] Fix: handle undefined value in migration 66 (#9908)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891)

* [PM-7972] Account switching integration with "remember email" functionality (#9750)

* add account switching logic to login email service

* enforce boolean and fix desktop account switcher order

* [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* feat: duplicate error behavior in fake storage service

* feat: fix all migrations that were setting undefined values

* feat: add test for disabled fingrint in migration 66

* fix: default single user state saving undefined value to state

* revert: awaiting floating promise

gonna fix this in a separate PR

* Revert "feat: fix all migrations that were setting undefined values"

This reverts commit 034713256c.

* feat: automatically convert save to remove

* Revert "fix: default single user state saving undefined value to state"

This reverts commit 6c36da6ba5.

* [AC-2805] Consolidated Billing UI Updates (#9893)

* Add empty state for invoices

* Make cards on create client dialog tabbable

* Add space in $ / month per member

* Mute text, remove (Monthly) and right align menu on clients table

* Made used seats account for all users and fixed column sort for used/remaining

* Resize pricing cards

* Rename assignedSeats to occupiedSeats

* [PM-9460][deps] Tools: Update electron to v31 (#9921)

* [deps] Tools: Update electron to v31

* Bump version in electron-builder

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483)

* Guard Organization Info route - Owners only

* Guard TwoFactor route - Owners only and Organization must be able to use 2FA

* Update guards to use function syntax

---------

Co-authored-by: Addison Beck <hello@addisonbeck.com>

* [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951)

* Add required env variables to desktop native build script (#9869)

* [AC-2676] Remove paging logic from GroupsComponent (#9705)

* remove infinite scroll, use virtual scroll instead
* use TableDataSource for search
* allow sorting by name
* replacing PlatformUtilsService.showToast with ToastService
* misc FIXMEs

* [PM-9441] Catch and log exceptions during migration (#9905)

* feat: catch and log exceptions during migration

* Revert "feat: catch and log exceptions during migration"

This reverts commit d68733b7e5.

* feat: use log service to log migration errors

* Autosync the updated translations (#9972)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9973)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Updated codeowners for new design system team (#9913)

* Updated codeowners for new design system team.

* Moved Angular and Bootstrap dependencies

* Moved additional dependencies.

* Updated ownership

Co-authored-by: Will Martin <contact@willmartian.com>

---------

Co-authored-by: Will Martin <contact@willmartian.com>

* [SM-1016] Fix new access token dialog (#9918)

* swap to bit-dialog title & subtitle

* remove dialogRef.disableClose & use toastService

* Add shared two-factor-options component (#9767)

* Communicate the upcoming client vault privacy changes to MSPs (#9994)

* Add a banner notification to the provider portal

* Feature flag the banner

* Move banner copy to messages.json

* Allow for dismissing the banner

* Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653)

* PM-7321 - Temp add input password

* PM-7321 - update input password based on latest PR changes to test.

* PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks

* PM-7321 - more progress on registration finish.

* PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method.

* PM-7321 - WIP Registration Finish - wiring up request building and API call on submit.

* PM-7321 - WIP registratin finish

* PM-7321 - WIP on creating registration-finish service + web override to add org invite handling

* PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service

* PM-7321 - Get CLI building

* PM-7321 - Move all finish registration service and content to registration-finish feature folder.

* PM-7321 - Fix RegistrationFinishService config

* PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP

* PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password)

* PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component.

* PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey

* PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic.

* PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled.

* PM-7321 - Registration Finish - use validation service for error handling.

* PM-7321 - All register routes must be dynamic and change if the feature flag changes.

* PM-7321 - Test registration finish services.

* PM-7321 - RegisterRouteService - Add comment documenting why the service exists.

* PM-7321 - Add missing input password translations to browser & desktop

* PM-7321 - WebRegistrationFinishSvc - apply PR feedback

* [deps] Autofill: Update rimraf to v5.0.8 (#10008)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-9318] Fix username on protonpass import (#9889)

* Fix username field used for ProtonPass import

ProtonPass has changed their export format and userName is not itemEmail

* Import additional field itemUsername

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926)

* create onload() for qrious as well as error messaging if QR code cannot be displayed

* button and message updates and formpromise removal

* load QR script async

* rename and reorder methods

* Delete Unused Bits of StateService (#9858)

* Delete Unused Bits of StateService

* Fix Tests

* remove getBgService for auth request service (#10020)

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Addison Beck <github@addisonbeck.com>
Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Opeyemi <Alaoopeyemi101@gmail.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Bernd Schoolmann <mail@quexten.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>

* [PM-2858] Fixing an issue found where password fields addedin new account forms do not properly pull their value into the add cipher flow

* [PM-2858] Adjusting scrollbar stylings

* [PM-2858] Adjusting how we handle instantiating the feature flag guarded overlay background and how we handle instantiating identities and card ciphers in the inline menu

* [PM-2858] Adjusting how we handle instantiating the feature flag guarded overlay background and how we handle instantiating identities and card ciphers in the inline menu

* [PM-2858] Adjusting how we handle instantiating the feature flag guarded overlay background and how we handle instantiating identities and card ciphers in the inline menu

* [PM-2858] Incorporating some changes that ensure the inline menu list fades in as expected

* [PM-2858] Incorporating some changes that ensure the inline menu list fades in as expected

* [PM-2858] Incorporating some changes that ensure the inline menu list fades in as expected

* [PM-2858] Adjusting how we inject translations for a couple of aria label elements

* [PM-2858] Fixing duplicate globalThis references

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Addison Beck <github@addisonbeck.com>
Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Opeyemi <Alaoopeyemi101@gmail.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Bernd Schoolmann <mail@quexten.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>
This commit is contained in:
Cesar Gonzalez 2024-07-25 14:01:24 -05:00 committed by GitHub
parent 6830e471bb
commit c9d0cd207e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 3020 additions and 322 deletions

View File

@ -3054,6 +3054,10 @@
"message": "Unlock account",
"description": "Button text to display in overlay when the account is locked."
},
"unlockAccountAria": {
"message": "Unlock your account, opens in a new window",
"description": "Screen reader text (aria-label) for unlock account button in overlay"
},
"fillCredentialsFor": {
"message": "Fill credentials for",
"description": "Screen reader text for when overlay item is in focused"
@ -3078,18 +3082,26 @@
"message": "New login",
"description": "Button text to display within inline menu when there are no matching items on a login field"
},
"addNewLoginItem": {
"message": "Add new vault login item",
"addNewLoginItemAria": {
"message": "Add new vault login item, opens in a new window",
"description": "Screen reader text (aria-label) for new login button within inline menu"
},
"newCard": {
"message": "New card",
"description": "Button text to display within inline menu when there are no matching items on a credit card field"
},
"addNewCardItem": {
"message": "Add new vault card item",
"addNewCardItemAria": {
"message": "Add new vault card item, opens in a new window",
"description": "Screen reader text (aria-label) for new card button within inline menu"
},
"newIdentity": {
"message": "New identity",
"description": "Button text to display within inline menu when there are no matching items on an identity field"
},
"addNewIdentityItemAria": {
"message": "Add new vault identity item, opens in a new window",
"description": "Screen reader text (aria-label) for new identity button within inline menu"
},
"bitwardenOverlayMenuAvailable": {
"message": "Bitwarden auto-fill menu available. Press the down arrow key to select.",
"description": "Screen reader text for announcing when the overlay opens on the page"

View File

@ -37,6 +37,8 @@ export type FocusedFieldData = {
filledByCipherType?: CipherType;
tabId?: number;
frameId?: number;
accountCreationFieldType?: string;
showInlineMenuAccountCreation?: boolean;
};
export type InlineMenuElementPosition = {
@ -67,10 +69,30 @@ export type NewCardCipherData = {
cvv: string;
};
export type NewIdentityCipherData = {
title: string;
firstName: string;
middleName: string;
lastName: string;
fullName: string;
address1: string;
address2: string;
address3: string;
city: string;
state: string;
postalCode: string;
country: string;
company: string;
phone: string;
email: string;
username: string;
};
export type OverlayAddNewItemMessage = {
addNewCipherType?: CipherType;
login?: NewLoginCipherData;
card?: NewCardCipherData;
identity?: NewIdentityCipherData;
};
export type CloseInlineMenuMessage = {
@ -115,8 +137,13 @@ export type InlineMenuCipherData = {
reprompt: CipherRepromptType;
favorite: boolean;
icon: WebsiteIconData;
accountCreationFieldType?: string;
login?: { username: string };
card?: string;
identity?: {
fullName: string;
username?: string;
};
};
export type BackgroundMessageParam = {
@ -180,7 +207,7 @@ export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
export type InlineMenuButtonPortMessageHandlers = {
[key: string]: CallableFunction;
triggerDelayedAutofillInlineMenuClosure: ({ port }: PortConnectionParam) => void;
triggerDelayedAutofillInlineMenuClosure: () => void;
autofillInlineMenuButtonClicked: ({ port }: PortConnectionParam) => void;
autofillInlineMenuBlurred: () => void;
redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;

View File

@ -43,11 +43,11 @@ import {
} from "../enums/autofill-overlay.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
import {
createChromeTabMock,
createAutofillPageDetailsMock,
createPortSpyMock,
createChromeTabMock,
createFocusedFieldDataMock,
createPageDetailMock,
createPortSpyMock,
} from "../spec/autofill-mocks";
import {
flushPromises,
@ -713,9 +713,22 @@ describe("OverlayBackground", () => {
type: CipherType.Login,
login: { username: "username-3", uri: url },
});
const cipher4 = mock<CipherView>({
id: "id-4",
localData: { lastUsedDate: 222 },
name: "name-4",
type: CipherType.Identity,
identity: {
username: "username",
firstName: "Test",
lastName: "User",
email: "email@example.com",
},
});
beforeEach(() => {
beforeEach(async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
await initOverlayElementPorts();
});
it("skips updating the overlay ciphers if the user's auth status is not unlocked", async () => {
@ -767,7 +780,10 @@ describe("OverlayBackground", () => {
await overlayBackground.updateOverlayCiphers();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [CipherType.Card]);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [
CipherType.Card,
CipherType.Identity,
]);
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
@ -804,7 +820,10 @@ describe("OverlayBackground", () => {
await overlayBackground.updateOverlayCiphers(false);
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [CipherType.Card]);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [
CipherType.Card,
CipherType.Identity,
]);
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
@ -815,7 +834,6 @@ describe("OverlayBackground", () => {
});
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
overlayBackground["inlineMenuListPort"] = mock<chrome.runtime.Port>();
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
@ -823,11 +841,12 @@ describe("OverlayBackground", () => {
await overlayBackground.updateOverlayCiphers();
expect(overlayBackground["inlineMenuListPort"].postMessage).toHaveBeenCalledWith({
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: false,
ciphers: [
{
card: null,
accountCreationFieldType: undefined,
favorite: cipher1.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
@ -846,6 +865,205 @@ describe("OverlayBackground", () => {
],
});
});
it("updates the inline menu list with card ciphers", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
filledByCipherType: CipherType.Card,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: false,
ciphers: [
{
accountCreationFieldType: undefined,
favorite: cipher2.favorite,
icon: {
fallbackImage: "",
icon: "bwi-credit-card",
image: undefined,
imageEnabled: true,
},
id: "inline-menu-cipher-0",
card: cipher2.card.subTitle,
name: cipher2.name,
reprompt: cipher2.reprompt,
type: CipherType.Card,
},
],
});
});
describe("updating ciphers for an account creation inline menu", () => {
it("updates the ciphers with a list of identity ciphers that contain a username", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
accountCreationFieldType: "text",
showInlineMenuAccountCreation: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher4, cipher2]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
ciphers: [
{
accountCreationFieldType: "text",
favorite: cipher4.favorite,
icon: {
fallbackImage: "",
icon: "bwi-id-card",
image: undefined,
imageEnabled: true,
},
id: "inline-menu-cipher-1",
name: cipher4.name,
reprompt: cipher4.reprompt,
type: CipherType.Identity,
identity: {
fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`,
username: cipher4.identity.username,
},
},
],
});
});
it("appends any found login ciphers to the list of identity ciphers", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
accountCreationFieldType: "text",
showInlineMenuAccountCreation: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher4]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
ciphers: [
{
accountCreationFieldType: "text",
favorite: cipher4.favorite,
icon: {
fallbackImage: "",
icon: "bwi-id-card",
image: undefined,
imageEnabled: true,
},
id: "inline-menu-cipher-0",
name: cipher4.name,
reprompt: cipher4.reprompt,
type: CipherType.Identity,
identity: {
fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`,
username: cipher4.identity.username,
},
},
{
accountCreationFieldType: "text",
favorite: cipher1.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
id: "inline-menu-cipher-1",
login: {
username: cipher1.login.username,
},
name: cipher1.name,
reprompt: cipher1.reprompt,
type: CipherType.Login,
},
],
});
});
it("skips any identity ciphers that do not contain a username or an email address", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
accountCreationFieldType: "email",
showInlineMenuAccountCreation: true,
});
const identityCipherWithoutUsername = mock<CipherView>({
id: "id-5",
localData: { lastUsedDate: 222 },
name: "name-5",
type: CipherType.Identity,
identity: {
username: "",
email: "",
},
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([
cipher4,
identityCipherWithoutUsername,
]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
ciphers: [
{
accountCreationFieldType: "email",
favorite: cipher4.favorite,
icon: {
fallbackImage: "",
icon: "bwi-id-card",
image: undefined,
imageEnabled: true,
},
id: "inline-menu-cipher-1",
name: cipher4.name,
reprompt: cipher4.reprompt,
type: CipherType.Identity,
identity: {
fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`,
username: cipher4.identity.email,
},
},
],
});
});
it("does not add the identity ciphers if the field is for a password field", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
accountCreationFieldType: "password",
showInlineMenuAccountCreation: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher4]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
ciphers: [],
});
});
});
});
describe("extension message handlers", () => {
@ -954,6 +1172,95 @@ describe("OverlayBackground", () => {
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
});
describe("creating a new identity cipher", () => {
it("populates an identity cipher view and creates it", async () => {
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
addNewCipherType: CipherType.Identity,
identity: {
title: "title",
firstName: "firstName",
middleName: "middleName",
lastName: "lastName",
fullName: "fullName",
address1: "address1",
address2: "address2",
address3: "address3",
city: "city",
state: "state",
postalCode: "postalCode",
country: "country",
company: "company",
phone: "phone",
email: "email",
username: "username",
},
},
sender,
);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
});
it("saves the first name based on the full name value", async () => {
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
addNewCipherType: CipherType.Identity,
identity: {
firstName: "",
lastName: "",
fullName: "fullName",
},
},
sender,
);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
});
it("saves the first and middle names based on the full name value", async () => {
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
addNewCipherType: CipherType.Identity,
identity: {
firstName: "",
lastName: "",
fullName: "firstName middleName",
},
},
sender,
);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
});
it("saves the first, middle, and last names based on the full name value", async () => {
sendMockExtensionMessage(
{
command: "autofillOverlayAddNewVaultItem",
addNewCipherType: CipherType.Identity,
identity: {
firstName: "",
lastName: "",
fullName: "firstName middleName lastName",
},
},
sender,
);
await flushPromises();
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
});
});
});
describe("checkIsInlineMenuCiphersPopulated message handler", () => {
@ -1030,6 +1337,29 @@ describe("OverlayBackground", () => {
{ frameId: firstSender.frameId },
);
});
it("triggers an update of the identity ciphers present on a login field", async () => {
await initOverlayElementPorts();
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
const tab = createChromeTabMock({ id: 2 });
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock();
overlayBackground["isInlineMenuButtonVisible"] = true;
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
const focusedFieldData = createFocusedFieldDataMock({
tabId: tab.id,
frameId: sender.frameId,
showInlineMenuAccountCreation: true,
});
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
await flushPromises();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
ciphers: [],
showInlineMenuAccountCreation: true,
});
});
});
describe("checkIsFieldCurrentlyFocused message handler", () => {

View File

@ -21,6 +21,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
@ -40,23 +41,24 @@ import { generateRandomChars } from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import {
CloseInlineMenuMessage,
FocusedFieldData,
InlineMenuButtonPortMessageHandlers,
InlineMenuCipherData,
InlineMenuListPortMessageHandlers,
InlineMenuPosition,
NewCardCipherData,
NewIdentityCipherData,
NewLoginCipherData,
OverlayAddNewItemMessage,
OverlayBackground as OverlayBackgroundInterface,
OverlayBackgroundExtensionMessage,
OverlayBackgroundExtensionMessageHandlers,
InlineMenuButtonPortMessageHandlers,
InlineMenuCipherData,
InlineMenuListPortMessageHandlers,
OverlayPortMessage,
PageDetailsForTab,
SubFrameOffsetData,
SubFrameOffsetsForTab,
CloseInlineMenuMessage,
InlineMenuPosition,
ToggleInlineMenuHiddenMessage,
NewLoginCipherData,
NewCardCipherData,
} from "./abstractions/overlay.background";
export class OverlayBackground implements OverlayBackgroundInterface {
@ -125,7 +127,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
deletedCipher: () => this.updateOverlayCiphers(),
};
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
triggerDelayedAutofillInlineMenuClosure: ({ port }) => this.triggerDelayedInlineMenuClosure(),
triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(),
autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port),
autofillInlineMenuBlurred: () => this.checkInlineMenuListFocused(),
redirectAutofillInlineMenuFocusOut: ({ message, port }) =>
@ -249,6 +251,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.inlineMenuListPort?.postMessage({
command: "updateAutofillInlineMenuListCiphers",
ciphers,
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
});
}
@ -285,15 +288,25 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.cardAndIdentityCiphers.clear();
const cipherViews = (
await this.cipherService.getAllDecryptedForUrl(currentTab.url, [CipherType.Card])
await this.cipherService.getAllDecryptedForUrl(currentTab.url, [
CipherType.Card,
CipherType.Identity,
])
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
for (let cipherIndex = 0; cipherIndex < cipherViews.length; cipherIndex++) {
const cipherView = cipherViews[cipherIndex];
if (cipherView.type === CipherType.Card && !this.cardAndIdentityCiphers.has(cipherView)) {
if (
!this.cardAndIdentityCiphers.has(cipherView) &&
[CipherType.Card, CipherType.Identity].includes(cipherView.type)
) {
this.cardAndIdentityCiphers.add(cipherView);
}
}
if (!this.cardAndIdentityCiphers.size) {
this.cardAndIdentityCiphers = null;
}
return cipherViews;
}
@ -304,6 +317,75 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private async getInlineMenuCipherData(): Promise<InlineMenuCipherData[]> {
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers);
let inlineMenuCipherData: InlineMenuCipherData[] = [];
if (this.showInlineMenuAccountCreation()) {
inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers(
inlineMenuCiphersArray,
true,
);
} else {
inlineMenuCipherData = this.buildInlineMenuCiphers(inlineMenuCiphersArray, showFavicons);
}
this.currentInlineMenuCiphersCount = inlineMenuCipherData.length;
return inlineMenuCipherData;
}
/**
* Builds the inline menu ciphers for a form field that is meant for account creation.
*
* @param inlineMenuCiphersArray - Array of inline menu ciphers
* @param showFavicons - Identifies whether favicons should be shown
*/
private buildInlineMenuAccountCreationCiphers(
inlineMenuCiphersArray: [string, CipherView][],
showFavicons: boolean,
) {
const inlineMenuCipherData: InlineMenuCipherData[] = [];
const accountCreationLoginCiphers: InlineMenuCipherData[] = [];
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex];
if (cipher.type === CipherType.Login) {
accountCreationLoginCiphers.push(
this.buildCipherData(inlineMenuCipherId, cipher, showFavicons, true),
);
continue;
}
if (cipher.type !== CipherType.Identity || !this.focusedFieldData?.accountCreationFieldType) {
continue;
}
const identity = this.getIdentityCipherData(cipher, true);
if (!identity?.username) {
continue;
}
inlineMenuCipherData.push(
this.buildCipherData(inlineMenuCipherId, cipher, showFavicons, true, identity),
);
}
if (accountCreationLoginCiphers.length) {
return inlineMenuCipherData.concat(accountCreationLoginCiphers);
}
return inlineMenuCipherData;
}
/**
* Builds the inline menu ciphers for a form field that is not meant for account creation.
*
* @param inlineMenuCiphersArray - Array of inline menu ciphers
* @param showFavicons - Identifies whether favicons should be shown
*/
private buildInlineMenuCiphers(
inlineMenuCiphersArray: [string, CipherView][],
showFavicons: boolean,
) {
const inlineMenuCipherData: InlineMenuCipherData[] = [];
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
@ -312,22 +394,111 @@ export class OverlayBackground implements OverlayBackgroundInterface {
continue;
}
inlineMenuCipherData.push({
id: inlineMenuCipherId,
name: cipher.name,
type: cipher.type,
reprompt: cipher.reprompt,
favorite: cipher.favorite,
icon: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons),
login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null,
card: cipher.type === CipherType.Card ? cipher.card.subTitle : null,
});
inlineMenuCipherData.push(this.buildCipherData(inlineMenuCipherId, cipher, showFavicons));
}
this.currentInlineMenuCiphersCount = inlineMenuCipherData.length;
return inlineMenuCipherData;
}
/**
* Builds the cipher data for the inline menu list.
*
* @param inlineMenuCipherId - The ID of the inline menu cipher
* @param cipher - The cipher to build data for
* @param showFavicons - Identifies whether favicons should be shown
* @param showInlineMenuAccountCreation - Identifies whether the inline menu is for account creation
* @param identityData - Pre-created identity data
*/
private buildCipherData(
inlineMenuCipherId: string,
cipher: CipherView,
showFavicons: boolean,
showInlineMenuAccountCreation: boolean = false,
identityData?: { fullName: string; username?: string },
): InlineMenuCipherData {
const inlineMenuData: InlineMenuCipherData = {
id: inlineMenuCipherId,
name: cipher.name,
type: cipher.type,
reprompt: cipher.reprompt,
favorite: cipher.favorite,
icon: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons),
accountCreationFieldType: this.focusedFieldData?.accountCreationFieldType,
};
if (cipher.type === CipherType.Login) {
inlineMenuData.login = { username: cipher.login.username };
return inlineMenuData;
}
if (cipher.type === CipherType.Card) {
inlineMenuData.card = cipher.card.subTitle;
return inlineMenuData;
}
inlineMenuData.identity =
identityData || this.getIdentityCipherData(cipher, showInlineMenuAccountCreation);
return inlineMenuData;
}
/**
* Gets the identity data for a cipher based on whether the inline menu is for account creation.
*
* @param cipher - The cipher to get the identity data for
* @param showInlineMenuAccountCreation - Identifies whether the inline menu is for account creation
*/
private getIdentityCipherData(
cipher: CipherView,
showInlineMenuAccountCreation: boolean = false,
): { fullName: string; username?: string } {
const { firstName, lastName } = cipher.identity;
let fullName = "";
if (firstName) {
fullName += firstName;
}
if (lastName) {
fullName += ` ${lastName}`;
fullName = fullName.trim();
}
if (
!showInlineMenuAccountCreation ||
!this.focusedFieldData?.accountCreationFieldType ||
this.focusedFieldData.accountCreationFieldType === "password"
) {
return { fullName };
}
return {
fullName,
username:
this.focusedFieldData.accountCreationFieldType === "email"
? cipher.identity.email
: cipher.identity.username,
};
}
/**
* Identifies whether the inline menu is being shown on an account creation field.
*/
private showInlineMenuAccountCreation(): boolean {
if (typeof this.focusedFieldData?.showInlineMenuAccountCreation !== "undefined") {
return this.focusedFieldData?.showInlineMenuAccountCreation;
}
if (this.focusedFieldData?.filledByCipherType !== CipherType.Login) {
return false;
}
if (this.cardAndIdentityCiphers) {
return this.inlineMenuCiphers.size === this.cardAndIdentityCiphers.size;
}
return this.inlineMenuCiphers.size === 0;
}
/**
* Gets the currently focused field and closes the inline menu on that tab.
*/
@ -926,7 +1097,37 @@ export class OverlayBackground implements OverlayBackgroundInterface {
);
}
const previousFocusedFieldData = this.focusedFieldData;
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId };
const accountCreationFieldBlurred =
previousFocusedFieldData?.showInlineMenuAccountCreation &&
!this.focusedFieldData.showInlineMenuAccountCreation;
if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) {
void this.updateIdentityCiphersOnLoginField(previousFocusedFieldData);
}
}
/**
* Triggers an update of populated identity ciphers when a login field is focused.
*
* @param previousFocusedFieldData - The data set of the previously focused field
*/
private async updateIdentityCiphersOnLoginField(previousFocusedFieldData: FocusedFieldData) {
if (
!previousFocusedFieldData ||
!this.isInlineMenuButtonVisible ||
(await this.getAuthStatus()) !== AuthenticationStatus.Unlocked
) {
return;
}
this.inlineMenuListPort?.postMessage({
command: "updateAutofillInlineMenuListCiphers",
ciphers: await this.getInlineMenuCipherData(),
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
});
}
/**
@ -1116,6 +1317,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
listPageTitle: this.i18nService.translate("bitwardenVault"),
unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewAutofillSuggestions"),
unlockAccount: this.i18nService.translate("unlockAccount"),
unlockAccountAria: this.i18nService.translate("unlockAccountAria"),
fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"),
username: this.i18nService.translate("username")?.toLowerCase(),
view: this.i18nService.translate("view"),
@ -1123,9 +1325,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
newItem: this.i18nService.translate("newItem"),
addNewVaultItem: this.i18nService.translate("addNewVaultItem"),
newLogin: this.i18nService.translate("newLogin"),
addNewLoginItem: this.i18nService.translate("addNewLoginItem"),
addNewLoginItem: this.i18nService.translate("addNewLoginItemAria"),
newCard: this.i18nService.translate("newCard"),
addNewCardItem: this.i18nService.translate("addNewCardItem"),
addNewCardItem: this.i18nService.translate("addNewCardItemAria"),
newIdentity: this.i18nService.translate("newIdentity"),
addNewIdentityItem: this.i18nService.translate("addNewIdentityItemAria"),
cardNumberEndsWith: this.i18nService.translate("cardNumberEndsWith"),
};
}
@ -1184,10 +1388,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param addNewCipherType - The type of cipher to add
* @param login - The login data captured from the extension message
* @param card - The card data captured from the extension message
* @param identity - The identity data captured from the extension message
* @param sender - The sender of the extension message
*/
private async addNewVaultItem(
{ addNewCipherType, login, card }: OverlayAddNewItemMessage,
{ addNewCipherType, login, card, identity }: OverlayAddNewItemMessage,
sender: chrome.runtime.MessageSender,
) {
if (!addNewCipherType) {
@ -1198,6 +1403,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
addNewCipherType,
login,
card,
identity,
});
if (cipherView) {
@ -1218,8 +1424,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param addNewCipherType - The type of cipher to add
* @param login - The login data captured from the extension message
* @param card - The card data captured from the extension message
* @param identity - The identity data captured from the extension message
*/
private buildNewVaultItemCipherView({ addNewCipherType, login, card }: OverlayAddNewItemMessage) {
private buildNewVaultItemCipherView({
addNewCipherType,
login,
card,
identity,
}: OverlayAddNewItemMessage) {
if (login && addNewCipherType === CipherType.Login) {
return this.buildLoginCipherView(login);
}
@ -1227,6 +1439,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (card && addNewCipherType === CipherType.Card) {
return this.buildCardCipherView(card);
}
if (identity && addNewCipherType === CipherType.Identity) {
return this.buildIdentityCipherView(identity);
}
}
/**
@ -1275,6 +1491,68 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return cipherView;
}
/**
* Builds a new identity cipher view with the provided identity data.
*
* @param identity - The identity data captured from the extension message
*/
private buildIdentityCipherView(identity: NewIdentityCipherData) {
const identityView = new IdentityView();
identityView.title = identity.title || "";
identityView.firstName = identity.firstName || "";
identityView.middleName = identity.middleName || "";
identityView.lastName = identity.lastName || "";
identityView.address1 = identity.address1 || "";
identityView.address2 = identity.address2 || "";
identityView.address3 = identity.address3 || "";
identityView.city = identity.city || "";
identityView.state = identity.state || "";
identityView.postalCode = identity.postalCode || "";
identityView.country = identity.country || "";
identityView.company = identity.company || "";
identityView.phone = identity.phone || "";
identityView.email = identity.email || "";
identityView.username = identity.username || "";
if (identity.fullName && !identityView.firstName && !identityView.lastName) {
this.buildIdentityNameParts(identity, identityView);
}
const cipherView = new CipherView();
cipherView.name = "";
cipherView.folderId = null;
cipherView.type = CipherType.Identity;
cipherView.identity = identityView;
return cipherView;
}
/**
* Splits the identity full name into first, middle, and last name parts.
*
* @param identity - The identity data captured from the extension message
* @param identityView - The identity view to update
*/
private buildIdentityNameParts(identity: NewIdentityCipherData, identityView: IdentityView) {
const fullNameParts = identity.fullName.split(" ");
if (fullNameParts.length === 1) {
identityView.firstName = fullNameParts[0] || "";
return;
}
if (fullNameParts.length === 2) {
identityView.firstName = fullNameParts[0] || "";
identityView.lastName = fullNameParts[1] || "";
return;
}
identityView.firstName = fullNameParts[0] || "";
identityView.middleName = fullNameParts[1] || "";
identityView.lastName = fullNameParts[2] || "";
}
/**
* Updates the property that identifies if a form field set up for the inline menu is currently focused.
*
@ -1523,7 +1801,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
Promise.resolve(messageResponse)
.then((response) => sendResponse(response))
.catch(this.logService.error);
.catch((error) => this.logService.error(error));
return true;
};
@ -1598,6 +1876,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
? AutofillOverlayPort.ListMessageConnector
: AutofillOverlayPort.ButtonMessageConnector,
filledByCipherType: this.focusedFieldData?.filledByCipherType,
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
});
void this.updateInlineMenuPosition(
{

View File

@ -61,13 +61,10 @@ describe("AutofillInit", () => {
autofillInit.init();
jest.advanceTimersByTime(250);
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
{
command: "bgCollectPageDetails",
sender: "autofillInit",
},
expect.any(Function),
);
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
command: "bgCollectPageDetails",
sender: "autofillInit",
});
});
it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => {

View File

@ -506,16 +506,17 @@ exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty l
aria-hidden="true"
fill="none"
height="17"
viewBox="0 0 16 17"
width="16"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
clip-rule="evenodd"
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
fill="#175DDC"
fill-rule="evenodd"
/>
</g>
<defs>
@ -523,7 +524,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty l
id="a"
>
<path
d="M0 .49h16v16H0z"
d="M.421.421h16v16h-16z"
fill="#fff"
/>
</clippath>

View File

@ -0,0 +1,29 @@
export const AutofillFieldQualifier = {
password: "password",
username: "username",
cardholderName: "cardholderName",
cardNumber: "cardNumber",
cardExpirationMonth: "cardExpirationMonth",
cardExpirationYear: "cardExpirationYear",
cardExpirationDate: "cardExpirationDate",
cardCvv: "cardCvv",
identityTitle: "identityTitle",
identityFirstName: "identityFirstName",
identityMiddleName: "identityMiddleName",
identityLastName: "identityLastName",
identityFullName: "identityFullName",
identityAddress1: "identityAddress1",
identityAddress2: "identityAddress2",
identityAddress3: "identityAddress3",
identityCity: "identityCity",
identityState: "identityState",
identityPostalCode: "identityPostalCode",
identityCountry: "identityCountry",
identityCompany: "identityCompany",
identityPhone: "identityPhone",
identityEmail: "identityEmail",
identityUsername: "identityUsername",
} as const;
export type AutofillFieldQualifierType =
(typeof AutofillFieldQualifier)[keyof typeof AutofillFieldQualifier];

View File

@ -1,5 +1,7 @@
import { CipherType } from "@bitwarden/common/vault/enums";
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
/**
* Represents a single field that is collected from the page source and is potentially autofilled.
*/
@ -110,4 +112,8 @@ export default class AutofillField {
checked?: boolean;
filledByCipherType?: CipherType;
showInlineMenuAccountCreation?: boolean;
fieldQualifier?: AutofillFieldQualifierType;
}

View File

@ -7,6 +7,7 @@ type AutofillInlineMenuListMessage = { command: string };
export type UpdateAutofillInlineMenuListCiphersMessage = AutofillInlineMenuListMessage & {
ciphers: InlineMenuCipherData[];
showInlineMenuAccountCreation?: boolean;
};
export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage & {
@ -16,6 +17,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
translations: Record<string, string>;
ciphers?: InlineMenuCipherData[];
filledByCipherType?: CipherType;
showInlineMenuAccountCreation?: boolean;
portKey: string;
};

View File

@ -373,7 +373,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* ensure that the inline menu elements are always present at the bottom of the
* body element.
*/
private handleBodyElementMutationObserverUpdate = async () => {
private handleBodyElementMutationObserverUpdate = () => {
if (
(!this.buttonElement && !this.listElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
@ -410,17 +410,18 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
return;
}
const isInlineMenuListVisible = await this.isInlineMenuListVisible();
if (
!lastChild ||
(lastChildIsInlineMenuList && secondToLastChildIsInlineMenuButton) ||
(lastChildIsInlineMenuButton && !(await this.isInlineMenuListVisible()))
(lastChildIsInlineMenuButton && !isInlineMenuListVisible)
) {
return;
}
if (
(lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) ||
(lastChildIsInlineMenuButton && (await this.isInlineMenuListVisible()))
(lastChildIsInlineMenuButton && isInlineMenuListVisible)
) {
globalThis.document.body.insertBefore(this.buttonElement, this.listElement);
return;

View File

@ -80,10 +80,11 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
this.defaultIframeAttributes.title = this.iframeTitle;
this.iframe = globalThis.document.createElement("iframe");
this.updateElementStyles(this.iframe, { ...this.iframeStyles, ...this.initStyles });
for (const [attribute, value] of Object.entries(this.defaultIframeAttributes)) {
this.iframe.setAttribute(attribute, value);
}
this.iframeStyles = { ...this.iframeStyles, ...this.initStyles };
this.setElementStyles(this.iframe, this.iframeStyles, true);
this.iframe.addEventListener(EVENTS.LOAD, this.setupPortMessageListener);
if (this.ariaAlert) {
@ -91,6 +92,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
}
this.shadow.appendChild(this.iframe);
this.observeIframe();
}
/**
@ -143,7 +145,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
clearTimeout(this.ariaAlertTimeout);
}
this.ariaAlertTimeout = setTimeout(() => this.shadow.appendChild(this.ariaAlertElement), 2000);
this.ariaAlertTimeout = globalThis.setTimeout(
() => this.shadow.appendChild(this.ariaAlertElement),
2000,
);
}
/**
@ -255,7 +260,9 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
return;
}
this.clearFadeInTimeout();
if (this.fadeInTimeout) {
this.handleFadeInInlineMenuIframe();
}
this.updateElementStyles(this.iframe, position);
this.announceAriaAlert();
}
@ -325,6 +332,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
private clearFadeInTimeout() {
if (this.fadeInTimeout) {
clearTimeout(this.fadeInTimeout);
this.fadeInTimeout = null;
}
}
@ -442,7 +450,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
}
this.mutationObserverIterations++;
this.mutationObserverIterationsResetTimeout = setTimeout(() => resetCounters(), 2000);
this.mutationObserverIterationsResetTimeout = globalThis.setTimeout(
() => resetCounters(),
2000,
);
if (this.mutationObserverIterations > 20) {
clearTimeout(this.mutationObserverIterationsResetTimeout);

View File

@ -13,8 +13,8 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
class="inline-menu-list-button-container"
>
<button
aria-label=", opensInANewWindow"
class="add-new-item-button inline-menu-list-button"
aria-label=""
class="add-new-item-button inline-menu-list-button inline-menu-list-action"
id="new-item-button"
tabindex="-1"
>
@ -22,16 +22,17 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
aria-hidden="true"
fill="none"
height="17"
viewBox="0 0 16 17"
width="16"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
clip-rule="evenodd"
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
fill="#175DDC"
fill-rule="evenodd"
/>
</g>
<defs>
@ -39,7 +40,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
id="a"
>
<path
d="M0 .49h16v16H0z"
d="M.421.421h16v16h-16z"
fill="#fff"
/>
</clippath>
@ -63,8 +64,8 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
class="inline-menu-list-button-container"
>
<button
aria-label=", opensInANewWindow"
class="add-new-item-button inline-menu-list-button"
aria-label=""
class="add-new-item-button inline-menu-list-button inline-menu-list-action"
id="new-item-button"
tabindex="-1"
>
@ -72,16 +73,17 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
aria-hidden="true"
fill="none"
height="17"
viewBox="0 0 16 17"
width="16"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
clip-rule="evenodd"
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
fill="#175DDC"
fill-rule="evenodd"
/>
</g>
<defs>
@ -89,7 +91,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
id="a"
>
<path
d="M0 .49h16v16H0z"
d="M.421.421h16v16h-16z"
fill="#fff"
/>
</clippath>
@ -113,8 +115,8 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
class="inline-menu-list-button-container"
>
<button
aria-label=", opensInANewWindow"
class="add-new-item-button inline-menu-list-button"
aria-label=""
class="add-new-item-button inline-menu-list-button inline-menu-list-action"
id="new-item-button"
tabindex="-1"
>
@ -122,16 +124,17 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
aria-hidden="true"
fill="none"
height="17"
viewBox="0 0 16 17"
width="16"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
clip-rule="evenodd"
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
fill="#175DDC"
fill-rule="evenodd"
/>
</g>
<defs>
@ -139,7 +142,177 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
id="a"
>
<path
d="M0 .49h16v16H0z"
d="M.421.421h16v16h-16z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that should be filled by an identity cipher 1`] = `
<div
class="inline-menu-list-container theme_light"
>
<div
class="no-items inline-menu-list-message"
>
noItemsToShow
</div>
<div
class="inline-menu-list-button-container"
>
<button
aria-label=""
class="add-new-item-button inline-menu-list-button inline-menu-list-action"
id="new-item-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="17"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
clip-rule="evenodd"
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
fill="#175DDC"
fill-rule="evenodd"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M.421.421h16v16h-16z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user account creation elements creates the inline menu account creation view 1`] = `
<div
class="inline-menu-list-container theme_light inline-menu-list-container--with-new-item-button"
>
<ul
class="inline-menu-list-actions"
role="list"
>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon bwi bwi-id-card"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 1"
>
website login 1
</span>
<span
class="cipher-subtitle"
title="username"
>
username
</span>
</span>
</button>
<button
aria-label="view website login 1, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
</ul>
<div
class="inline-menu-list-button-container"
>
<button
aria-label=""
class="add-new-item-button inline-menu-list-button inline-menu-list-action"
id="new-item-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="17"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
clip-rule="evenodd"
d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z"
fill="#175DDC"
fill-rule="evenodd"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M.421.421h16v16h-16z"
fill="#fff"
/>
</clippath>
@ -166,9 +339,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username1"
aria-description="username: username1"
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -236,9 +409,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username2"
aria-description="username: username2"
aria-label="fillCredentialsFor website login 2"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -305,9 +478,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, "
aria-description="username: "
aria-label="fillCredentialsFor "
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -361,9 +534,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username4"
aria-description="username: username4"
aria-label="fillCredentialsFor website login 4"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -446,9 +619,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username5"
aria-description="username: username5"
aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -516,9 +689,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username6"
aria-description="username: username6"
aria-label="fillCredentialsFor website login 6"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -598,9 +771,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username1"
aria-description="username: username1"
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -668,9 +841,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username2"
aria-description="username: username2"
aria-label="fillCredentialsFor website login 2"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -737,9 +910,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, "
aria-description="username: "
aria-label="fillCredentialsFor "
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -793,9 +966,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username4"
aria-description="username: username4"
aria-label="fillCredentialsFor website login 4"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -878,9 +1051,9 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username5"
aria-description="username: username5"
aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -948,9 +1121,441 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username, username6"
aria-description="username: username6"
aria-label="fillCredentialsFor website login 6"
class="fill-cipher-button"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 6"
>
website login 6
</span>
<span
class="cipher-subtitle"
title="username6"
>
username6
</span>
</span>
</button>
<button
aria-label="view website login 6, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
</ul>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the views for a list of identity ciphers 1`] = `
<div
class="inline-menu-list-container theme_light"
>
<ul
class="inline-menu-list-actions"
role="list"
>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username1"
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 1"
>
website login 1
</span>
<span
class="cipher-subtitle"
title="username1"
>
username1
</span>
</span>
</button>
<button
aria-label="view website login 1, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username2"
aria-label="fillCredentialsFor website login 2"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon bwi bw-icon"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 2"
>
website login 2
</span>
<span
class="cipher-subtitle"
title="username2"
>
username2
</span>
</span>
</button>
<button
aria-label="view website login 2, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: "
aria-label="fillCredentialsFor "
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon bwi bw-icon"
/>
<span
class="cipher-details"
/>
</button>
<button
aria-label="view , opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username4"
aria-label="fillCredentialsFor website login 4"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
>
<svg
aria-hidden="true"
fill="none"
height="25"
viewBox="0 0 24 25"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M18.026 17.842c-1.418 1.739-3.517 2.84-5.86 2.84a7.364 7.364 0 0 1-3.431-.848l.062-.15.062-.151.063-.157c.081-.203.17-.426.275-.646.133-.28.275-.522.426-.68.026-.028.101-.075.275-.115.165-.037.376-.059.629-.073.138-.008.288-.014.447-.02.399-.016.847-.034 1.266-.092.314-.044.566-.131.755-.271a.884.884 0 0 0 .352-.555c.037-.2.008-.392-.03-.543-.035-.135-.084-.264-.12-.355l-.01-.03a4.26 4.26 0 0 0-.145-.33c-.126-.264-.237-.497-.288-1.085-.03-.344.09-.73.251-1.138l.089-.22c.05-.123.1-.247.14-.355.064-.171.129-.375.129-.566a1.51 1.51 0 0 0-.134-.569 2.573 2.573 0 0 0-.319-.547c-.246-.323-.635-.669-1.093-.669-.44 0-1.006.169-1.487.368-.246.102-.48.216-.68.33-.192.111-.372.235-.492.359-.93.96-1.48 1.239-1.81 1.258-.277.017-.478-.15-.736-.525a9.738 9.738 0 0 1-.19-.29l-.006-.01a11.568 11.568 0 0 0-.198-.305 2.76 2.76 0 0 0-.521-.6 1.39 1.39 0 0 0-1.088-.314 8.302 8.302 0 0 1 1.987-3.936c.055.342.146.626.272.856.23.42.561.64.926.716.406.086.857-.061 1.26-.216.125-.047.248-.097.372-.147.309-.125.618-.25.947-.341.26-.072.581-.057.959.012.264.049.529.118.8.19l.36.091c.379.094.782.178 1.135.148.374-.032.733-.197.934-.623a.874.874 0 0 0 .024-.752c-.087-.197-.24-.355-.35-.47-.26-.267-.412-.427-.412-.685 0-.125.037-.2.09-.263a.982.982 0 0 1 .303-.211c.059-.03.119-.058.183-.089l.036-.016a3.79 3.79 0 0 0 .236-.118c.047-.026.098-.056.148-.093 1.936.747 3.51 2.287 4.368 4.249a7.739 7.739 0 0 0-.031-.004c-.38-.047-.738-.056-1.063.061-.34.123-.603.368-.817.74-.122.211-.284.43-.463.67l-.095.129c-.207.281-.431.595-.58.92-.15.326-.245.705-.142 1.103.104.397.387.738.837 1.036.099.065.225.112.314.145l.02.008c.108.04.195.074.268.117.07.042.106.08.124.114.017.03.037.087.022.206-.047.376-.069.73-.052 1.034.017.292.071.59.218.809.118.174.12.421.108.786v.01a2.46 2.46 0 0 0 .021.518.809.809 0 0 0 .15.35Zm1.357.059a9.654 9.654 0 0 0 1.62-5.386c0-5.155-3.957-9.334-8.836-9.334-4.88 0-8.836 4.179-8.836 9.334 0 3.495 1.82 6.543 4.513 8.142v.093h.161a8.426 8.426 0 0 0 4.162 1.098c2.953 0 5.568-1.53 7.172-3.882a.569.569 0 0 0 .048-.062l-.004-.003ZM8.152 19.495a43.345 43.345 0 0 1 .098-.238l.057-.142c.082-.205.182-.455.297-.698.143-.301.323-.624.552-.864.163-.172.392-.254.602-.302.219-.05.473-.073.732-.088.162-.01.328-.016.495-.023.386-.015.782-.03 1.168-.084.255-.036.392-.099.461-.15.06-.045.076-.084.083-.12a.534.534 0 0 0-.02-.223 2.552 2.552 0 0 0-.095-.278l-.01-.027a3.128 3.128 0 0 0-.104-.232c-.134-.282-.31-.65-.374-1.381-.046-.533.138-1.063.3-1.472.035-.09.069-.172.1-.249.046-.11.086-.21.123-.31.062-.169.083-.264.083-.312a.812.812 0 0 0-.076-.283 1.867 1.867 0 0 0-.23-.394c-.21-.274-.428-.408-.577-.408-.315 0-.788.13-1.246.32a5.292 5.292 0 0 0-.603.293 1.727 1.727 0 0 0-.347.244c-.936.968-1.641 1.421-2.235 1.457-.646.04-1.036-.413-1.31-.813-.07-.103-.139-.21-.203-.311l-.005-.007c-.064-.101-.125-.197-.188-.29a2.098 2.098 0 0 0-.387-.453.748.748 0 0 0-.436-.18c-.1-.006-.22.005-.365.046a8.707 8.707 0 0 0-.056.992c0 2.957 1.488 5.547 3.716 6.98Zm10.362-2.316.003-.192.002-.046c.01-.305.026-.786-.232-1.169-.036-.054-.082-.189-.096-.444-.014-.243.003-.55.047-.9a1.051 1.051 0 0 0-.105-.649.987.987 0 0 0-.374-.374 2.285 2.285 0 0 0-.367-.166h-.003a1.243 1.243 0 0 1-.205-.088c-.369-.244-.505-.46-.549-.629-.044-.168-.015-.364.099-.61.115-.25.297-.511.507-.796l.089-.12c.178-.239.368-.495.512-.745.152-.263.302-.382.466-.441.18-.065.416-.073.77-.03.142.018.275.04.397.063.274.837.423 1.736.423 2.671a8.45 8.45 0 0 1-1.384 4.665Zm-4.632-12.63a7.362 7.362 0 0 0-1.715-.201c-1.89 0-3.621.716-4.965 1.905.025.54.12.887.24 1.105.13.238.295.34.482.38.2.042.484-.027.905-.188l.328-.13c.32-.13.681-.275 1.048-.377.398-.111.833-.075 1.24 0 .289.053.59.132.871.205l.326.084c.383.094.694.151.932.13.216-.017.326-.092.395-.237.039-.083.027-.114.014-.144-.027-.062-.088-.136-.212-.264l-.043-.044c-.218-.222-.567-.578-.567-1.142 0-.305.101-.547.262-.734.137-.159.308-.267.46-.347Z"
fill="#777"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 4"
>
website login 4
</span>
<span
class="cipher-subtitle"
title="username4"
>
username4
</span>
</span>
</button>
<button
aria-label="view website login 4, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username5"
aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 5"
>
website login 5
</span>
<span
class="cipher-subtitle"
title="username5"
>
username5
</span>
</span>
</button>
<button
aria-label="view website login 5, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username6"
aria-label="fillCredentialsFor website login 6"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
@ -1028,8 +1633,8 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline men
class="inline-menu-list-button-container"
>
<button
aria-label="unlockAccount, opensInANewWindow"
class="unlock-button inline-menu-list-button"
aria-label=""
class="unlock-button inline-menu-list-button inline-menu-list-action"
id="unlock-button"
tabindex="-1"
>

View File

@ -90,6 +90,19 @@ describe("AutofillInlineMenuList", () => {
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
it("creates the views for the no results inline menu that should be filled by an identity cipher", () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
authStatus: AuthenticationStatus.Unlocked,
ciphers: [],
filledByCipherType: CipherType.Identity,
portKey,
}),
);
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
it("creates the views for the no results inline menu that does not have a fill by cipher type", () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
@ -155,12 +168,34 @@ describe("AutofillInlineMenuList", () => {
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
it("creates the views for a list of identity ciphers", () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
filledByCipherType: CipherType.Card,
ciphers: [
createAutofillOverlayCipherDataMock(1, {
type: CipherType.Identity,
identity: { fullName: "firstName lastName" },
login: null,
icon: {
imageEnabled: true,
icon: "bwi-id-card",
},
}),
],
}),
);
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
it("loads ciphers on scroll one page at a time", () => {
jest.useFakeTimers();
autofillInlineMenuList["ciphersList"].scrollTop = 10;
const originalListOfElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(".cipher-container");
window.dispatchEvent(new Event("scroll"));
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.runAllTimers();
const updatedListOfElements =
@ -172,17 +207,18 @@ describe("AutofillInlineMenuList", () => {
it("debounces the ciphers scroll handler", () => {
jest.useFakeTimers();
autofillInlineMenuList["ciphersList"].scrollTop = 10;
autofillInlineMenuList["cipherListScrollDebounceTimeout"] = setTimeout(jest.fn, 0);
const handleDebouncedScrollEventSpy = jest.spyOn(
autofillInlineMenuList as any,
"handleDebouncedScrollEvent",
);
window.dispatchEvent(new Event("scroll"));
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(100);
window.dispatchEvent(new Event("scroll"));
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(100);
window.dispatchEvent(new Event("scroll"));
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(400);
expect(handleDebouncedScrollEventSpy).toHaveBeenCalledTimes(1);
@ -219,7 +255,7 @@ describe("AutofillInlineMenuList", () => {
expect((secondFillCipherElement as HTMLElement).focus).toBeCalled();
});
it("directs focus to the first item in the cipher list if no cipher is present after the current one when pressing ArrowDown", () => {
it("directs focus to the first item in the cipher list if no cipher is present after the current one when pressing ArrowDown and no new item button exists", () => {
const fillCipherElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
@ -233,6 +269,26 @@ describe("AutofillInlineMenuList", () => {
expect((firstFillCipherElement as HTMLElement).focus).toBeCalled();
});
it("directs focus to the new item button if no cipher is present after the current one when pressing ArrowDown", async () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
portKey,
showInlineMenuAccountCreation: true,
}),
);
await flushPromises();
const fillCipherElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
);
const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1];
jest.spyOn(autofillInlineMenuList["newItemButtonElement"], "focus");
lastFillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
expect(autofillInlineMenuList["newItemButtonElement"].focus).toBeCalled();
});
it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => {
const fillCipherElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
@ -261,6 +317,26 @@ describe("AutofillInlineMenuList", () => {
expect((lastFillCipherElement as HTMLElement).focus).toBeCalled();
});
it("directs focus to the new item button if no cipher is present before the current one when pressing ArrowUp", async () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
portKey,
showInlineMenuAccountCreation: true,
}),
);
await flushPromises();
const fillCipherElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
);
const firstFillCipherElement = fillCipherElements[0];
jest.spyOn(autofillInlineMenuList["newItemButtonElement"], "focus");
firstFillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" }));
expect(autofillInlineMenuList["newItemButtonElement"].focus).toBeCalled();
});
it("allows the user to move keyboard focus to the view cipher button on ArrowRight", () => {
const cipherContainerElement =
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".cipher-container");
@ -349,6 +425,85 @@ describe("AutofillInlineMenuList", () => {
expect((viewCipherButton as HTMLElement).focus).not.toBeCalled();
});
});
describe("account creation elements", () => {
let newVaultItemButtonSpy: HTMLButtonElement;
beforeEach(async () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
filledByCipherType: CipherType.Login,
showInlineMenuAccountCreation: true,
portKey,
ciphers: [
createAutofillOverlayCipherDataMock(1, {
type: CipherType.Identity,
identity: { username: "username", fullName: "firstName lastName" },
login: null,
icon: {
imageEnabled: true,
icon: "bwi-id-card",
},
}),
],
}),
);
await flushPromises();
newVaultItemButtonSpy = autofillInlineMenuList["newItemButtonElement"];
});
it("creates the inline menu account creation view", async () => {
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
expect(newVaultItemButtonSpy).not.toBeUndefined();
});
it("allows for the creation of a new login cipher", () => {
newVaultItemButtonSpy.dispatchEvent(new Event("click"));
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
"*",
);
});
describe("keydown events on the add new vault item button", () => {
it("ignores keydown events that are not ArrowDown or ArrowUp", () => {
const fillCipherButton =
autofillInlineMenuList["inlineMenuListContainer"].querySelector(
".fill-cipher-button",
);
jest.spyOn(fillCipherButton as HTMLElement, "focus");
newVaultItemButtonSpy.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowRight" }));
expect((fillCipherButton as HTMLElement).focus).not.toHaveBeenCalled();
});
it("focuses the first element of the cipher list when ArrowDown is pressed on the newItem button", () => {
const fillCipherButton =
autofillInlineMenuList["inlineMenuListContainer"].querySelector(
".fill-cipher-button",
);
jest.spyOn(fillCipherButton as HTMLElement, "focus");
newVaultItemButtonSpy.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
expect((fillCipherButton as HTMLElement).focus).toHaveBeenCalled();
});
it("focuses the last element of the cipher list when ArrowUp is pressed on the newItem button", () => {
const fillCipherButton =
autofillInlineMenuList["inlineMenuListContainer"].querySelector(
".fill-cipher-button",
);
jest.spyOn(fillCipherButton as HTMLElement, "focus");
newVaultItemButtonSpy.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" }));
expect((fillCipherButton as HTMLElement).focus).toHaveBeenCalled();
});
});
});
});
});

View File

@ -23,12 +23,15 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
private currentCipherIndex = 0;
private filledByCipherType: CipherType;
private showInlineMenuAccountCreation: boolean;
private readonly showCiphersPerPage = 6;
private newItemButtonElement: HTMLButtonElement;
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
{
initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message),
checkAutofillInlineMenuListFocused: () => this.checkInlineMenuListFocused(),
updateAutofillInlineMenuListCiphers: ({ message }) => this.updateListItems(message.ciphers),
updateAutofillInlineMenuListCiphers: ({ message }) =>
this.updateListItems(message.ciphers, message.showInlineMenuAccountCreation),
focusAutofillInlineMenuList: () => this.focusInlineMenuList(),
};
@ -49,6 +52,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param ciphers - The ciphers to display in the inline menu list.
* @param portKey - Background generated key that allows the port to communicate with the background.
* @param filledByCipherType - The type of cipher that fills the current field.
* @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields.
*/
private async initAutofillInlineMenuList({
translations,
@ -58,6 +62,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
ciphers,
portKey,
filledByCipherType,
showInlineMenuAccountCreation,
}: InitAutofillInlineMenuListMessage) {
const linkElement = await this.initAutofillInlineMenuPage(
"list",
@ -78,7 +83,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.shadowDom.append(linkElement, this.inlineMenuListContainer);
if (authStatus === AuthenticationStatus.Unlocked) {
this.updateListItems(ciphers);
this.updateListItems(ciphers, showInlineMenuAccountCreation);
return;
}
@ -98,18 +103,17 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
const unlockButtonElement = globalThis.document.createElement("button");
unlockButtonElement.id = "unlock-button";
unlockButtonElement.tabIndex = -1;
unlockButtonElement.classList.add("unlock-button", "inline-menu-list-button");
unlockButtonElement.textContent = this.getTranslation("unlockAccount");
unlockButtonElement.setAttribute(
"aria-label",
`${this.getTranslation("unlockAccount")}, ${this.getTranslation("opensInANewWindow")}`,
unlockButtonElement.classList.add(
"unlock-button",
"inline-menu-list-button",
"inline-menu-list-action",
);
unlockButtonElement.textContent = this.getTranslation("unlockAccount");
unlockButtonElement.setAttribute("aria-label", this.getTranslation("unlockAccountAria"));
unlockButtonElement.prepend(buildSvgDomElement(lockIcon));
unlockButtonElement.addEventListener(EVENTS.CLICK, this.handleUnlockButtonClick);
const inlineMenuListButtonContainer = globalThis.document.createElement("div");
inlineMenuListButtonContainer.classList.add("inline-menu-list-button-container");
inlineMenuListButtonContainer.appendChild(unlockButtonElement);
const inlineMenuListButtonContainer = this.buildButtonContainer(unlockButtonElement);
this.inlineMenuListContainer.append(lockedInlineMenu, inlineMenuListButtonContainer);
}
@ -127,12 +131,20 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* If no ciphers are passed, the no results inline menu is built.
*
* @param ciphers - The ciphers to display in the inline menu list.
* @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields.
*/
private updateListItems(ciphers: InlineMenuCipherData[]) {
private updateListItems(
ciphers: InlineMenuCipherData[],
showInlineMenuAccountCreation?: boolean,
) {
this.ciphers = ciphers;
this.currentCipherIndex = 0;
this.showInlineMenuAccountCreation = showInlineMenuAccountCreation;
if (this.inlineMenuListContainer) {
this.inlineMenuListContainer.innerHTML = "";
this.inlineMenuListContainer.classList.remove(
"inline-menu-list-container--with-new-item-button",
);
}
if (!ciphers?.length) {
@ -143,11 +155,22 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.ciphersList = globalThis.document.createElement("ul");
this.ciphersList.classList.add("inline-menu-list-actions");
this.ciphersList.setAttribute("role", "list");
globalThis.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
this.ciphersList.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent, {
passive: true,
});
this.loadPageOfCiphers();
this.inlineMenuListContainer.appendChild(this.ciphersList);
if (!this.showInlineMenuAccountCreation) {
return;
}
const addNewLoginButtonContainer = this.buildNewItemButton();
this.inlineMenuListContainer.appendChild(addNewLoginButtonContainer);
this.inlineMenuListContainer.classList.add("inline-menu-list-container--with-new-item-button");
this.newItemButtonElement.addEventListener(EVENTS.KEYUP, this.handleNewItemButtonKeyUpEvent);
}
/**
@ -159,37 +182,47 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
noItemsMessage.classList.add("no-items", "inline-menu-list-message");
noItemsMessage.textContent = this.getTranslation("noItemsToShow");
const newItemButton = globalThis.document.createElement("button");
newItemButton.tabIndex = -1;
newItemButton.id = "new-item-button";
newItemButton.classList.add("add-new-item-button", "inline-menu-list-button");
newItemButton.textContent = this.getNewItemButtonText();
newItemButton.setAttribute(
"aria-label",
`${this.getNewItemAriaLabel()}, ${this.getTranslation("opensInANewWindow")}`,
const newItemButton = this.buildNewItemButton();
this.inlineMenuListContainer.append(noItemsMessage, newItemButton);
}
/**
* Builds a "New Item" button and returns the container of that button.
*/
private buildNewItemButton() {
this.newItemButtonElement = globalThis.document.createElement("button");
this.newItemButtonElement.tabIndex = -1;
this.newItemButtonElement.id = "new-item-button";
this.newItemButtonElement.classList.add(
"add-new-item-button",
"inline-menu-list-button",
"inline-menu-list-action",
);
newItemButton.prepend(buildSvgDomElement(plusIcon));
newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
this.newItemButtonElement.textContent = this.getNewItemButtonText();
this.newItemButtonElement.setAttribute("aria-label", this.getNewItemAriaLabel());
this.newItemButtonElement.prepend(buildSvgDomElement(plusIcon));
this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
const inlineMenuListButtonContainer = globalThis.document.createElement("div");
inlineMenuListButtonContainer.classList.add("inline-menu-list-button-container");
inlineMenuListButtonContainer.appendChild(newItemButton);
this.inlineMenuListContainer.append(noItemsMessage, inlineMenuListButtonContainer);
return this.buildButtonContainer(this.newItemButtonElement);
}
/**
* Gets the new item text for the button based on the cipher type the focused field is filled by.
*/
private getNewItemButtonText() {
if (this.filledByCipherType === CipherType.Login) {
if (this.isFilledByLoginCipher() || this.showInlineMenuAccountCreation) {
return this.getTranslation("newLogin");
}
if (this.filledByCipherType === CipherType.Card) {
if (this.isFilledByCardCipher()) {
return this.getTranslation("newCard");
}
if (this.isFilledByIdentityCipher()) {
return this.getTranslation("newIdentity");
}
return this.getTranslation("newItem");
}
@ -197,25 +230,48 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* Gets the aria label for the new item button based on the cipher type the focused field is filled by.
*/
private getNewItemAriaLabel() {
if (this.filledByCipherType === CipherType.Login) {
if (this.isFilledByLoginCipher() || this.showInlineMenuAccountCreation) {
return this.getTranslation("addNewLoginItem");
}
if (this.filledByCipherType === CipherType.Card) {
if (this.isFilledByCardCipher()) {
return this.getTranslation("addNewCardItem");
}
if (this.isFilledByIdentityCipher()) {
return this.getTranslation("addNewIdentityItem");
}
return this.getTranslation("addNewVaultItem");
}
/**
* Builds a container for a given element.
*
* @param element - The element to build the container for.
*/
private buildButtonContainer(element: Element) {
const inlineMenuListButtonContainer = globalThis.document.createElement("div");
inlineMenuListButtonContainer.classList.add("inline-menu-list-button-container");
inlineMenuListButtonContainer.appendChild(element);
return inlineMenuListButtonContainer;
}
/**
* Handles the click event for the new item button.
* Sends a message to the parent window to add a new vault item.
*/
private handeNewItemButtonClick = () => {
let addNewCipherType = this.filledByCipherType;
if (this.showInlineMenuAccountCreation) {
addNewCipherType = CipherType.Login;
}
this.postMessageToParent({
command: "addNewVaultItem",
addNewCipherType: this.filledByCipherType,
addNewCipherType,
});
};
@ -233,7 +289,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}
if (this.currentCipherIndex >= this.ciphers.length) {
globalThis.removeEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
this.ciphersList.removeEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
}
}
@ -263,7 +319,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private handleDebouncedScrollEvent = () => {
this.cipherListScrollIsDebounced = false;
if (globalThis.scrollY + globalThis.innerHeight >= this.ciphersList.clientHeight - 300) {
const scrollPercentage =
(this.ciphersList.scrollTop /
(this.ciphersList.scrollHeight - this.ciphersList.offsetHeight)) *
100;
if (scrollPercentage >= 80) {
this.loadPageOfCiphers();
}
};
@ -301,7 +361,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
const fillCipherElement = globalThis.document.createElement("button");
fillCipherElement.tabIndex = -1;
fillCipherElement.classList.add("fill-cipher-button");
fillCipherElement.classList.add("fill-cipher-button", "inline-menu-list-action");
fillCipherElement.setAttribute(
"aria-label",
`${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
@ -327,7 +387,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
if (cipher.login) {
fillCipherElement.setAttribute(
"aria-description",
`${this.getTranslation("username")}, ${cipher.login.username}`,
`${this.getTranslation("username")}: ${cipher.login.username}`,
);
return;
}
@ -398,6 +458,32 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.focusViewCipherButton(currentListItem, event.target as HTMLElement);
};
/**
* Handles the keyup event for the "New Item" button. Allows for keyboard navigation
* between ciphers elements if the other ciphers exist in the inline menu.
*
* @param event - The captured keyup event.
*/
private handleNewItemButtonKeyUpEvent = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["ArrowDown", "ArrowUp"]);
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
return;
}
if (event.code === "ArrowDown") {
const firstFillButton = this.ciphersList.firstElementChild?.querySelector(
".fill-cipher-button",
) as HTMLButtonElement;
firstFillButton?.focus();
return;
}
const lastFillButton = this.ciphersList.lastElementChild?.querySelector(
".fill-cipher-button",
) as HTMLButtonElement;
lastFillButton?.focus();
};
/**
* Builds the button that facilitates viewing a cipher in the vault.
*
@ -552,7 +638,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param cipher - The cipher to build the username login element for.
*/
private buildCipherSubtitleElement(cipher: InlineMenuCipherData): HTMLSpanElement | null {
const subTitleText = cipher.login?.username || cipher.card;
const subTitleText = this.getSubTitleText(cipher);
if (!subTitleText) {
return null;
}
@ -565,6 +651,31 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return cipherSubtitleElement;
}
/**
* Gets the subtitle text for a given cipher.
*
* @param cipher - The cipher to get the subtitle text for.
*/
private getSubTitleText(cipher: InlineMenuCipherData): string {
if (cipher.identity?.username) {
return cipher.identity.username;
}
if (cipher.identity?.fullName) {
return cipher.identity.fullName;
}
if (cipher.login?.username) {
return cipher.login.username;
}
if (cipher.card) {
return cipher.card;
}
return "";
}
/**
* Validates whether the inline menu list iframe is currently focused.
* If not focused, will check if the button element is focused.
@ -594,18 +705,10 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return;
}
const newItemButtonElement = this.inlineMenuListContainer.querySelector(
"#new-item-button",
const firstListElement = this.inlineMenuListContainer.querySelector(
".inline-menu-list-action",
) as HTMLElement;
if (newItemButtonElement) {
newItemButtonElement.focus();
return;
}
const firstCipherElement = this.inlineMenuListContainer.querySelector(
".fill-cipher-button",
) as HTMLElement;
firstCipherElement?.focus();
firstListElement?.focus();
}
/**
@ -631,6 +734,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}
const { height } = entry.contentRect;
this.toggleScrollClass(height);
this.postMessageToParent({
command: "updateAutofillInlineMenuListHeight",
styles: { height: `${height}px` },
@ -639,6 +743,25 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}
};
/**
* Toggles the scrollbar class on the inline menu list actions container.
*
* @param height - The height of the inline menu list actions container.
*/
private toggleScrollClass = (height: number) => {
if (!this.ciphersList) {
return;
}
const scrollbarClass = "inline-menu-list-actions--scrollbar";
if (height >= 170) {
this.ciphersList.classList.add(scrollbarClass);
return;
}
this.ciphersList.classList.remove(scrollbarClass);
};
/**
* Establishes a memoized event handler for a given event.
*
@ -657,14 +780,19 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
*/
private focusNextListItem(currentListItem: HTMLElement) {
const nextListItem = currentListItem.nextSibling as HTMLElement;
const nextSibling = nextListItem?.querySelector(".fill-cipher-button") as HTMLElement;
const nextSibling = nextListItem?.querySelector(".inline-menu-list-action") as HTMLElement;
if (nextSibling) {
nextSibling.focus();
return;
}
if (this.newItemButtonElement) {
this.newItemButtonElement.focus();
return;
}
const firstListItem = currentListItem.parentElement?.firstChild as HTMLElement;
const firstSibling = firstListItem?.querySelector(".fill-cipher-button") as HTMLElement;
const firstSibling = firstListItem?.querySelector(".inline-menu-list-action") as HTMLElement;
firstSibling?.focus();
}
@ -676,14 +804,21 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
*/
private focusPreviousListItem(currentListItem: HTMLElement) {
const previousListItem = currentListItem.previousSibling as HTMLElement;
const previousSibling = previousListItem?.querySelector(".fill-cipher-button") as HTMLElement;
const previousSibling = previousListItem?.querySelector(
".inline-menu-list-action",
) as HTMLElement;
if (previousSibling) {
previousSibling.focus();
return;
}
if (this.newItemButtonElement) {
this.newItemButtonElement.focus();
return;
}
const lastListItem = currentListItem.parentElement?.lastChild as HTMLElement;
const lastSibling = lastListItem?.querySelector(".fill-cipher-button") as HTMLElement;
const lastSibling = lastListItem?.querySelector(".inline-menu-list-action") as HTMLElement;
lastSibling?.focus();
}
@ -700,4 +835,25 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
const nextSibling = currentButtonElement.nextElementSibling as HTMLElement;
nextSibling?.focus();
}
/**
* Identifies if the current focused field is filled by a login cipher.
*/
private isFilledByLoginCipher = () => {
return this.filledByCipherType === CipherType.Login;
};
/**
* Identifies if the current focused field is filled by a card cipher.
*/
private isFilledByCardCipher = () => {
return this.filledByCipherType === CipherType.Card;
};
/**
* Identifies if the current focused field is filled by an identity cipher.
*/
private isFilledByIdentityCipher = () => {
return this.filledByCipherType === CipherType.Identity;
};
}

View File

@ -9,6 +9,7 @@
html {
font-size: 10px;
overflow: hidden;
}
body {
@ -42,12 +43,12 @@ body {
.inline-menu-list-button-container {
width: 100%;
padding: 0.2rem;
background: transparent;
transition: background-color 0.2s ease-in-out;
border-top-width: 0.1rem;
border-top-style: solid;
@include themify($themes) {
background: themed("backgroundColor");
border-top-color: themed("borderColor");
}
@ -110,7 +111,7 @@ body {
.add-new-item-button {
svg {
top: 0.2rem;
top: 0.25rem;
width: 1.7rem;
height: 1.7rem;
}
@ -119,6 +120,56 @@ body {
.inline-menu-list-actions {
padding: 0;
margin: 0;
max-height: 18rem;
-ms-overflow-style: none;
scrollbar-width: thin;
&--scrollbar {
overflow-y: scroll;
}
@include themify($themes) {
scrollbar-color: themed("inputBorderColor") darken(themed("backgroundColor"), 1%);
border-right-color: themed("borderColor");
}
}
.inline-menu-list-actions::-webkit-scrollbar {
width: 0.5rem;
background-color: transparent;
}
.inline-menu-list-actions::-webkit-scrollbar-button {
display: none;
}
.inline-menu-list-actions::-webkit-scrollbar-track {
background-color: transparent;
border: none;
-webkit-box-shadow: none;
box-shadow: none;
border-radius: 1rem;
}
.inline-menu-list-actions::-webkit-scrollbar-track-piece {
background-color: transparent;
border: none;
-webkit-box-shadow: none;
box-shadow: none;
}
.inline-menu-list-actions::-webkit-scrollbar-thumb {
border-radius: 1rem;
@include themify($themes) {
background-color: themed("borderColor");
}
}
.inline-menu-list-container--with-new-item-button {
.inline-menu-list-actions {
max-height: 13.8rem;
}
}
.inline-menu-list-actions-item {
@ -220,7 +271,7 @@ body {
background-repeat: no-repeat;
@include themify($themes) {
color: themed("mutedTextColor");
color: themed("buttonPrimaryColor");
}
svg {

View File

@ -124,7 +124,7 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
* @param event - The document keydown event
*/
private handleDocumentKeyDownEvent = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["Tab", "Escape"]);
const listenedForKeys = new Set(["Tab", "Escape", "ArrowUp", "ArrowDown"]);
if (!listenedForKeys.has(event.code)) {
return;
}
@ -139,7 +139,9 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
return;
}
this.sendRedirectFocusOutMessage(RedirectFocusDirection.Current);
if (event.code === "Escape") {
this.sendRedirectFocusOutMessage(RedirectFocusDirection.Current);
}
};
/**

View File

@ -10,12 +10,33 @@ export type AutofillKeywordsMap = WeakMap<
>;
export interface InlineMenuFieldQualificationService {
isUsernameField(field: AutofillField): boolean;
isNewPasswordField(field: AutofillField): boolean;
isEmailField(field: AutofillField): boolean;
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
isFieldForIdentityForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
isFieldForCardholderName(field: AutofillField): boolean;
isFieldForCardNumber(field: AutofillField): boolean;
isFieldForCardExpirationDate(field: AutofillField): boolean;
isFieldForCardExpirationMonth(field: AutofillField): boolean;
isFieldForCardExpirationYear(field: AutofillField): boolean;
isFieldForCardCvv(field: AutofillField): boolean;
isFieldForIdentityTitle(field: AutofillField): boolean;
isFieldForIdentityFirstName(field: AutofillField): boolean;
isFieldForIdentityMiddleName(field: AutofillField): boolean;
isFieldForIdentityLastName(field: AutofillField): boolean;
isFieldForIdentityFullName(field: AutofillField): boolean;
isFieldForIdentityAddress1(field: AutofillField): boolean;
isFieldForIdentityAddress2(field: AutofillField): boolean;
isFieldForIdentityAddress3(field: AutofillField): boolean;
isFieldForIdentityCity(field: AutofillField): boolean;
isFieldForIdentityState(field: AutofillField): boolean;
isFieldForIdentityPostalCode(field: AutofillField): boolean;
isFieldForIdentityCountry(field: AutofillField): boolean;
isFieldForIdentityCompany(field: AutofillField): boolean;
isFieldForIdentityPhone(field: AutofillField): boolean;
isFieldForIdentityEmail(field: AutofillField): boolean;
isFieldForIdentityUsername(field: AutofillField): boolean;
}

View File

@ -1,12 +1,19 @@
export class AutoFillConstants {
static readonly UsernameFieldNames: string[] = [
static readonly EmailFieldNames: string[] = [
// English
"username",
"user name",
"email",
"email address",
"e-mail",
"e-mail address",
// German
"email adresse",
"e-mail adresse",
];
static readonly UsernameFieldNames: string[] = [
// English
"username",
"user name",
"userid",
"user id",
"customer id",
@ -15,10 +22,9 @@ export class AutoFillConstants {
// German
"benutzername",
"benutzer name",
"email adresse",
"e-mail adresse",
"benutzerid",
"benutzer id",
...AutoFillConstants.EmailFieldNames,
];
static readonly TotpFieldNames: string[] = [

View File

@ -37,9 +37,10 @@ describe("AutofillOverlayContentService", () => {
);
autofillInit = new AutofillInit(autofillOverlayContentService);
autofillInit.init();
sendExtensionMessageSpy = jest
.spyOn(autofillOverlayContentService as any, "sendExtensionMessage")
.mockResolvedValue(undefined);
sendExtensionMessageSpy = jest.spyOn(
autofillOverlayContentService as any,
"sendExtensionMessage",
);
Object.defineProperty(document, "readyState", {
value: defaultWindowReadyState,
writable: true,
@ -422,11 +423,13 @@ describe("AutofillOverlayContentService", () => {
const randomElement = document.createElement(
"input",
) as ElementWithOpId<FillableFormFieldElement>;
jest.spyOn(autofillOverlayContentService as any, "storeUserFilledLoginField");
jest.spyOn(autofillOverlayContentService as any, "qualifyUserFilledLoginField");
autofillOverlayContentService["storeModifiedFormElement"](randomElement);
expect(autofillOverlayContentService["storeUserFilledLoginField"]).not.toHaveBeenCalled();
expect(
autofillOverlayContentService["qualifyUserFilledLoginField"],
).not.toHaveBeenCalled();
});
it("sets the field as the most recently focused form field element", async () => {
@ -464,6 +467,7 @@ describe("AutofillOverlayContentService", () => {
const passwordFieldElement = document.getElementById(
"password-field",
) as ElementWithOpId<FormFieldElement>;
autofillFieldData.type = "password";
await autofillOverlayContentService.setupInlineMenu(
passwordFieldElement,
@ -715,6 +719,295 @@ describe("AutofillOverlayContentService", () => {
);
});
});
describe("input changes on a field filled by a identity cipher", () => {
let inputFieldElement: ElementWithOpId<FillableFormFieldElement>;
let inputFieldData: AutofillField;
beforeEach(() => {
inputFieldElement = document.createElement(
"input",
) as ElementWithOpId<FillableFormFieldElement>;
inputFieldData = createAutofillFieldMock({
opid: "input-field",
form: "validFormId",
elementNumber: 3,
autoCompleteType: "given-name",
type: "text",
filledByCipherType: CipherType.Identity,
viewable: true,
});
pageDetailsMock.fields = [inputFieldData];
});
it("stores identity title fields", async () => {
inputFieldData.autoCompleteType = "honorific-prefix";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityTitle).toEqual(
inputFieldElement,
);
});
it("stores first name fields", async () => {
inputFieldData.autoCompleteType = "given-name";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityFirstName).toEqual(
inputFieldElement,
);
});
it("stores identity middle name fields", async () => {
inputFieldData.autoCompleteType = "additional-name";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityMiddleName).toEqual(
inputFieldElement,
);
});
it("stores identity last name fields", async () => {
inputFieldData.autoCompleteType = "family-name";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityLastName).toEqual(
inputFieldElement,
);
});
it("stores identity full name fields", async () => {
inputFieldData.autoCompleteType = "name";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityFullName).toEqual(
inputFieldElement,
);
});
it("stores identity address1 fields", async () => {
inputFieldData.autoCompleteType = "address-line1";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityAddress1).toEqual(
inputFieldElement,
);
});
it("stores identity address2 fields", async () => {
inputFieldData.autoCompleteType = "address-line2";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityAddress2).toEqual(
inputFieldElement,
);
});
it("stores identity address3 fields", async () => {
inputFieldData.autoCompleteType = "address-line3";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityAddress3).toEqual(
inputFieldElement,
);
});
it("stores identity city fields", async () => {
inputFieldData.autoCompleteType = "address-level2";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityCity).toEqual(
inputFieldElement,
);
});
it("stores identity state fields", async () => {
inputFieldData.autoCompleteType = "address-level1";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityState).toEqual(
inputFieldElement,
);
});
it("stores identity postal code fields", async () => {
inputFieldData.autoCompleteType = "postal-code";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityPostalCode).toEqual(
inputFieldElement,
);
});
it("stores identity country fields", async () => {
inputFieldData.autoCompleteType = "country-name";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityCountry).toEqual(
inputFieldElement,
);
});
it("stores identity company fields", async () => {
inputFieldData.autoCompleteType = "organization";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityCompany).toEqual(
inputFieldElement,
);
});
it("stores identity phone fields", async () => {
inputFieldData.autoCompleteType = "tel";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityPhone).toEqual(
inputFieldElement,
);
});
it("stores identity email fields", async () => {
jest
.spyOn(inlineMenuFieldQualificationService, "isFieldForLoginForm")
.mockReturnValue(false);
inputFieldData.autoCompleteType = "email";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityEmail).toEqual(
inputFieldElement,
);
expect(autofillOverlayContentService["userFilledFields"].username).toEqual(
inputFieldElement,
);
});
it("stores identity username fields", async () => {
jest
.spyOn(inlineMenuFieldQualificationService, "isFieldForLoginForm")
.mockReturnValue(false);
inputFieldData.autoCompleteType = "username";
await autofillOverlayContentService.setupInlineMenu(
inputFieldElement,
inputFieldData,
pageDetailsMock,
);
inputFieldElement.dispatchEvent(new Event("input"));
expect(autofillOverlayContentService["userFilledFields"].identityUsername).toEqual(
inputFieldElement,
);
expect(autofillOverlayContentService["userFilledFields"].username).toEqual(
inputFieldElement,
);
});
});
});
describe("form field click event listener", () => {
@ -1062,6 +1355,67 @@ describe("AutofillOverlayContentService", () => {
);
});
});
describe("setting up the form field listeners on account creation fields", () => {
const inputAccountFieldData = createAutofillFieldMock({
opid: "create-account-field",
form: "validFormId",
elementNumber: 3,
autoCompleteType: "username",
placeholder: "new username",
type: "text",
viewable: true,
});
const passwordAccountFieldData = createAutofillFieldMock({
opid: "create-account-password-field",
form: "validFormId",
elementNumber: 4,
autoCompleteType: "new-password",
placeholder: "new password",
type: "password",
viewable: true,
});
beforeEach(() => {
pageDetailsMock.fields = [inputAccountFieldData, passwordAccountFieldData];
jest
.spyOn(inlineMenuFieldQualificationService, "isFieldForLoginForm")
.mockReturnValue(false);
});
it("sets up the field listeners on the field", async () => {
await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
inputAccountFieldData,
pageDetailsMock,
);
await flushPromises();
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
EVENTS.BLUR,
expect.any(Function),
);
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
EVENTS.KEYUP,
expect.any(Function),
);
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
EVENTS.INPUT,
expect.any(Function),
);
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
EVENTS.CLICK,
expect.any(Function),
);
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
EVENTS.FOCUS,
expect.any(Function),
);
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
expect(inputAccountFieldData.filledByCipherType).toEqual(CipherType.Identity);
expect(inputAccountFieldData.showInlineMenuAccountCreation).toEqual(true);
});
});
});
it("skips triggering the form field focused handler if the document is not focused", async () => {
@ -1406,6 +1760,40 @@ describe("AutofillOverlayContentService", () => {
},
});
});
it("sends a message that facilitates adding an identity cipher vault item", async () => {
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
sendMockExtensionMessage({
command: "addNewVaultItemFromOverlay",
addNewCipherType: CipherType.Identity,
});
await flushPromises();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
addNewCipherType: CipherType.Identity,
identity: {
address1: "",
address2: "",
address3: "",
city: "",
company: "",
country: "",
email: "",
firstName: "",
fullName: "",
lastName: "",
middleName: "",
phone: "",
postalCode: "",
state: "",
title: "",
username: "",
},
});
});
});
describe("unsetMostRecentlyFocusedField message handler", () => {

View File

@ -12,9 +12,13 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import {
FocusedFieldData,
NewCardCipherData,
NewIdentityCipherData,
NewLoginCipherData,
SubFrameOffsetData,
} from "../background/abstractions/overlay.background";
import { AutofillExtensionMessage } from "../content/abstractions/autofill-init";
import { AutofillFieldQualifier, AutofillFieldQualifierType } from "../enums/autofill-field.enums";
import {
AutofillOverlayElement,
MAX_SUB_FRAME_DEPTH,
@ -76,6 +80,54 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(),
destroyAutofillInlineMenuListeners: () => this.destroy(),
};
private readonly cardFieldQualifiers: Record<string, CallableFunction> = {
[AutofillFieldQualifier.cardholderName]:
this.inlineMenuFieldQualificationService.isFieldForCardholderName,
[AutofillFieldQualifier.cardNumber]:
this.inlineMenuFieldQualificationService.isFieldForCardNumber,
[AutofillFieldQualifier.cardExpirationMonth]:
this.inlineMenuFieldQualificationService.isFieldForCardExpirationMonth,
[AutofillFieldQualifier.cardExpirationYear]:
this.inlineMenuFieldQualificationService.isFieldForCardExpirationYear,
[AutofillFieldQualifier.cardExpirationDate]:
this.inlineMenuFieldQualificationService.isFieldForCardExpirationDate,
[AutofillFieldQualifier.cardCvv]: this.inlineMenuFieldQualificationService.isFieldForCardCvv,
};
private readonly identityFieldQualifiers: Record<string, CallableFunction> = {
[AutofillFieldQualifier.identityTitle]:
this.inlineMenuFieldQualificationService.isFieldForIdentityTitle,
[AutofillFieldQualifier.identityFirstName]:
this.inlineMenuFieldQualificationService.isFieldForIdentityFirstName,
[AutofillFieldQualifier.identityMiddleName]:
this.inlineMenuFieldQualificationService.isFieldForIdentityMiddleName,
[AutofillFieldQualifier.identityLastName]:
this.inlineMenuFieldQualificationService.isFieldForIdentityLastName,
[AutofillFieldQualifier.identityFullName]:
this.inlineMenuFieldQualificationService.isFieldForIdentityFullName,
[AutofillFieldQualifier.identityAddress1]:
this.inlineMenuFieldQualificationService.isFieldForIdentityAddress1,
[AutofillFieldQualifier.identityAddress2]:
this.inlineMenuFieldQualificationService.isFieldForIdentityAddress2,
[AutofillFieldQualifier.identityAddress3]:
this.inlineMenuFieldQualificationService.isFieldForIdentityAddress3,
[AutofillFieldQualifier.identityCity]:
this.inlineMenuFieldQualificationService.isFieldForIdentityCity,
[AutofillFieldQualifier.identityState]:
this.inlineMenuFieldQualificationService.isFieldForIdentityState,
[AutofillFieldQualifier.identityPostalCode]:
this.inlineMenuFieldQualificationService.isFieldForIdentityPostalCode,
[AutofillFieldQualifier.identityCountry]:
this.inlineMenuFieldQualificationService.isFieldForIdentityCountry,
[AutofillFieldQualifier.identityCompany]:
this.inlineMenuFieldQualificationService.isFieldForIdentityCompany,
[AutofillFieldQualifier.identityPhone]:
this.inlineMenuFieldQualificationService.isFieldForIdentityPhone,
[AutofillFieldQualifier.identityEmail]:
this.inlineMenuFieldQualificationService.isFieldForIdentityEmail,
[AutofillFieldQualifier.identityUsername]:
this.inlineMenuFieldQualificationService.isFieldForIdentityUsername,
[AutofillFieldQualifier.password]: this.inlineMenuFieldQualificationService.isNewPasswordField,
};
constructor(private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService) {}
@ -204,7 +256,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
const command = "autofillOverlayAddNewVaultItem";
if (addNewCipherType === CipherType.Login) {
const login = {
const login: NewLoginCipherData = {
username: this.userFilledFields["username"]?.value || "",
password: this.userFilledFields["password"]?.value || "",
uri: globalThis.document.URL,
@ -217,7 +269,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (addNewCipherType === CipherType.Card) {
const card = {
const card: NewCardCipherData = {
cardholderName: this.userFilledFields["cardholderName"]?.value || "",
number: this.userFilledFields["cardNumber"]?.value || "",
expirationMonth: this.userFilledFields["cardExpirationMonth"]?.value || "",
@ -227,6 +279,31 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
};
void this.sendExtensionMessage(command, { addNewCipherType, card });
return;
}
if (addNewCipherType === CipherType.Identity) {
const identity: NewIdentityCipherData = {
title: this.userFilledFields["identityTitle"]?.value || "",
firstName: this.userFilledFields["identityFirstName"]?.value || "",
middleName: this.userFilledFields["identityMiddleName"]?.value || "",
lastName: this.userFilledFields["identityLastName"]?.value || "",
fullName: this.userFilledFields["identityFullName"]?.value || "",
address1: this.userFilledFields["identityAddress1"]?.value || "",
address2: this.userFilledFields["identityAddress2"]?.value || "",
address3: this.userFilledFields["identityAddress3"]?.value || "",
city: this.userFilledFields["identityCity"]?.value || "",
state: this.userFilledFields["identityState"]?.value || "",
postalCode: this.userFilledFields["identityPostalCode"]?.value || "",
country: this.userFilledFields["identityCountry"]?.value || "",
company: this.userFilledFields["identityCompany"]?.value || "",
phone: this.userFilledFields["identityPhone"]?.value || "",
email: this.userFilledFields["identityEmail"]?.value || "",
username: this.userFilledFields["identityUsername"]?.value || "",
};
void this.sendExtensionMessage(command, { addNewCipherType, identity });
}
}
@ -459,67 +536,92 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return;
}
if (autofillFieldData.filledByCipherType === CipherType.Login) {
this.storeUserFilledLoginField(formFieldElement);
if (!autofillFieldData.fieldQualifier) {
switch (autofillFieldData.filledByCipherType) {
case CipherType.Login:
this.qualifyUserFilledLoginField(autofillFieldData);
break;
case CipherType.Card:
this.qualifyUserFilledCardField(autofillFieldData);
break;
case CipherType.Identity:
this.qualifyUserFilledIdentityField(autofillFieldData);
break;
}
}
if (autofillFieldData.filledByCipherType === CipherType.Card) {
this.storeUserFilledCardField(formFieldElement, autofillFieldData);
}
this.storeQualifiedUserFilledField(formFieldElement, autofillFieldData);
}
/**
* Handles storing the user field login field to be used when adding a new vault item.
* Handles qualifying the user field login field to be used when adding a new vault item.
*
* @param formFieldElement - The form field element that triggered the input event.
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private storeUserFilledLoginField(formFieldElement: ElementWithOpId<FillableFormFieldElement>) {
if (formFieldElement.type === "password") {
this.userFilledFields.password = formFieldElement;
private qualifyUserFilledLoginField(autofillFieldData: AutofillField) {
if (autofillFieldData.type === "password") {
autofillFieldData.fieldQualifier = AutofillFieldQualifier.password;
return;
}
this.userFilledFields.username = formFieldElement;
autofillFieldData.fieldQualifier = AutofillFieldQualifier.username;
}
/**
* Handles storing the user field card field to be used when adding a new vault item.
* Handles qualifying the user field card field to be used when adding a new vault item.
*
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private qualifyUserFilledCardField(autofillFieldData: AutofillField) {
for (const [fieldQualifier, fieldQualifierFunction] of Object.entries(
this.cardFieldQualifiers,
)) {
if (fieldQualifierFunction(autofillFieldData)) {
autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType;
return;
}
}
}
/**
* Handles qualifying the user field identity field to be used when adding a new vault item.
*
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private qualifyUserFilledIdentityField(autofillFieldData: AutofillField) {
for (const [fieldQualifier, fieldQualifierFunction] of Object.entries(
this.identityFieldQualifiers,
)) {
if (fieldQualifierFunction(autofillFieldData)) {
autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType;
return;
}
}
}
/**
* Stores the qualified user filled filed to allow for referencing its value when adding a new vault item.
*
* @param formFieldElement - The form field element that triggered the input event.
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private storeUserFilledCardField(
private storeQualifiedUserFilledField(
formFieldElement: ElementWithOpId<FillableFormFieldElement>,
autofillFieldData: AutofillField,
) {
if (this.inlineMenuFieldQualificationService.isFieldForCardholderName(autofillFieldData)) {
this.userFilledFields.cardholderName = formFieldElement;
if (!autofillFieldData.fieldQualifier) {
return;
}
if (this.inlineMenuFieldQualificationService.isFieldForCardNumber(autofillFieldData)) {
this.userFilledFields.cardNumber = formFieldElement;
return;
const identityLoginFields: AutofillFieldQualifierType[] = [
AutofillFieldQualifier.identityUsername,
AutofillFieldQualifier.identityEmail,
];
if (identityLoginFields.includes(autofillFieldData.fieldQualifier)) {
this.userFilledFields[AutofillFieldQualifier.username] = formFieldElement;
}
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationMonth(autofillFieldData)) {
this.userFilledFields.cardExpirationMonth = formFieldElement;
return;
}
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationYear(autofillFieldData)) {
this.userFilledFields.cardExpirationYear = formFieldElement;
return;
}
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationDate(autofillFieldData)) {
this.userFilledFields.cardExpirationDate = formFieldElement;
return;
}
if (this.inlineMenuFieldQualificationService.isFieldForCardCvv(autofillFieldData)) {
this.userFilledFields.cardCvv = formFieldElement;
}
this.userFilledFields[autofillFieldData.fieldQualifier] = formFieldElement;
}
/**
@ -665,10 +767,25 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
const { width, height, top, left } =
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
const autofillFieldData = this.formFieldElements.get(formFieldElement);
let accountCreationFieldType = null;
if (
(autofillFieldData?.showInlineMenuAccountCreation ||
autofillFieldData?.filledByCipherType === CipherType.Login) &&
this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData)
) {
accountCreationFieldType = this.inlineMenuFieldQualificationService.isEmailField(
autofillFieldData,
)
? "email"
: autofillFieldData.type;
}
this.focusedFieldData = {
focusedFieldStyles: { paddingRight, paddingLeft },
focusedFieldRects: { width, height, top, left },
filledByCipherType: autofillFieldData?.filledByCipherType,
showInlineMenuAccountCreation: autofillFieldData?.showInlineMenuAccountCreation,
accountCreationFieldType,
};
await this.sendExtensionMessage("updateFocusedFieldData", {
@ -763,6 +880,27 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return false;
}
if (
this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm(
autofillFieldData,
pageDetails,
)
) {
autofillFieldData.filledByCipherType = CipherType.Identity;
autofillFieldData.showInlineMenuAccountCreation = true;
return false;
}
if (
this.inlineMenuFieldQualificationService.isFieldForIdentityForm(
autofillFieldData,
pageDetails,
)
) {
autofillFieldData.filledByCipherType = CipherType.Identity;
return false;
}
return true;
}

View File

@ -1236,7 +1236,10 @@ export default class AutofillService implements AutofillServiceInterface {
const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f) => {
if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes)) {
if (
AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes) ||
["current-password", "new-password"].includes(f.autoCompleteType)
) {
return;
}

View File

@ -15,7 +15,7 @@ import {
elementIsTextAreaElement,
nodeIsFormElement,
nodeIsInputElement,
// sendExtensionMessage,
sendExtensionMessage,
getAttributeBoolean,
getPropertyOrAttribute,
requestIdleCallbackPolyfill,
@ -32,6 +32,7 @@ import {
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
class CollectAutofillContentService implements CollectAutofillContentServiceInterface {
private readonly sendExtensionMessage = sendExtensionMessage;
private readonly domElementVisibilityService: DomElementVisibilityService;
private readonly autofillOverlayContentService: AutofillOverlayContentService;
private readonly getAttributeBoolean = getAttributeBoolean;
@ -1037,6 +1038,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
this.domRecentlyMutated = true;
if (this.autofillOverlayContentService) {
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true });
}
this.noFieldsFound = false;

View File

@ -16,8 +16,10 @@ describe("InlineMenuFieldQualificationService", () => {
forms: {},
fields: [],
});
chrome.runtime.sendMessage = jest.fn().mockImplementation((message) => ({
result: message.command === "getInlineMenuFieldQualificationFeatureFlag",
}));
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
inlineMenuFieldQualificationService["inlineMenuFieldQualificationFlagSet"] = true;
});
describe("isFieldForLoginForm", () => {
@ -864,4 +866,73 @@ describe("InlineMenuFieldQualificationService", () => {
});
});
});
describe("isFieldForAccountCreationForm", () => {
it("validates a field for an account creation if the field is formless but at least one new password field exists in the page details", () => {
const field = mock<AutofillField>({
placeholder: "username",
autoCompleteType: "username",
type: "text",
htmlName: "username",
htmlID: "username",
});
const passwordField = mock<AutofillField>({
placeholder: "new password",
autoCompleteType: "new-password",
type: "password",
htmlName: "new-password",
htmlID: "new-password",
});
pageDetails.forms = {};
pageDetails.fields = [field, passwordField];
expect(
inlineMenuFieldQualificationService.isFieldForAccountCreationForm(field, pageDetails),
).toBe(true);
});
it("validates a field for an account creation if the field is formless and contains an account creation keyword", () => {
const field = mock<AutofillField>({
placeholder: "register username",
autoCompleteType: "username",
type: "text",
htmlName: "username",
htmlID: "username",
});
pageDetails.forms = {};
pageDetails.fields = [field];
expect(
inlineMenuFieldQualificationService.isFieldForAccountCreationForm(field, pageDetails),
).toBe(true);
});
});
describe("isFieldForIdentityUsername", () => {
it("returns true if the field contains a keyword indicating that it is for a username field", () => {
const field = mock<AutofillField>({
placeholder: "user-name",
autoCompleteType: "",
type: "text",
htmlName: "user-name",
htmlID: "user-name",
});
expect(inlineMenuFieldQualificationService.isFieldForIdentityUsername(field)).toBe(true);
});
});
describe("isEmailField", () => {
it("returns true if the field type is of `email`", () => {
const field = mock<AutofillField>({
placeholder: "email",
autoCompleteType: "",
type: "email",
htmlName: "email",
htmlID: "email",
});
expect(inlineMenuFieldQualificationService.isEmailField(field)).toBe(true);
});
});
});

View File

@ -6,7 +6,11 @@ import {
AutofillKeywordsMap,
InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface,
} from "./abstractions/inline-menu-field-qualifications.service";
import { AutoFillConstants, CreditCardAutoFillConstants } from "./autofill-constants";
import {
AutoFillConstants,
CreditCardAutoFillConstants,
IdentityAutoFillConstants,
} from "./autofill-constants";
export class InlineMenuFieldQualificationService
implements InlineMenuFieldQualificationServiceInterface
@ -14,34 +18,33 @@ export class InlineMenuFieldQualificationService
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
private usernameAutocompleteValues = new Set(["username", "email"]);
private usernameAutocompleteValue = "username";
private emailAutocompleteValue = "email";
private loginUsernameAutocompleteValues = new Set([
this.usernameAutocompleteValue,
this.emailAutocompleteValue,
]);
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
private currentPasswordAutocompleteValue = "current-password";
private newPasswordAutoCompleteValue = "new-password";
private passwordAutoCompleteValues = new Set([
this.currentPasswordAutocompleteValue,
this.newPasswordAutoCompleteValue,
]);
private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap();
private autocompleteDisabledValues = new Set(["off", "false"]);
private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
private accountCreationFieldKeywords = new Set([
"register",
"registration",
"create",
"confirm",
...this.newFieldKeywords,
]);
private creditCardFieldKeywords = new Set([
...CreditCardAutoFillConstants.CardHolderFieldNames,
...CreditCardAutoFillConstants.CardNumberFieldNames,
...CreditCardAutoFillConstants.CardExpiryFieldNames,
...CreditCardAutoFillConstants.ExpiryMonthFieldNames,
...CreditCardAutoFillConstants.ExpiryYearFieldNames,
...CreditCardAutoFillConstants.CVVFieldNames,
...CreditCardAutoFillConstants.CardBrandFieldNames,
]);
private accountCreationFieldKeywords = [
...new Set(["register", "registration", "create", "confirm", ...this.newFieldKeywords]),
];
private creditCardFieldKeywords = [
...new Set([
...CreditCardAutoFillConstants.CardHolderFieldNames,
...CreditCardAutoFillConstants.CardNumberFieldNames,
...CreditCardAutoFillConstants.CardExpiryFieldNames,
...CreditCardAutoFillConstants.ExpiryMonthFieldNames,
...CreditCardAutoFillConstants.ExpiryYearFieldNames,
...CreditCardAutoFillConstants.CVVFieldNames,
...CreditCardAutoFillConstants.CardBrandFieldNames,
]),
];
private creditCardNameAutocompleteValues = new Set([
"cc-name",
"cc-given-name,",
@ -63,6 +66,79 @@ export class InlineMenuFieldQualificationService
this.creditCardCvvAutocompleteValue,
this.creditCardTypeAutocompleteValue,
]);
private identityHonorificPrefixAutocompleteValue = "honorific-prefix";
private identityFullNameAutocompleteValue = "name";
private identityFirstNameAutocompleteValue = "given-name";
private identityMiddleNameAutocompleteValue = "additional-name";
private identityLastNameAutocompleteValue = "family-name";
private identityNameAutocompleteValues = new Set([
this.identityFullNameAutocompleteValue,
this.identityHonorificPrefixAutocompleteValue,
this.identityFirstNameAutocompleteValue,
this.identityMiddleNameAutocompleteValue,
this.identityLastNameAutocompleteValue,
"honorific-suffix",
"nickname",
]);
private identityCompanyAutocompleteValue = "organization";
private identityStreetAddressAutocompleteValue = "street-address";
private identityAddressLine1AutocompleteValue = "address-line1";
private identityAddressLine2AutocompleteValue = "address-line2";
private identityAddressLine3AutocompleteValue = "address-line3";
private identityAddressCityAutocompleteValue = "address-level2";
private identityAddressStateAutocompleteValue = "address-level1";
private identityAddressAutoCompleteValues = new Set([
this.identityStreetAddressAutocompleteValue,
this.identityAddressLine1AutocompleteValue,
this.identityAddressLine2AutocompleteValue,
this.identityAddressLine3AutocompleteValue,
this.identityAddressCityAutocompleteValue,
this.identityAddressStateAutocompleteValue,
"shipping",
"billing",
"address-level4",
"address-level3",
]);
private identityCountryAutocompleteValues = new Set(["country", "country-name"]);
private identityPostalCodeAutocompleteValue = "postal-code";
private identityPhoneAutocompleteValue = "tel";
private identityPhoneNumberAutocompleteValues = new Set([
this.identityPhoneAutocompleteValue,
"tel-country-code",
"tel-area-code",
"tel-local",
"tel-extension",
]);
private identityAutocompleteValues = new Set([
...this.identityNameAutocompleteValues,
...this.loginUsernameAutocompleteValues,
...this.identityCompanyAutocompleteValue,
...this.identityAddressAutoCompleteValues,
...this.identityCountryAutocompleteValues,
...this.identityPhoneNumberAutocompleteValues,
this.identityPostalCodeAutocompleteValue,
]);
private identityFieldKeywords = [
...new Set([
...IdentityAutoFillConstants.TitleFieldNames,
...IdentityAutoFillConstants.FullNameFieldNames,
...IdentityAutoFillConstants.FirstnameFieldNames,
...IdentityAutoFillConstants.MiddlenameFieldNames,
...IdentityAutoFillConstants.LastnameFieldNames,
...IdentityAutoFillConstants.AddressFieldNames,
...IdentityAutoFillConstants.Address1FieldNames,
...IdentityAutoFillConstants.Address2FieldNames,
...IdentityAutoFillConstants.Address3FieldNames,
...IdentityAutoFillConstants.PostalCodeFieldNames,
...IdentityAutoFillConstants.CityFieldNames,
...IdentityAutoFillConstants.StateFieldNames,
...IdentityAutoFillConstants.CountryFieldNames,
...IdentityAutoFillConstants.CompanyFieldNames,
...IdentityAutoFillConstants.PhoneFieldNames,
...IdentityAutoFillConstants.EmailFieldNames,
...IdentityAutoFillConstants.UserNameFieldNames,
]),
];
private inlineMenuFieldQualificationFlagSet = false;
constructor() {
@ -135,7 +211,7 @@ export class InlineMenuFieldQualificationService
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords])
this.keywordsFoundInFieldData(field, this.creditCardFieldKeywords)
);
}
@ -162,6 +238,64 @@ export class InlineMenuFieldQualificationService
);
}
/** Validates the provided field as a field for an account creation form.
*
* @param field - The field to validate
* @param pageDetails - The details of the page that the field is on.
*/
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
if (!this.isUsernameField(field) && !this.isPasswordField(field)) {
return false;
}
const parentForm = pageDetails.forms[field.form];
if (!parentForm) {
// If the field does not have a parent form, but we can identify that the page contains at least
// one new password field, we should assume that the field is part of an account creation form.
const newPasswordFields = pageDetails.fields.filter(this.isNewPasswordField);
if (newPasswordFields.length >= 1) {
return true;
}
// If no password fields are found on the page, check for keywords that indicate the field is
// part of an account creation form.
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
);
}
// If the field has a parent form, check the fields from that form exclusively
const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form);
const newPasswordFields = fieldsFromSameForm.filter(this.isNewPasswordField);
if (newPasswordFields.length >= 1) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
);
}
/**
* Validates the provided field as a field for an identity form.
*
* @param field - The field to validate
* @param _pageDetails - Currently unused, will likely be required in the future
*/
isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean {
if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, this.identityFieldKeywords)
);
}
/**
* Validates the provided field as a password field for a login form.
*
@ -245,7 +379,7 @@ export class InlineMenuFieldQualificationService
): boolean {
// If the provided field is set with an autocomplete of "username", we should assume that
// the page developer intends for this field to be interpreted as a username field.
if (this.fieldContainsAutocompleteValues(field, this.usernameAutocompleteValues)) {
if (this.fieldContainsAutocompleteValues(field, this.loginUsernameAutocompleteValues)) {
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField);
return newPasswordFieldsInPageDetails.length === 0;
}
@ -329,7 +463,7 @@ export class InlineMenuFieldQualificationService
}
/**
* Validates the provided field as a field for a credit card name field.
* Validates the provided field as a credit card name field.
*
* @param field - The field to validate
*/
@ -345,7 +479,7 @@ export class InlineMenuFieldQualificationService
};
/**
* Validates the provided field as a field for a credit card number field.
* Validates the provided field as a credit card number field.
*
* @param field - The field to validate
*/
@ -361,7 +495,7 @@ export class InlineMenuFieldQualificationService
};
/**
* Validates the provided field as a field for a credit card expiration date field.
* Validates the provided field as a credit card expiration date field.
*
* @param field - The field to validate
*/
@ -379,7 +513,7 @@ export class InlineMenuFieldQualificationService
};
/**
* Validates the provided field as a field for a credit card expiration month field.
* Validates the provided field as a credit card expiration month field.
*
* @param field - The field to validate
*/
@ -397,7 +531,7 @@ export class InlineMenuFieldQualificationService
};
/**
* Validates the provided field as a field for a credit card expiration year field.
* Validates the provided field as a credit card expiration year field.
*
* @param field - The field to validate
*/
@ -415,7 +549,7 @@ export class InlineMenuFieldQualificationService
};
/**
* Validates the provided field as a field for a credit card CVV field.
* Validates the provided field as a credit card CVV field.
*
* @param field - The field to validate
*/
@ -430,12 +564,273 @@ export class InlineMenuFieldQualificationService
);
};
/**
* Validates the provided field as an identity title type field.
*
* @param field - The field to validate
*/
isFieldForIdentityTitle = (field: AutofillField): boolean => {
if (
this.fieldContainsAutocompleteValues(field, this.identityHonorificPrefixAutocompleteValue)
) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.TitleFieldNames, false)
);
};
/**
* Validates the provided field as an identity full name field.
*
* @param field
*/
isFieldForIdentityFirstName = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityFirstNameAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.FirstnameFieldNames, false)
);
};
/**
* Validates the provided field as an identity middle name field.
*
* @param field - The field to validate
*/
isFieldForIdentityMiddleName = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityMiddleNameAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.MiddlenameFieldNames, false)
);
};
/**
* Validates the provided field as an identity last name field.
*
* @param field - The field to validate
*/
isFieldForIdentityLastName = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityLastNameAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.LastnameFieldNames, false)
);
};
/**
* Validates the provided field as an identity full name field.
*
* @param field - The field to validate
*/
isFieldForIdentityFullName = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityFullNameAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.FullNameFieldNames, false)
);
};
/**
* Validates the provided field as an identity address field.
*
* @param field - The field to validate
*/
isFieldForIdentityAddress1 = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine1AutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address1FieldNames, false)
);
};
/**
* Validates the provided field as an identity address field.
*
* @param field - The field to validate
*/
isFieldForIdentityAddress2 = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine2AutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address2FieldNames, false)
);
};
/**
* Validates the provided field as an identity address field.
*
* @param field - The field to validate
*/
isFieldForIdentityAddress3 = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine3AutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address3FieldNames, false)
);
};
/**
* Validates the provided field as an identity city field.
*
* @param field - The field to validate
*/
isFieldForIdentityCity = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityAddressCityAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CityFieldNames, false)
);
};
/**
* Validates the provided field as an identity state field.
*
* @param field - The field to validate
*/
isFieldForIdentityState = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityAddressStateAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.AddressFieldNames, false)
);
};
/**
* Validates the provided field as an identity postal code field.
*
* @param field - The field to validate
*/
isFieldForIdentityPostalCode = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityPostalCodeAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PostalCodeFieldNames, false)
);
};
/**
* Validates the provided field as an identity country field.
*
* @param field - The field to validate
*/
isFieldForIdentityCountry = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityCountryAutocompleteValues)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CountryFieldNames, false)
);
};
/**
* Validates the provided field as an identity company field.
*
* @param field - The field to validate
*/
isFieldForIdentityCompany = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityCompanyAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CompanyFieldNames, false)
);
};
/**
* Validates the provided field as an identity phone field.
*
* @param field - The field to validate
*/
isFieldForIdentityPhone = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.identityPhoneAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PhoneFieldNames, false)
);
};
/**
* Validates the provided field as an identity email field.
*
* @param field - The field to validate
*/
isFieldForIdentityEmail = (field: AutofillField): boolean => {
if (
this.fieldContainsAutocompleteValues(field, this.emailAutocompleteValue) ||
field.type === "email"
) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.EmailFieldNames, false)
);
};
/**
* Validates the provided field as an identity username field.
*
* @param field - The field to validate
*/
isFieldForIdentityUsername = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.usernameAutocompleteValue)) {
return true;
}
return (
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.UserNameFieldNames, false)
);
};
/**
* Validates the provided field as a username field.
*
* @param field - The field to validate
*/
private isUsernameField = (field: AutofillField): boolean => {
isUsernameField = (field: AutofillField): boolean => {
if (
!this.usernameFieldTypes.has(field.type) ||
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)
@ -446,6 +841,22 @@ export class InlineMenuFieldQualificationService
return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames);
};
/**
* Validates the provided field as an email field.
*
* @param field - The field to validate
*/
isEmailField = (field: AutofillField): boolean => {
if (field.type === "email") {
return true;
}
return (
!this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) &&
this.keywordsFoundInFieldData(field, AutoFillConstants.EmailFieldNames)
);
};
/**
* Validates the provided field as a current password field.
*
@ -454,7 +865,7 @@ export class InlineMenuFieldQualificationService
private isCurrentPasswordField = (field: AutofillField): boolean => {
if (
this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue) ||
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
) {
return false;
}
@ -467,14 +878,14 @@ export class InlineMenuFieldQualificationService
*
* @param field - The field to validate
*/
private isNewPasswordField = (field: AutofillField): boolean => {
isNewPasswordField = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
return false;
}
return (
this.isPasswordField(field) &&
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
);
};

View File

@ -7,7 +7,10 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { InlineMenuCipherData } from "../background/abstractions/overlay.background";
import {
FocusedFieldData,
InlineMenuCipherData,
} from "../background/abstractions/overlay.background";
import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form";
import AutofillPageDetails from "../models/autofill-page-details";
@ -243,7 +246,9 @@ export function createInitAutofillInlineMenuListMessageMock(
};
}
export function createFocusedFieldDataMock(customFields = {}) {
export function createFocusedFieldDataMock(
customFields: Partial<FocusedFieldData> = {},
): FocusedFieldData {
return {
focusedFieldRects: {
top: 1,

View File

@ -38,25 +38,14 @@ describe("generateRandomCustomElementName", () => {
describe("sendExtensionMessage", () => {
it("sends a message to the extension", async () => {
const extensionMessagePromise = sendExtensionMessage("some-extension-message");
chrome.runtime.sendMessage = jest.fn().mockResolvedValue("sendMessageResponse");
// Jest doesn't give anyway to select the typed overload of "sendMessage",
// a cast is needed to get the correct spy type.
const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage") as unknown as jest.SpyInstance<
void,
[message: string, responseCallback: (response: string) => void],
unknown
>;
expect(sendMessageSpy).toHaveBeenCalled();
const [latestCall] = sendMessageSpy.mock.calls;
const responseCallback = latestCall[1];
responseCallback("sendMessageResponse");
const response = await extensionMessagePromise;
const response = await sendExtensionMessage("some-extension-message", { value: "value" });
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
command: "some-extension-message",
value: "value",
});
expect(response).toEqual("sendMessageResponse");
});
});

View File

@ -104,16 +104,8 @@ export function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLEl
export async function sendExtensionMessage(
command: string,
options: Record<string, any> = {},
): Promise<any | void> {
return new Promise((resolve) => {
chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => {
if (chrome.runtime.lastError) {
// Do nothing
}
resolve(response);
});
});
): Promise<any> {
return chrome.runtime.sendMessage({ command, ...options });
}
/**

View File

@ -11,7 +11,7 @@ const lockIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M8.799 11.633a.68.68 0 0 0-.639.422.695.695 0 0 0-.054.264.682.682 0 0 0 .374.6v1.13a.345.345 0 1 0 .693 0v-1.17a.68.68 0 0 0 .315-.56.695.695 0 0 0-.204-.486.682.682 0 0 0-.485-.2Zm4.554-4.657h-7.11a.438.438 0 0 1-.406-.26A3.81 3.81 0 0 1 5.584 4.3c.112-.435.312-.842.588-1.195A3.196 3.196 0 0 1 7.19 2.25a3.468 3.468 0 0 1 3.225-.059A3.62 3.62 0 0 1 11.94 3.71l.327.59a.502.502 0 1 0 .885-.483l-.307-.552a4.689 4.689 0 0 0-2.209-2.078 4.466 4.466 0 0 0-3.936.185A4.197 4.197 0 0 0 5.37 2.49a4.234 4.234 0 0 0-.768 1.565 4.714 4.714 0 0 0 .162 2.682.182.182 0 0 1-.085.22.173.173 0 0 1-.082.02h-.353a1.368 1.368 0 0 0-1.277.842c-.07.168-.107.348-.109.53v7.1a1.392 1.392 0 0 0 .412.974 1.352 1.352 0 0 0 .974.394h9.117c.363.001.711-.142.97-.4a1.39 1.39 0 0 0 .407-.972v-7.1a1.397 1.397 0 0 0-.414-.973 1.368 1.368 0 0 0-.972-.396Zm.37 8.469a.373.373 0 0 1-.11.26.364.364 0 0 1-.26.107H4.246a.366.366 0 0 1-.26-.107.374.374 0 0 1-.11-.261V8.349a.374.374 0 0 1 .11-.26.366.366 0 0 1 .26-.108h9.116a.366.366 0 0 1 .37.367l-.008 7.097Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M.798.817h16v16h-16z"/></clipPath></defs></svg>';
const plusIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .49h16v16H0z"/></clipPath></defs></svg>';
'<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" fill-rule="evenodd" d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M.421.421h16v16h-16z"/></clipPath></defs></svg>';
const viewCipherIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .113h20v19.773H0z"/></clipPath></defs></svg>';

View File

@ -347,6 +347,7 @@ export default class MainBackground {
kdfConfigService: kdfConfigServiceAbstraction;
offscreenDocumentService: OffscreenDocumentService;
syncServiceListener: SyncServiceListener;
themeStateService: DefaultThemeStateService;
onUpdatedRan: boolean;
onReplacedRan: boolean;
@ -568,7 +569,7 @@ export default class MainBackground {
migrationRunner,
);
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
@ -1055,7 +1056,7 @@ export default class MainBackground {
this.domainSettingsService,
this.environmentService,
this.logService,
themeStateService,
this.themeStateService,
this.configService,
);
@ -1147,47 +1148,6 @@ export default class MainBackground {
}
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
this.configService
.getFeatureFlag(FeatureFlag.InlineMenuPositioningImprovements)
.then(async (enabled) => {
if (!enabled) {
this.overlayBackground = new LegacyOverlayBackground(
this.cipherService,
this.autofillService,
this.authService,
this.environmentService,
this.domainSettingsService,
this.autofillSettingsService,
this.i18nService,
this.platformUtilsService,
themeStateService,
);
} else {
this.overlayBackground = new OverlayBackground(
this.logService,
this.cipherService,
this.autofillService,
this.authService,
this.environmentService,
this.domainSettingsService,
this.autofillSettingsService,
this.i18nService,
this.platformUtilsService,
themeStateService,
);
}
this.tabsBackground = new TabsBackground(
this,
this.notificationBackground,
this.overlayBackground,
);
await this.overlayBackground.init();
await this.tabsBackground.init();
})
.catch((error) => this.logService.error(`Error initializing OverlayBackground: ${error}`));
}
async bootstrap() {
@ -1238,11 +1198,13 @@ export default class MainBackground {
);
}
await this.initOverlayAndTabsBackground();
return new Promise<void>((resolve) => {
setTimeout(async () => {
await this.refreshBadge();
await this.fullSync(true);
await this.taskSchedulerService.setInterval(
this.taskSchedulerService.setInterval(
ScheduledTaskNames.scheduleNextSyncInterval,
5 * 60 * 1000, // check every 5 minutes
);
@ -1509,4 +1471,50 @@ export default class MainBackground {
await this.syncService.fullSync(override);
}
}
/**
* Temporary solution to handle initialization of the overlay background behind a feature flag.
* Will be reverted to instantiation within the constructor once the feature flag is removed.
*/
private async initOverlayAndTabsBackground() {
const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.InlineMenuPositioningImprovements,
);
if (!inlineMenuPositioningImprovementsEnabled) {
this.overlayBackground = new LegacyOverlayBackground(
this.cipherService,
this.autofillService,
this.authService,
this.environmentService,
this.domainSettingsService,
this.autofillSettingsService,
this.i18nService,
this.platformUtilsService,
this.themeStateService,
);
} else {
this.overlayBackground = new OverlayBackground(
this.logService,
this.cipherService,
this.autofillService,
this.authService,
this.environmentService,
this.domainSettingsService,
this.autofillSettingsService,
this.i18nService,
this.platformUtilsService,
this.themeStateService,
);
}
this.tabsBackground = new TabsBackground(
this,
this.notificationBackground,
this.overlayBackground,
);
await this.overlayBackground.init();
await this.tabsBackground.init();
}
}