@@ -488,6 +488,107 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
488488 } ) ;
489489} ) ;
490490
491+ /**
492+ * WebKit can report a datetime that is still on screen as not intersecting,
493+ * so the datetime must not tear down its ready state on that alone.
494+ */
495+ configs ( { modes : [ 'md' ] , directions : [ 'ltr' ] } ) . forEach ( ( { title, config } ) => {
496+ test . describe ( title ( 'datetime: spurious hidden report' ) , ( ) => {
497+ test ( 'should stay ready when the observer reports it hidden while it is on screen' , async ( { page } , testInfo ) => {
498+ testInfo . annotations . push ( {
499+ type : 'issue' ,
500+ description : 'https://github.com/ionic-team/ionic-framework/issues/30933' ,
501+ } ) ;
502+
503+ await page . addInitScript ( ( ) => {
504+ const OriginalIO = window . IntersectionObserver ;
505+ const datetimeObservers : {
506+ callback : IntersectionObserverCallback ;
507+ targets : Element [ ] ;
508+ sawVisible : boolean ;
509+ } [ ] = [ ] ;
510+ let reportedHidden = false ;
511+
512+ /**
513+ * The datetime only tears down once its observers have reported it
514+ * visible, so the test waits for that first.
515+ */
516+ ( window as any ) . datetimeObserversSawVisible = ( ) =>
517+ datetimeObservers . length > 0 && datetimeObservers . every ( ( { sawVisible } ) => sawVisible ) ;
518+
519+ /**
520+ * Reports the datetime as not intersecting and then goes quiet,
521+ * since WebKit never sends a recovery entry to undo it.
522+ */
523+ ( window as any ) . reportHiddenToDatetimeObservers = ( ) => {
524+ reportedHidden = true ;
525+ datetimeObservers . forEach ( ( { callback, targets } ) => {
526+ targets . forEach ( ( target ) => {
527+ callback ( [ { isIntersecting : false , target } as IntersectionObserverEntry ] , null as any ) ;
528+ } ) ;
529+ } ) ;
530+ } ;
531+
532+ ( window as any ) . IntersectionObserver = function (
533+ callback : IntersectionObserverCallback ,
534+ options ?: IntersectionObserverInit
535+ ) {
536+ const root = options ?. root as Element | null ;
537+
538+ if ( root ?. tagName !== 'ION-DATETIME' ) {
539+ return new OriginalIO ( callback , options ) ;
540+ }
541+
542+ const record = { callback, targets : [ ] as Element [ ] , sawVisible : false } ;
543+ datetimeObservers . push ( record ) ;
544+
545+ const instance = new OriginalIO ( ( entries , observer ) => {
546+ if ( reportedHidden ) {
547+ return ;
548+ }
549+
550+ if ( entries . some ( ( entry ) => entry . isIntersecting ) ) {
551+ record . sawVisible = true ;
552+ }
553+
554+ callback ( entries , observer ) ;
555+ } , options ) ;
556+
557+ const originalObserve = instance . observe . bind ( instance ) ;
558+ instance . observe = ( target : Element ) => {
559+ record . targets . push ( target ) ;
560+ originalObserve ( target ) ;
561+ } ;
562+
563+ return instance ;
564+ } as any ;
565+ } ) ;
566+
567+ await page . setContent ( `<ion-datetime value="2022-05-03"></ion-datetime>` , config ) ;
568+
569+ const datetime = page . locator ( 'ion-datetime' ) ;
570+ const calendarBody = datetime . locator ( '.calendar-body' ) ;
571+
572+ await expect ( datetime ) . toHaveClass ( / d a t e t i m e - r e a d y / ) ;
573+ await expect ( calendarBody ) . toHaveCSS ( 'opacity' , '1' ) ;
574+ await page . waitForFunction ( ( ) => ( window as any ) . datetimeObserversSawVisible ( ) ) ;
575+
576+ /**
577+ * A one-shot layout fallback runs 100ms after the datetime loads and
578+ * would add the class back. Real reports arrive long after that, so
579+ * wait it out.
580+ */
581+ await page . waitForTimeout ( 300 ) ;
582+
583+ await page . evaluate ( ( ) => ( window as any ) . reportHiddenToDatetimeObservers ( ) ) ;
584+ await page . waitForChanges ( ) ;
585+
586+ await expect ( datetime ) . toHaveClass ( / d a t e t i m e - r e a d y / ) ;
587+ await expect ( calendarBody ) . toHaveCSS ( 'opacity' , '1' ) ;
588+ } ) ;
589+ } ) ;
590+ } ) ;
591+
491592/**
492593 * We are setting RTL on the component
493594 * instead, so we don't need to test
0 commit comments