11import React from 'react' ;
2- import * as ReactRedux from 'react-redux' ;
3- import { mount , ReactWrapper } from 'enzyme' ;
4- import PopupBase from '../PopupBase' ;
2+ import { render , screen } from '@testing-library/react' ;
3+ import { useSelector } from 'react-redux' ;
54import PopupReply , { Props } from '../PopupReply' ;
6- import ReplyForm from '../../ReplyForm' ;
75
86jest . mock ( 'react-redux' , ( ) => ( {
97 useSelector : jest . fn ( ) ,
108} ) ) ;
119
12- jest . mock ( '../PopupBase' ) ;
13- jest . mock ( '../../ReplyForm' ) ;
10+ const mockUseSelector = useSelector as jest . MockedFunction < typeof useSelector > ;
11+
12+ jest . mock ( '../PopupBase' , ( ) => {
13+ const ReactMock = jest . requireActual ( 'react' ) ;
14+ const MockPopupBase = ReactMock . forwardRef (
15+ ( { children, options, ...rest } : Record < string , unknown > , ref : React . Ref < HTMLDivElement > ) =>
16+ ReactMock . createElement (
17+ 'div' ,
18+ { ref, 'data-testid' : 'popup-base' , 'data-options' : JSON . stringify ( options ) , ...rest } ,
19+ children ,
20+ ) ,
21+ ) ;
22+ MockPopupBase . displayName = 'MockPopupBase' ;
23+ return MockPopupBase ;
24+ } ) ;
25+
26+ jest . mock ( '../../ReplyForm' , ( ) => {
27+ const ReactMock = jest . requireActual ( 'react' ) ;
28+ return ( { isPending, value, ...rest } : Record < string , unknown > ) =>
29+ ReactMock . createElement ( 'div' , {
30+ 'data-testid' : 'reply-form' ,
31+ 'data-pending' : String ( isPending ) ,
32+ 'data-value' : value || '' ,
33+ ...rest ,
34+ } ) ;
35+ } ) ;
36+
37+ jest . mock ( '../PopupReplyV2' , ( ) => {
38+ const ReactMock = jest . requireActual ( 'react' ) ;
39+ return ( ) => ReactMock . createElement ( 'div' , { 'data-testid' : 'popup-reply-v2' } ) ;
40+ } ) ;
1441
1542describe ( 'PopupReply' , ( ) => {
1643 const defaults : Props = {
@@ -21,93 +48,116 @@ describe('PopupReply', () => {
2148 reference : document . createElement ( 'div' ) ,
2249 } ;
2350
24- const getWrapper = ( props = { } ) : ReactWrapper => mount ( < PopupReply { ...defaults } { ...props } /> ) ;
25-
2651 beforeEach ( ( ) => {
27- jest . spyOn ( React , 'useEffect' ) . mockImplementation ( f => f ( ) ) ;
52+ mockUseSelector . mockReturnValue ( 0 ) ;
2853 } ) ;
2954
30- describe ( 'state changes' , ( ) => {
31- test ( 'should call update on the underlying popper instance when the store changes' , ( ) => {
32- const popupMock = { popper : { update : jest . fn ( ) } } ;
33- const reduxSpy = jest . spyOn ( ReactRedux , 'useSelector' ) . mockImplementation ( ( ) => false ) ;
34- const refSpy = jest . spyOn ( React , 'useRef' ) . mockImplementation ( ( ) => ( { current : popupMock } ) ) ;
35- const wrapper = getWrapper ( ) ;
55+ afterEach ( ( ) => {
56+ jest . clearAllMocks ( ) ;
57+ } ) ;
3658
37- reduxSpy . mockReturnValueOnce ( true ) ;
38- wrapper . setProps ( { value : '1' } ) ;
59+ describe ( 'render()' , ( ) => {
60+ test ( 'should render the ReplyForm' , ( ) => {
61+ render ( < PopupReply { ...defaults } /> ) ;
62+
63+ expect ( screen . getByTestId ( 'popup-base' ) ) . toBeDefined ( ) ;
64+ expect ( screen . getByTestId ( 'reply-form' ) ) . toBeDefined ( ) ;
65+ } ) ;
3966
40- reduxSpy . mockReturnValueOnce ( false ) ;
41- wrapper . setProps ( { value : '2' } ) ;
67+ test ( 'should render PopupReplyV2 when isThreadedAnnotation is true' , ( ) => {
68+ render ( < PopupReply { ... defaults } isThreadedAnnotation /> ) ;
4269
43- expect ( refSpy ) . toHaveBeenCalled ( ) ;
44- expect ( popupMock . popper . update ) . toHaveBeenCalledTimes ( 3 ) ;
70+ expect ( screen . getByTestId ( 'popup-reply-v2' ) ) . toBeDefined ( ) ;
71+ expect ( screen . queryByTestId ( 'popup-base' ) ) . toBeNull ( ) ;
72+ expect ( screen . queryByTestId ( 'reply-form' ) ) . toBeNull ( ) ;
4573 } ) ;
46- } ) ;
4774
48- describe ( 'render()' , ( ) => {
49- test ( 'should render the ReplyForm' , ( ) => {
50- const wrapper = getWrapper ( ) ;
75+ test ( 'should render existing UI when isThreadedAnnotation is false' , ( ) => {
76+ render ( < PopupReply { ...defaults } isThreadedAnnotation = { false } /> ) ;
77+
78+ expect ( screen . queryByTestId ( 'popup-reply-v2' ) ) . toBeNull ( ) ;
79+ expect ( screen . getByTestId ( 'popup-base' ) ) . toBeDefined ( ) ;
80+ expect ( screen . getByTestId ( 'reply-form' ) ) . toBeDefined ( ) ;
81+ } ) ;
5182
52- expect ( wrapper . exists ( PopupBase ) ) . toBe ( true ) ;
53- expect ( wrapper . exists ( ReplyForm ) ) . toBe ( true ) ;
83+ test ( 'should render existing UI when isThreadedAnnotation is undefined' , ( ) => {
84+ render ( < PopupReply { ...defaults } /> ) ;
85+
86+ expect ( screen . queryByTestId ( 'popup-reply-v2' ) ) . toBeNull ( ) ;
87+ expect ( screen . getByTestId ( 'popup-base' ) ) . toBeDefined ( ) ;
88+ expect ( screen . getByTestId ( 'reply-form' ) ) . toBeDefined ( ) ;
5489 } ) ;
5590
56- test ( 'should maintain referential integrity of the PopupBase options object across renders' , ( ) => {
57- const wrapper = getWrapper ( ) ;
91+ test ( 'should pass value prop to ReplyForm' , ( ) => {
92+ render ( < PopupReply { ...defaults } value = "test value" /> ) ;
93+
94+ const replyForm = screen . getByTestId ( 'reply-form' ) ;
95+ expect ( replyForm . getAttribute ( 'data-value' ) ) . toBe ( 'test value' ) ;
96+ } ) ;
5897
59- const popupOptions = wrapper . find ( PopupBase ) . prop ( 'options' ) ;
98+ test ( 'should pass isPending prop to ReplyForm' , ( ) => {
99+ render ( < PopupReply { ...defaults } isPending /> ) ;
60100
61- expect ( wrapper . exists ( PopupBase ) ) . toBe ( true ) ;
62- expect ( wrapper . find ( ReplyForm ) . prop ( 'value' ) ) . toBe ( '' ) ;
101+ const replyForm = screen . getByTestId ( 'reply-form' ) ;
102+ expect ( replyForm . getAttribute ( 'data-pending' ) ) . toBe ( 'true' ) ;
103+ } ) ;
104+ } ) ;
63105
64- wrapper . setProps ( { value : '1' } ) ;
106+ describe ( 'useEffect popper update' , ( ) => {
107+ test ( 'should call useSelector for rotation and scale values' , ( ) => {
108+ mockUseSelector . mockReturnValueOnce ( 0 ) . mockReturnValueOnce ( 1 ) ;
65109
66- expect ( wrapper . find ( PopupBase ) . prop ( 'options' ) ) . toStrictEqual ( popupOptions ) ;
67- expect ( wrapper . find ( ReplyForm ) . prop ( 'value' ) ) . toBe ( '1' ) ;
110+ render ( < PopupReply { ...defaults } /> ) ;
111+
112+ expect ( mockUseSelector ) . toHaveBeenCalledTimes ( 2 ) ;
113+ } ) ;
114+
115+ test ( 'should re-render when rotation/scale changes' , ( ) => {
116+ mockUseSelector . mockReturnValue ( 0 ) ;
117+
118+ const { rerender } = render ( < PopupReply { ...defaults } /> ) ;
119+ const callCountAfterFirstRender = mockUseSelector . mock . calls . length ;
120+
121+ mockUseSelector . mockReturnValue ( 90 ) ;
122+ rerender ( < PopupReply { ...defaults } /> ) ;
123+
124+ expect ( mockUseSelector . mock . calls . length ) . toBeGreaterThan ( callCountAfterFirstRender ) ;
68125 } ) ;
69126 } ) ;
70127
71128 describe ( 'Popup options' , ( ) => {
72- const eventListenersModifier = {
73- name : 'eventListeners' ,
74- options : { scroll : false } ,
75- } ;
76- const IEUserAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko' ;
77- const otherUserAgent = 'Other' ;
78-
79- test . each `
80- userAgent | expectedPlacement
81- ${ IEUserAgent } | ${ 'top' }
82- ${ otherUserAgent } | ${ 'bottom' }
83- ` (
84- 'should set placement option as $expectedPlacement based on userAgent=$userAgent' ,
85- ( { userAgent, expectedPlacement } ) => {
86- global . window . navigator . userAgent = userAgent ;
87-
88- const wrapper = getWrapper ( ) ;
89-
90- expect ( window . navigator . userAgent ) . toEqual ( userAgent ) ;
91- expect ( wrapper . find ( PopupBase ) . prop ( 'options' ) . placement ) . toBe ( expectedPlacement ) ;
92- } ,
93- ) ;
94-
95- test ( 'should disable scroll event listeners if browser is IE' , ( ) => {
96- global . window . navigator . userAgent = IEUserAgent ;
97-
98- const wrapper = getWrapper ( ) ;
99-
100- expect ( window . navigator . userAgent ) . toEqual ( IEUserAgent ) ;
101- expect ( wrapper . find ( PopupBase ) . prop ( 'options' ) . modifiers ) . toContainEqual ( eventListenersModifier ) ;
129+ test ( 'should pass options to PopupBase' , ( ) => {
130+ render ( < PopupReply { ...defaults } /> ) ;
131+
132+ const popupBase = screen . getByTestId ( 'popup-base' ) ;
133+ const options = JSON . parse ( popupBase . getAttribute ( 'data-options' ) || '{}' ) ;
134+
135+ expect ( options ) . toHaveProperty ( 'placement' ) ;
136+ expect ( options ) . toHaveProperty ( 'modifiers' ) ;
137+ expect ( Array . isArray ( options . modifiers ) ) . toBe ( true ) ;
138+ } ) ;
139+
140+ test ( 'should maintain the same options reference between renders' , ( ) => {
141+ const { rerender } = render ( < PopupReply { ...defaults } /> ) ;
142+ const optionsBefore = screen . getByTestId ( 'popup-base' ) . getAttribute ( 'data-options' ) ;
143+
144+ rerender ( < PopupReply { ...defaults } /> ) ;
145+ const optionsAfter = screen . getByTestId ( 'popup-base' ) . getAttribute ( 'data-options' ) ;
146+
147+ expect ( optionsBefore ) . toBe ( optionsAfter ) ;
102148 } ) ;
103149
104- test ( 'should not disable scroll event listeners for non IE browsers ' , ( ) => {
105- global . window . navigator . userAgent = otherUserAgent ;
150+ test ( 'should include required modifiers in options ' , ( ) => {
151+ render ( < PopupReply { ... defaults } /> ) ;
106152
107- const wrapper = getWrapper ( ) ;
153+ const popupBase = screen . getByTestId ( 'popup-base' ) ;
154+ const options = JSON . parse ( popupBase . getAttribute ( 'data-options' ) || '{}' ) ;
108155
109- expect ( window . navigator . userAgent ) . toEqual ( otherUserAgent ) ;
110- expect ( wrapper . find ( PopupBase ) . prop ( 'options' ) . modifiers ) . not . toContainEqual ( eventListenersModifier ) ;
156+ const modifierNames = options . modifiers . map ( ( m : { name : string } ) => m . name ) ;
157+ expect ( modifierNames ) . toContain ( 'arrow' ) ;
158+ expect ( modifierNames ) . toContain ( 'flip' ) ;
159+ expect ( modifierNames ) . toContain ( 'offset' ) ;
160+ expect ( modifierNames ) . toContain ( 'preventOverflow' ) ;
111161 } ) ;
112162 } ) ;
113163} ) ;
0 commit comments