@@ -4,13 +4,24 @@ import { ThemeProvider } from "contexts/ThemeProvider";
4
4
import {
5
5
type UseClipboardInput ,
6
6
type UseClipboardResult ,
7
+ COPY_FAILED_MESSAGE ,
7
8
useClipboard ,
8
9
} from "./useClipboard" ;
9
10
11
+ // execCommand is the workaround for copying text to the clipboard on HTTP-only
12
+ // connections
13
+ const originalExecCommand = global . document . execCommand ;
14
+ const originalNavigator = window . navigator ;
15
+
16
+ // Need to mock console.error because we deliberately need to trigger errors in
17
+ // the code to assert that it can recover from them, but we also don't want them
18
+ // logged if they're expected
19
+ const originalConsoleError = console . error ;
20
+
10
21
type SetupMockClipboardResult = Readonly < {
11
22
mockClipboard : Clipboard ;
23
+ mockExecCommand : typeof originalExecCommand ;
12
24
getClipboardText : ( ) => string ;
13
- setClipboardText : ( newText : string ) => void ;
14
25
setSimulateFailure : ( shouldFail : boolean ) => void ;
15
26
} > ;
16
27
@@ -60,12 +71,31 @@ function setupMockClipboard(isSecure: boolean): SetupMockClipboardResult {
60
71
return {
61
72
mockClipboard,
62
73
getClipboardText : ( ) => mockClipboardText ,
63
- setClipboardText : ( newText ) => {
64
- mockClipboardText = newText ;
65
- } ,
66
74
setSimulateFailure : ( newShouldFailValue ) => {
67
75
shouldSimulateFailure = newShouldFailValue ;
68
76
} ,
77
+ mockExecCommand : ( commandId ) => {
78
+ if ( commandId !== "copy" ) {
79
+ return false ;
80
+ }
81
+
82
+ if ( shouldSimulateFailure ) {
83
+ throw new Error ( "Failed to execute command 'copy'" ) ;
84
+ }
85
+
86
+ const dummyInput = document . querySelector ( "input[data-testid=dummy]" ) ;
87
+ const inputIsFocused =
88
+ dummyInput instanceof HTMLInputElement &&
89
+ document . activeElement === dummyInput ;
90
+
91
+ let copySuccessful = false ;
92
+ if ( inputIsFocused ) {
93
+ mockClipboardText = dummyInput . value ;
94
+ copySuccessful = true ;
95
+ }
96
+
97
+ return copySuccessful ;
98
+ } ,
69
99
} ;
70
100
}
71
101
@@ -86,73 +116,75 @@ function renderUseClipboard<TInput extends UseClipboardInput>(inputs: TInput) {
86
116
}
87
117
88
118
const secureContextValues : readonly boolean [ ] = [ true , false ] ;
89
- const originalNavigator = window . navigator ;
90
- const originalExecCommand = global . document . execCommand ;
91
119
92
120
// Not a big fan of describe.each most of the time, but since we need to test
93
121
// the exact same test cases against different inputs, and we want them to run
94
122
// as sequentially as possible to minimize flakes, they make sense here
95
- describe . each ( secureContextValues ) ( "useClipboard - secure: %j" , ( context ) => {
123
+ describe . each ( secureContextValues ) ( "useClipboard - secure: %j" , ( isSecure ) => {
96
124
const {
97
125
mockClipboard,
126
+ mockExecCommand,
98
127
getClipboardText,
99
- setClipboardText,
100
128
setSimulateFailure,
101
- } = setupMockClipboard ( context ) ;
129
+ } = setupMockClipboard ( isSecure ) ;
102
130
103
131
beforeEach ( ( ) => {
104
132
jest . useFakeTimers ( ) ;
133
+ global . document . execCommand = mockExecCommand ;
105
134
jest . spyOn ( window , "navigator" , "get" ) . mockImplementation ( ( ) => ( {
106
135
...originalNavigator ,
107
136
clipboard : mockClipboard ,
108
137
} ) ) ;
109
138
110
- global . document . execCommand = jest . fn ( ( ) => {
111
- const dummyInput = document . querySelector ( "input[data-testid=dummy]" ) ;
112
- const inputIsFocused =
113
- dummyInput instanceof HTMLInputElement &&
114
- document . activeElement === dummyInput ;
139
+ console . error = ( errorValue , ...rest ) => {
140
+ const canIgnore =
141
+ errorValue instanceof Error &&
142
+ errorValue . message === COPY_FAILED_MESSAGE ;
115
143
116
- let copySuccessful = false ;
117
- if ( inputIsFocused ) {
118
- setClipboardText ( dummyInput . value ) ;
119
- copySuccessful = true ;
144
+ if ( ! canIgnore ) {
145
+ originalConsoleError ( errorValue , ...rest ) ;
120
146
}
121
-
122
- return copySuccessful ;
123
- } ) ;
147
+ } ;
124
148
} ) ;
125
149
126
150
afterEach ( ( ) => {
127
151
jest . runAllTimers ( ) ;
128
152
jest . useRealTimers ( ) ;
129
153
jest . resetAllMocks ( ) ;
154
+
155
+ console . error = originalConsoleError ;
130
156
global . document . execCommand = originalExecCommand ;
131
157
} ) ;
132
158
133
- const assertClipboardTextUpdate = async (
159
+ const assertClipboardUpdateLifecycle = async (
134
160
result : ReturnType < typeof renderUseClipboard > [ "result" ] ,
135
161
textToCheck : string ,
136
162
) : Promise < void > => {
137
163
await act ( ( ) => result . current . copyToClipboard ( ) ) ;
138
164
expect ( result . current . showCopiedSuccess ) . toBe ( true ) ;
139
165
166
+ // Because of timing trickery, any timeouts for flipping the copy status
167
+ // back to false will trigger before the test can complete. This will never
168
+ // be an issue in the real world, but it will kick up 'act' warnings in the
169
+ // console, which makes tests more annoying. Just wait for them to finish up
170
+ // to avoid anything from being logged, but note that the value of
171
+ // showCopiedSuccess will become false after this
172
+ await act ( ( ) => jest . runAllTimersAsync ( ) ) ;
173
+
140
174
const clipboardText = getClipboardText ( ) ;
141
175
expect ( clipboardText ) . toEqual ( textToCheck ) ;
142
176
} ;
143
177
144
178
it ( "Copies the current text to the user's clipboard" , async ( ) => {
145
179
const textToCopy = "dogs" ;
146
180
const { result } = renderUseClipboard ( { textToCopy } ) ;
147
- await assertClipboardTextUpdate ( result , textToCopy ) ;
181
+ await assertClipboardUpdateLifecycle ( result , textToCopy ) ;
148
182
} ) ;
149
183
150
184
it ( "Should indicate to components not to show successful copy after a set period of time" , async ( ) => {
151
185
const textToCopy = "cats" ;
152
186
const { result } = renderUseClipboard ( { textToCopy } ) ;
153
- await assertClipboardTextUpdate ( result , textToCopy ) ;
154
-
155
- await jest . runAllTimersAsync ( ) ;
187
+ await assertClipboardUpdateLifecycle ( result , textToCopy ) ;
156
188
expect ( result . current . showCopiedSuccess ) . toBe ( false ) ;
157
189
} ) ;
158
190
0 commit comments