33import React from 'react' ;
44import { act , render , waitFor } from '@testing-library/react' ;
55
6- import { isThemeActive } from '@cloudscape-design/component-toolkit/internal' ;
7-
86import CopyToClipboard from '../../../lib/components/copy-to-clipboard' ;
97import InternalCopyToClipboard from '../../../lib/components/copy-to-clipboard/internal' ;
108import createWrapper from '../../../lib/components/test-utils/dom' ;
11- import { KeyCode } from '../../internal/keycode' ;
129
1310import styles from '../../../lib/components/copy-to-clipboard/styles.css.js' ;
14- import iconStyles from '../../../lib/components/icon/styles.css.js' ;
15-
16- jest . mock ( '@cloudscape-design/component-toolkit/internal' , ( ) => ( {
17- ...jest . requireActual ( '@cloudscape-design/component-toolkit/internal' ) ,
18- isThemeActive : jest . fn ( ) . mockReturnValue ( false ) ,
19- } ) ) ;
2011
2112const defaultProps = {
2213 copyTarget : 'Test content' ,
@@ -31,7 +22,6 @@ describe('CopyToClipboard', () => {
3122 const originalNavigatorPermissions = global . navigator . permissions ;
3223
3324 beforeEach ( ( ) => {
34- ( isThemeActive as jest . Mock ) . mockReturnValue ( false ) ;
3525 Object . assign ( global . navigator , {
3626 clipboard : {
3727 writeText : ( text : string ) =>
@@ -144,20 +134,6 @@ describe('CopyToClipboard', () => {
144134 expect ( mockedWriteText ) . toHaveBeenCalledWith ( 'Text to copy' ) ;
145135 } ) ;
146136
147- test ( 'does not schedule the One Theme status icon timer outside One Theme' , async ( ) => {
148- jest . useFakeTimers ( ) ;
149- const { container } = render ( < CopyToClipboard { ...defaultProps } /> ) ;
150- const wrapper = createWrapper ( container ) . findCopyToClipboard ( ) ! ;
151-
152- await act ( async ( ) => {
153- wrapper . findCopyButton ( ) . click ( ) ;
154- await Promise . resolve ( ) ;
155- } ) ;
156-
157- expect ( jest . getTimerCount ( ) ) . toBe ( 0 ) ;
158- jest . useRealTimers ( ) ;
159- } ) ;
160-
161137 describe . each ( [ false , true ] ) ( 'popoverRenderWithPortal set to %s' , ( popoverRenderWithPortal : boolean ) => {
162138 test ( 'copies to clipboard and shows success message' , async ( ) => {
163139 const { container } = render (
@@ -554,168 +530,4 @@ describe('CopyToClipboard', () => {
554530 const { container } = render ( < InternalCopyToClipboard { ...defaultProps } variant = "inline" wrapText = { false } /> ) ;
555531 expect ( container . querySelector ( `.${ styles [ 'inline-container-no-wrap' ] } ` ) ) . not . toBeNull ( ) ;
556532 } ) ;
557-
558- describe ( 'One Theme glyph-swap behavior' , ( ) => {
559- beforeEach ( ( ) => {
560- ( isThemeActive as jest . Mock ) . mockReturnValue ( true ) ;
561- jest . useFakeTimers ( ) ;
562- } ) ;
563-
564- afterEach ( ( ) => {
565- jest . useRealTimers ( ) ;
566- } ) ;
567-
568- function renderCopyToClipboard ( props : Partial < React . ComponentProps < typeof CopyToClipboard > > = { } ) {
569- const { container, unmount } = render ( < CopyToClipboard { ...defaultProps } { ...props } /> ) ;
570- const wrapper = createWrapper ( container ) . findCopyToClipboard ( ) ! ;
571- const hasClass = ( className : string ) => container . querySelector ( `.${ className } ` ) !== null ;
572- const copy = ( ) =>
573- act ( async ( ) => {
574- wrapper . findCopyButton ( ) . click ( ) ;
575- await Promise . resolve ( ) ;
576- } ) ;
577- return { container, unmount, wrapper, hasClass, copy } ;
578- }
579-
580- test ( 'swaps the copy glyph for the success glyph and shows the message as plain text' , async ( ) => {
581- const { wrapper, hasClass, copy } = renderCopyToClipboard ( ) ;
582- // Both glyphs are mounted up front so the swap can transition; the result is hidden until a copy.
583- expect ( hasClass ( styles [ 'icon-swap' ] ) ) . toBe ( true ) ;
584- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( false ) ;
585-
586- await copy ( ) ;
587-
588- expect ( wrapper . findStatusText ( ) ! . getElement ( ) . textContent ) . toBe ( 'Copied to clipboard' ) ;
589- expect ( wrapper . findStatusText ( ) ! . findByClassName ( styles [ 'popover-message' ] ) ) . not . toBeNull ( ) ;
590- expect ( hasClass ( iconStyles [ 'variant-success' ] ) ) . toBe ( true ) ;
591- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
592- } ) ;
593-
594- test ( 'swaps the copy glyph for the error glyph when the copy fails' , async ( ) => {
595- const { wrapper, hasClass, copy } = renderCopyToClipboard ( { textToCopy : 'Text to copy with error' } ) ;
596-
597- await copy ( ) ;
598-
599- expect ( wrapper . findStatusText ( ) ! . getElement ( ) . textContent ) . toBe ( 'Failed to copy to clipboard' ) ;
600- expect ( hasClass ( iconStyles [ 'variant-error' ] ) ) . toBe ( true ) ;
601- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
602- } ) ;
603-
604- test ( 'hands back to the copy glyph after the status icon duration and keeps the popover open' , async ( ) => {
605- const { wrapper, hasClass, copy } = renderCopyToClipboard ( ) ;
606- await copy ( ) ;
607- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
608- expect ( wrapper . findStatusText ( ) ) . not . toBeNull ( ) ;
609-
610- act ( ( ) => jest . advanceTimersByTime ( 1499 ) ) ;
611- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
612-
613- act ( ( ) => jest . advanceTimersByTime ( 1 ) ) ;
614- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( false ) ;
615- expect ( wrapper . findStatusText ( ) ) . not . toBeNull ( ) ;
616- } ) ;
617-
618- test ( 'restarts the status icon timer without replaying the swap on rapid re-click' , async ( ) => {
619- const { hasClass, copy } = renderCopyToClipboard ( ) ;
620- await copy ( ) ;
621- act ( ( ) => jest . advanceTimersByTime ( 1000 ) ) ;
622- await copy ( ) ;
623-
624- act ( ( ) => jest . advanceTimersByTime ( 1000 ) ) ;
625- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
626-
627- act ( ( ) => jest . advanceTimersByTime ( 500 ) ) ;
628- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( false ) ;
629- } ) ;
630-
631- test ( 'does not render the stacked glyphs outside One Theme' , ( ) => {
632- ( isThemeActive as jest . Mock ) . mockReturnValue ( false ) ;
633- const { wrapper, hasClass } = renderCopyToClipboard ( ) ;
634- expect ( hasClass ( styles [ 'icon-swap' ] ) ) . toBe ( false ) ;
635- expect ( wrapper . findCopyButton ( ) . getElement ( ) . querySelector ( 'svg' ) ) . not . toBeNull ( ) ;
636- } ) ;
637-
638- test . each ( [ 'button' , 'icon' , 'inline' ] as const ) ( 'swaps the glyph for variant="%s"' , async variant => {
639- const { hasClass, copy } = renderCopyToClipboard ( { variant } ) ;
640- expect ( hasClass ( styles [ 'icon-swap' ] ) ) . toBe ( true ) ;
641- await copy ( ) ;
642- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
643- } ) ;
644-
645- test ( 'does not swap the glyph or show a popover when disabled' , async ( ) => {
646- const { container, hasClass, copy } = renderCopyToClipboard ( { disabled : true } ) ;
647- await copy ( ) ;
648- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( false ) ;
649- expect ( createWrapper ( container ) . findPopover ( ) ) . toBeNull ( ) ;
650- } ) ;
651-
652- test ( 'keeps the copy glyph when permission is denied on mount and recovers on a successful copy' , async ( ) => {
653- // The permission query resolves through a promise chain; real timers let `waitFor` observe it.
654- jest . useRealTimers ( ) ;
655- Object . assign ( global . navigator , {
656- permissions : { query : jest . fn ( ) . mockResolvedValue ( { state : 'denied' } ) } ,
657- } ) ;
658- const { hasClass, copy } = renderCopyToClipboard ( ) ;
659-
660- // The denial only seeds the status; the trigger does not announce anything before a click.
661- await waitFor ( ( ) => expect ( hasClass ( iconStyles [ 'variant-error' ] ) ) . toBe ( true ) ) ;
662- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( false ) ;
663-
664- await copy ( ) ;
665-
666- await waitFor ( ( ) => expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ) ;
667- expect ( hasClass ( iconStyles [ 'variant-success' ] ) ) . toBe ( true ) ;
668- expect ( hasClass ( iconStyles [ 'variant-error' ] ) ) . toBe ( false ) ;
669- } ) ;
670-
671- test ( 'swaps to the error glyph when the clipboard API is unavailable' , async ( ) => {
672- Object . assign ( global . navigator , { clipboard : undefined } ) ;
673- const onCopyFailure = jest . fn ( ) ;
674- const { wrapper, hasClass, copy } = renderCopyToClipboard ( { onCopyFailure } ) ;
675- await copy ( ) ;
676-
677- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
678- expect ( hasClass ( iconStyles [ 'variant-error' ] ) ) . toBe ( true ) ;
679- expect ( wrapper . findStatusText ( ) ! . getElement ( ) . textContent ) . toBe ( 'Failed to copy to clipboard' ) ;
680- expect ( onCopyFailure ) . toHaveBeenCalledWith ( expect . objectContaining ( { detail : { text : 'Text to copy' } } ) ) ;
681- } ) ;
682-
683- test ( 'keeps the button accessible name and hides the glyphs from assistive technology' , async ( ) => {
684- const { wrapper, copy } = renderCopyToClipboard ( { copyButtonAriaLabel : 'Copy ARN' } ) ;
685- const button = wrapper . findCopyButton ( ) . getElement ( ) ;
686- expect ( button ) . toHaveAccessibleName ( 'Copy ARN' ) ;
687-
688- await copy ( ) ;
689-
690- expect ( button ) . toHaveAccessibleName ( 'Copy ARN' ) ;
691- expect ( button ) . not . toHaveAttribute ( 'aria-pressed' ) ;
692- const stack = button . querySelector ( `.${ styles [ 'icon-swap' ] } ` ) ! ;
693- expect ( stack . closest ( '[aria-hidden="true"]' ) ) . not . toBeNull ( ) ;
694- } ) ;
695-
696- test ( 'cancels the status icon timer on unmount' , async ( ) => {
697- const { unmount, copy } = renderCopyToClipboard ( ) ;
698- await copy ( ) ;
699- const pendingTimersWhileShown = jest . getTimerCount ( ) ;
700-
701- unmount ( ) ;
702-
703- expect ( jest . getTimerCount ( ) ) . toBeLessThan ( pendingTimersWhileShown ) ;
704- expect ( ( ) => act ( ( ) => jest . advanceTimersByTime ( 1500 ) ) ) . not . toThrow ( ) ;
705- } ) ;
706-
707- test ( 'closes the popover with Escape while the status icon finishes its own hold' , async ( ) => {
708- const { wrapper, hasClass, copy } = renderCopyToClipboard ( ) ;
709- await copy ( ) ;
710- expect ( wrapper . findStatusText ( ) ) . not . toBeNull ( ) ;
711-
712- act ( ( ) => wrapper . findCopyButton ( ) . keydown ( KeyCode . escape ) ) ;
713- expect ( wrapper . findStatusText ( ) ) . toBeNull ( ) ;
714- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( true ) ;
715-
716- act ( ( ) => jest . advanceTimersByTime ( 1500 ) ) ;
717- expect ( hasClass ( styles [ 'icon-swap-copied' ] ) ) . toBe ( false ) ;
718- expect ( wrapper . findStatusText ( ) ) . toBeNull ( ) ;
719- } ) ;
720- } ) ;
721533} ) ;
0 commit comments