11import ContentFeature from '../content-feature' ;
22import { isBeingFramed , withExponentialBackoff } from '../utils' ;
3+ import { click } from './broker-protection/actions/click' ;
34
45export const ANIMATION_DURATION_MS = 1000 ;
56export const ANIMATION_ITERATIONS = Infinity ;
@@ -55,6 +56,8 @@ export default class AutofillPasswordImport extends ContentFeature {
5556
5657 #exportId;
5758
59+ #processingBookmark;
60+
5861 #isBookmarkModalVisible = false ;
5962 #isBookmarkProcessed = false ;
6063
@@ -137,6 +140,59 @@ export default class AutofillPasswordImport extends ContentFeature {
137140 return this . #domLoaded;
138141 }
139142
143+ async runWithRetry ( fn , maxAttempts = 4 , delay = 500 ) {
144+ try {
145+ return await withExponentialBackoff ( fn , maxAttempts , delay ) ;
146+ } catch ( error ) {
147+ return null ;
148+ }
149+ }
150+
151+ /**
152+ * Wait for an element's attribute to change using mutation observer
153+ * @param {string } selector - CSS selector for the element
154+ * @param {string } attribute - Attribute to watch
155+ * @param {Function } condition - Function that returns true when condition is met
156+ * @returns {Promise<void> }
157+ */
158+ async waitForAttributeChange ( selector , attribute , condition ) {
159+ return new Promise ( ( resolve , reject ) => {
160+ const element = document . querySelector ( selector ) ;
161+ if ( ! element ) {
162+ reject ( new Error ( `Element with selector "${ selector } " not found` ) ) ;
163+ return ;
164+ }
165+
166+ // Check if condition is already met
167+ if ( condition ( element ) ) {
168+ resolve ( ) ;
169+ return ;
170+ }
171+
172+ const observer = new MutationObserver ( ( mutations ) => {
173+ mutations . forEach ( ( mutation ) => {
174+ if ( mutation . type === 'attributes' && mutation . attributeName === attribute ) {
175+ if ( condition ( element ) ) {
176+ observer . disconnect ( ) ;
177+ resolve ( ) ;
178+ }
179+ }
180+ } ) ;
181+ } ) ;
182+
183+ observer . observe ( element , {
184+ attributes : true ,
185+ attributeFilter : [ attribute ]
186+ } ) ;
187+
188+ // Fallback timeout after 10 seconds
189+ setTimeout ( ( ) => {
190+ observer . disconnect ( ) ;
191+ reject ( new Error ( `Timeout waiting for attribute "${ attribute } " to change` ) ) ;
192+ } , 10000 ) ;
193+ } ) ;
194+ }
195+
140196 /**
141197 * Takes a path and returns the element and style to animate.
142198 * @param {string } path
@@ -429,22 +485,38 @@ export default class AutofillPasswordImport extends ContentFeature {
429485 }
430486
431487 async downloadData ( ) {
432- const userId = document . querySelector ( 'a[href*="&user="]' ) ?. getAttribute ( 'href' ) ?. split ( '&user=' ) [ 1 ] ;
488+ const userId = document . querySelector ( this . userIdSelector ) ?. getAttribute ( 'href' ) ?. split ( '&user=' ) [ 1 ] ;
433489 console . log ( 'DEEP DEBUG autofill-password-import: userId' , userId ) ;
434- await withExponentialBackoff ( ( ) => document . querySelector ( `a[href="./manage/archive/${ this . #exportId} "]` ) , 8 ) ;
490+ await this . runWithRetry ( ( ) => document . querySelector ( `a[href="./manage/archive/${ this . #exportId} "]` ) , 8 ) ;
435491 const downloadURL = `${ TAKEOUT_DOWNLOAD_URL_BASE } ?j=${ this . #exportId} &i=0&user=${ userId } ` ;
436492 window . location . href = downloadURL ;
437493 }
438494
439495 async handleBookmarkImportPath ( pathname ) {
440496 console . log ( 'DEEP DEBUG autofill-password-import: handleBookmarkImportPath' , pathname ) ;
441497 if ( pathname === '/' && ! this . #isBookmarkModalVisible) {
442- await this . clickDisselectAllButton ( ) ;
498+ console . log ( 'DEEP DEBUG autofill-password-import: handleBookmarkImportPath' , this . disselectAllButtonSelector ) ;
499+ click ( {
500+ id : 'disselect-all-click' ,
501+ actionType : 'click' ,
502+ elements : [ {
503+ type : 'element' ,
504+ selector : this . disselectAllButtonSelector
505+ } ]
506+ } , { } , /** @type HTMLElement */ ( this . getRoot ( this . tabPanelSelector ) ) )
507+ await this . scrollToChromeSection ( ) ;
508+ await this . openBookmarkModal ( ) ;
443509 await this . selectBookmark ( ) ;
444- this . startExportProcess ( ) ;
510+ await this . startExportProcess ( ) ;
445511 await this . storeExportId ( ) ;
446- const manageButton = /** @type HTMLAnchorElement */ ( document . querySelector ( 'a[href="manage"]' ) ) ;
447- manageButton ?. click ( ) ;
512+ click ( {
513+ id : 'manage-button-click' ,
514+ actionType : 'click' ,
515+ elements : [ {
516+ type : 'element' ,
517+ selector : this . manageButtonSelector
518+ } ]
519+ } , { } , document )
448520 await this . downloadData ( ) ;
449521 }
450522 }
@@ -456,6 +528,10 @@ export default class AutofillPasswordImport extends ContentFeature {
456528 async handleLocation ( location ) {
457529 const { pathname, hostname } = location ;
458530 if ( hostname === BOOKMARK_IMPORT_DOMAIN ) {
531+ if ( this . #processingBookmark) {
532+ return ;
533+ }
534+ this . #processingBookmark = true ;
459535 this . handleBookmarkImportPath ( pathname ) ;
460536 } else {
461537 await this . handlePasswordManagerPath ( pathname ) ;
@@ -526,39 +602,76 @@ export default class AutofillPasswordImport extends ContentFeature {
526602 return `${ this . #settingsButtonSettings?. selectors ?. join ( ',' ) } , ${ this . settingsLabelTextSelector } ` ;
527603 }
528604
605+ get manageButtonSelector ( ) {
606+ return 'a[href="manage"]' ;
607+ }
608+
609+ get userIdSelector ( ) {
610+ return 'a[href*="&user="]' ;
611+ }
612+
529613 setButtonSettings ( ) {
530614 this . #exportButtonSettings = this . getFeatureSetting ( 'exportButton' ) ;
531615 this . #signInButtonSettings = this . getFeatureSetting ( 'signInButton' ) ;
532616 this . #settingsButtonSettings = this . getFeatureSetting ( 'settingsButton' ) ;
533617 }
534618
535- /** Bookmark import code */
619+ /* ****************************** Bookmark import code ****************************** */
620+
621+ getRoot ( selector ) {
622+ return /** @type HTMLElement */ ( document . querySelector ( selector ) ) ?? document ;
623+ }
624+
536625 get disselectAllButtonSelector ( ) {
537- return 'c-wiz[data-node-index="4;0"] button' ;
626+ return `${ this . tabPanelSelector } div:nth-child(2) div:nth-child(2)` ;
627+ }
628+
629+ get bookmarkModalSelector ( ) {
630+ return 'fieldset.rcetic' ;
538631 }
539632
540633 get bookmarkSelectButtonSelector ( ) {
541- return 'fieldset.rcetic input' ;
634+ return `${ this . bookmarkModalSelector } input` ;
635+ }
636+
637+ get chromeInputCheckboxSelector ( ) {
638+ return `${ this . tabPanelSelector } div:nth-child(10) input[type="checkbox"]` ;
542639 }
543640
544641 get chromeSectionSelector ( ) {
545642 return 'c-wiz [data-id="chrome"]' ;
546643 }
547644
645+ get chromeDataButtonSelector ( ) {
646+ return `${ this . tabPanelSelector } div:nth-child(10) > div:nth-child(2) > div:nth-child(2) button` ;
647+ }
648+
548649 get nextStepButtonSelector ( ) {
549- return ' div[data-jobid] > div:nth-of-type (2) button' ;
650+ return ` ${ this . tabPanelSelector } > div:nth-child(1) > div:nth-child (2) button` ;
550651 }
551652
552653 get createExportButtonSelector ( ) {
553- return 'div[data-configure-step] button' ;
654+ return 'div[data-configure-step="1"] button' ;
655+ }
656+
657+ get tabPanelSelector ( ) {
658+ return 'div[role="tabpanel"]' ;
659+ }
660+
661+ get inputCheckboxSelector ( ) {
662+ return `${ this . tabPanelSelector } input[type="checkbox"]` ;
663+ }
664+
665+ get okButtonSelector ( ) {
666+ return 'div[isfullscreen] div:nth-child(3) div:last-child' ;
554667 }
555668
556- async findDisselectAllButton ( ) {
557- return await withExponentialBackoff ( ( ) => document . querySelectorAll ( this . disselectAllButtonSelector ) [ 1 ] ) ;
669+ get bookmarkCheckboxSelector ( ) {
670+ return ` ${ this . bookmarkModalSelector } div:nth-child(3) > div:nth-of-type(2) input` ;
558671 }
559672
560673 async findExportId ( ) {
561- const panels = document . querySelectorAll ( 'div[role="tabpanel"]' ) ;
674+ const panels = document . querySelectorAll ( this . tabPanelSelector ) ;
562675 const exportPanel = panels [ panels . length - 1 ] ;
563676 return await withExponentialBackoff ( ( ) => exportPanel . querySelector ( 'div[data-archive-id]' ) ?. getAttribute ( 'data-archive-id' ) ) ;
564677 }
@@ -569,72 +682,114 @@ export default class AutofillPasswordImport extends ContentFeature {
569682 }
570683
571684 startExportProcess ( ) {
572- const nextStepButton = /** @type HTMLButtonElement */ ( document . querySelectorAll ( this . nextStepButtonSelector ) [ 0 ] ) ;
573- nextStepButton ?. scrollIntoView ( { behavior : 'smooth' , block : 'center' , inline : 'center' } ) ;
574- nextStepButton ?. click ( ) ;
685+ click ( {
686+ id : 'next-step-button-click' ,
687+ actionType : 'click' ,
688+ elements : [ {
689+ type : 'element' ,
690+ selector : this . nextStepButtonSelector
691+ } ]
692+ } , { } , document )
693+
694+ click ( {
695+ id : 'create-export-button-click' ,
696+ actionType : 'click' ,
697+ elements : [ {
698+ type : 'element' ,
699+ selector : this . createExportButtonSelector
700+ } ]
701+ } , { } , document )
702+ }
703+
704+ async openBookmarkModal ( ) {
705+ if ( this . #isBookmarkProcessed || this . #isBookmarkModalVisible) {
706+ return ;
707+ }
708+
709+ await this . runWithRetry ( ( ) => {
710+ const element = /** @type HTMLButtonElement */ ( document . querySelector ( this . chromeDataButtonSelector ) ) ;
711+ return element ?. checkVisibility ( ) ;
712+ } ) ;
575713
576- const createExportButton = /** @type HTMLButtonElement */ ( document . querySelectorAll ( this . createExportButtonSelector ) [ 0 ] ) ;
577- createExportButton ?. scrollIntoView ( { behavior : 'smooth' , block : 'center' , inline : 'center' } ) ;
578- createExportButton ?. click ( ) ;
714+ click ( {
715+ id : 'select-bookmark-click' ,
716+ actionType : 'click' ,
717+ elements : [ {
718+ type : 'element' ,
719+ selector : this . chromeDataButtonSelector
720+ } ]
721+ } , { } , document ) ;
722+ await this . runWithRetry ( ( ) => document . querySelector ( this . bookmarkModalSelector ) != null )
723+ this . #isBookmarkModalVisible = true ;
579724 }
580725
581726 async selectBookmark ( ) {
582727 if ( this . #isBookmarkProcessed) {
583728 return ;
584729 }
585- const chromeDataButtonSelector = `${ this . chromeSectionSelector } button` ;
586- const chromeDataButton = /** @type HTMLButtonElement */ (
587- await withExponentialBackoff ( ( ) => document . querySelectorAll ( chromeDataButtonSelector ) [ 1 ] , 5 )
588- ) ;
589- chromeDataButton ?. focus ( ) ;
590- chromeDataButton ?. click ( ) ;
591- this . #isBookmarkModalVisible = true ;
592- await this . domLoaded ;
593- const disselectAllButton = /** @type HTMLButtonElement */ (
594- await withExponentialBackoff ( ( ) => document . querySelectorAll ( 'fieldset.rcetic button' ) [ 1 ] )
595- ) ;
596730
597- disselectAllButton ?. click ( ) ;
731+ const disselectSelector = ` ${ this . bookmarkModalSelector } div:nth-child(2) button:nth-of-type(2)` ;
598732
599- const bookmarkSelectButton = /** @type HTMLInputElement */ (
600- await withExponentialBackoff ( ( ) => document . querySelectorAll ( this . bookmarkSelectButtonSelector ) [ 1 ] )
601- ) ;
733+ click ( {
734+ id : 'bookmark-disselect-all-click' ,
735+ actionType : 'click' ,
736+ elements : [ {
737+ type : 'element' ,
738+ selector : disselectSelector
739+ } ]
740+ } , { } , this . getRoot ( this . bookmarkModalSelector ) )
602741
603- await withExponentialBackoff ( ( ) => ! bookmarkSelectButton ?. checked ) ;
742+ await this . runWithRetry ( ( ) => {
743+ const element = /** @type HTMLInputElement */ ( document . querySelector ( this . bookmarkCheckboxSelector ) ) ;
744+ return ! element . checked ;
745+ } ) ;
604746
605- bookmarkSelectButton ?. click ( ) ;
747+ click ( {
748+ id : 'bookmark-checkbox-click' ,
749+ actionType : 'click' ,
750+ elements : [ {
751+ type : 'element' ,
752+ selector : this . bookmarkCheckboxSelector
753+ } ]
754+ } , { } , document )
755+
756+ await this . runWithRetry ( ( ) => {
757+ const element = /** @type HTMLInputElement */ ( document . querySelector ( this . bookmarkCheckboxSelector ) ) ;
758+ return element ?. checked ;
759+ } ) ;
606760
607- const okButton = /** @type HTMLButtonElement */ ( document . querySelectorAll ( 'div[role="button"]' ) [ 7 ] ) ;
761+ await this . runWithRetry ( ( ) => {
762+ const okButton = /** @type HTMLButtonElement */ ( document . querySelector ( this . okButtonSelector ) ) ;
763+ return okButton ?. ariaDisabled !== 'true' ;
764+ } ) ;
608765
609- await withExponentialBackoff ( ( ) => okButton . ariaDisabled !== 'true' ) ;
766+ click ( {
767+ id : 'bookmark-ok-button-click' ,
768+ actionType : 'click' ,
769+ elements : [ {
770+ type : 'element' ,
771+ selector : this . okButtonSelector
772+ } ]
773+ } , { } , document )
610774
611- okButton ?. click ( ) ;
612775 this . #isBookmarkModalVisible = false ;
613776 this . #isBookmarkProcessed = true ;
614777 }
615778
616- async clickDisselectAllButton ( ) {
617- const element = /** @type HTMLButtonElement */ ( await this . findDisselectAllButton ( ) ) ;
618- console . log ( 'Deep element' , element ) ;
619- if ( element != null ) {
620- element . click ( ) ;
621- }
622-
779+ async scrollToChromeSection ( ) {
623780 const chromeSectionElement = /** @type HTMLInputElement */ (
624- await withExponentialBackoff ( ( ) => document . querySelectorAll ( this . chromeSectionSelector ) [ 0 ] . querySelector ( 'input' ) )
781+ await this . runWithRetry ( ( ) => document . querySelectorAll ( this . chromeSectionSelector ) [ 0 ] . querySelector ( 'input' ) )
625782 ) ;
626783 console . log ( 'DEEP chromeSectionElement' , chromeSectionElement ) ;
627784
628- // First wait for the element to become unchecked (due to slow disselection)
629- await withExponentialBackoff ( ( ) => ! chromeSectionElement ?. checked ) ;
630-
631- chromeSectionElement . scrollIntoView ( { behavior : 'smooth' , block : 'center' , inline : 'center' } ) ;
785+ await this . runWithRetry ( ( ) => ! chromeSectionElement . checked ) ;
632786
787+ chromeSectionElement . scrollIntoView ( { behavior : 'instant' , block : 'center' , inline : 'center' } ) ;
633788 chromeSectionElement ?. click ( ) ;
634789 }
635790
636- urlChanged ( ) {
637- console . log ( 'DEEP DEBUG autofill-password-import: urlChanged' , window . location ) ;
791+ urlChanged ( navigationType ) {
792+ console . log ( 'DEEP DEBUG autofill-password-import: urlChanged' , window . location . pathname , navigationType ) ;
638793 this . handleLocation ( window . location ) ;
639794 }
640795
0 commit comments