1
1
import { type UseClipboardResult , useClipboard } from "./useClipboard" ;
2
2
import { act , renderHook } from "@testing-library/react" ;
3
- import userEvent from "@testing-library/user-event" ;
3
+
4
+ /*
5
+ Normally, you could call userEvent.setup to enable clipboard mocking, but
6
+ userEvent doesn't expose a teardown function. It also modifies the global
7
+ clipboard, so enabling just one userEvent session will make a mock clipboard
8
+ exist for all other tests, even though you didn't tell them to set up a
9
+ session. The mock also assumes that the clipboard API will always be
10
+ available, which is not true on HTTP-only connections
11
+
12
+ Since these tests need to split hairs and differentiate between HTTP and HTTPS
13
+ connections, setting up a single userEvent is disastrous. It will make all the
14
+ tests pass, even if they shouldn't. Have to avoid that by creating a custom
15
+ clipboard mock.
16
+ */
17
+ type MockClipboard = Readonly <
18
+ Clipboard & {
19
+ resetText : ( ) => void ;
20
+ setIsSecureContext : ( newContext : boolean ) => void ;
21
+ }
22
+ > ;
23
+
24
+ function makeMockClipboard ( ) : MockClipboard {
25
+ let mockClipboardValue = "" ;
26
+ let isSecureContext = true ;
27
+
28
+ return {
29
+ readText : async ( ) => {
30
+ if ( ! isSecureContext ) {
31
+ throw new Error (
32
+ "Trying to read from clipboard outside secure context!" ,
33
+ ) ;
34
+ }
35
+
36
+ return mockClipboardValue ;
37
+ } ,
38
+ writeText : async ( newText ) => {
39
+ if ( ! isSecureContext ) {
40
+ throw new Error ( "Trying to write to clipboard outside secure context!" ) ;
41
+ }
42
+
43
+ mockClipboardValue = newText ;
44
+ } ,
45
+ resetText : ( ) => {
46
+ mockClipboardValue = "" ;
47
+ } ,
48
+ setIsSecureContext : ( newContext ) => {
49
+ isSecureContext = newContext ;
50
+ } ,
51
+
52
+ addEventListener : jest . fn ( ) ,
53
+ removeEventListener : jest . fn ( ) ,
54
+ dispatchEvent : jest . fn ( ) ,
55
+ read : jest . fn ( ) ,
56
+ write : jest . fn ( ) ,
57
+ } ;
58
+ }
59
+
60
+ const mockClipboard = makeMockClipboard ( ) ;
4
61
5
62
beforeAll ( ( ) => {
63
+ const originalNavigator = window . navigator ;
64
+ jest . spyOn ( window , "navigator" , "get" ) . mockImplementation ( ( ) => ( {
65
+ ...originalNavigator ,
66
+ clipboard : mockClipboard ,
67
+ } ) ) ;
68
+
69
+ jest . spyOn ( document , "hasFocus" ) . mockImplementation ( ( ) => true ) ;
6
70
jest . useFakeTimers ( ) ;
7
- userEvent . setup ( {
8
- writeToClipboard : true ,
9
- } ) ;
71
+ } ) ;
72
+
73
+ afterEach ( ( ) => {
74
+ mockClipboard . resetText ( ) ;
10
75
} ) ;
11
76
12
77
afterAll ( ( ) => {
13
- jest . useRealTimers ( ) ;
14
78
jest . restoreAllMocks ( ) ;
79
+ jest . useRealTimers ( ) ;
15
80
} ) ;
16
81
17
82
function renderUseClipboard ( textToCopy : string ) {
18
83
type Props = Readonly < { textToCopy : string } > ;
19
-
20
84
return renderHook < UseClipboardResult , Props > (
21
85
( { textToCopy } ) => useClipboard ( textToCopy ) ,
22
86
{ initialProps : { textToCopy } } ,
@@ -25,33 +89,20 @@ function renderUseClipboard(textToCopy: string) {
25
89
26
90
type UseClipboardTestResult = ReturnType < typeof renderUseClipboard > [ "result" ] ;
27
91
28
- // This can and should be cleaned up - trying to call the clipboard's readText
29
- // method caused an error around blob input, even though the method takes no
30
- // arguments whatsoever, so here's this workaround using the lower-level API
31
92
async function assertClipboardTextUpdate (
32
93
result : UseClipboardTestResult ,
33
94
textToCheck : string ,
34
95
) : Promise < void > {
35
96
await act ( ( ) => result . current . copyToClipboard ( ) ) ;
36
97
expect ( result . current . showCopiedSuccess ) . toBe ( true ) ;
37
98
38
- const clipboardTextType = "text/plain" ;
39
- const clipboardItems = await window . navigator . clipboard . read ( ) ;
40
- const firstItem = clipboardItems [ 0 ] ;
41
-
42
- const hasData =
43
- firstItem !== undefined && firstItem . types . includes ( clipboardTextType ) ;
44
-
45
- if ( ! hasData ) {
46
- throw new Error ( "No clipboard items to process" ) ;
47
- }
48
-
49
- const blob = await firstItem . getType ( clipboardTextType ) ;
50
- const clipboardText = await blob . text ( ) ;
99
+ const clipboardText = await window . navigator . clipboard . readText ( ) ;
51
100
expect ( textToCheck ) . toEqual ( clipboardText ) ;
52
101
}
53
102
54
- describe ( useClipboard . name , ( ) => {
103
+ function scheduleTests ( isHttps : boolean ) {
104
+ mockClipboard . setIsSecureContext ( isHttps ) ;
105
+
55
106
it ( "Copies the current text to the user's clipboard" , async ( ) => {
56
107
const hookText = "dogs" ;
57
108
const { result } = renderUseClipboard ( hookText ) ;
@@ -69,27 +120,14 @@ describe(useClipboard.name, () => {
69
120
70
121
await jest . runAllTimersAsync ( ) ;
71
122
} ) ;
123
+ }
72
124
73
- it . skip ( "Should notify the user that a copy was not successful" , ( ) => {
74
- expect . hasAssertions ( ) ;
125
+ describe ( useClipboard . name , ( ) => {
126
+ describe ( "HTTP (non-secure) connections" , ( ) => {
127
+ scheduleTests ( false ) ;
75
128
} ) ;
76
129
77
- it . skip ( "Should work on non-secure (HTTP-only) connections" , async ( ) => {
78
- const prevClipboard = window . navigator . clipboard ;
79
-
80
- Object . assign ( window . navigator , {
81
- clipboard : {
82
- ...prevClipboard ,
83
- writeText : async ( ) => {
84
- throw new Error (
85
- "Trying to call clipboard API in non-secure context!" ,
86
- ) ;
87
- } ,
88
- } ,
89
- } ) ;
90
-
91
- const hookText = "birds" ;
92
- const { result } = renderUseClipboard ( hookText ) ;
93
- await assertClipboardTextUpdate ( result , hookText ) ;
130
+ describe ( "HTTPS connections" , ( ) => {
131
+ scheduleTests ( true ) ;
94
132
} ) ;
95
133
} ) ;
0 commit comments