UNPKG

12.6 kBPlain TextView Raw
1/*
2 * The MIT License (MIT)
3 *
4 * Copyright (c) 2015 - present Instructure, Inc.
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25import React, { ReactInstance } from 'react'
26import { findDOMNode } from 'react-dom'
27
28import {
29 elementToString,
30 wrapQueryResult,
31 isElement,
32 matches
33} from '@instructure/ui-test-queries'
34import { WrappedRef } from '@instructure/ui-test-sandbox/src/utils/reactComponentWrapper'
35import { QueriesHelpersEventsType } from '@instructure/ui-test-queries/src/utils/bindElementToUtilities'
36
37type AssertionParams = {
38 wrapper?: QueriesHelpersEventsType
39 markup: () => string
40 flag?: any
41 arg1?: any
42 arg2?: any
43 arg3?: any
44 sig?: string
45 inspect?: any //ChaiUtils.inspect
46}
47type AssertionMethod = (arg: AssertionParams) => void
48
49export default function assertions(
50 chai: Chai.ChaiStatic,
51 utils: Chai.ChaiUtils
52) {
53 const { flag, inspect } = utils
54 const { Assertion } = chai
55
56 function wrapObj(obj: ReactInstance | undefined | Element | WrappedRef) {
57 if (obj && typeof (obj as WrappedRef).getDOMNode === 'function') {
58 return (obj as unknown) as QueriesHelpersEventsType
59 }
60
61 let node
62
63 if (isElement(obj)) {
64 node = obj as Element
65 } else if (React.isValidElement(obj)) {
66 node = findDOMNode(obj as ReactInstance)
67 }
68
69 if (node) {
70 return wrapQueryResult(node as Element)
71 }
72 return undefined
73 }
74
75 function addAssertion(name: string, assertion: AssertionMethod) {
76 // @ts-expect-error don't know how to type this..
77 if (Assertion.prototype[name]) {
78 overwriteMethod(name, assertion)
79 } else {
80 addMethod(name, assertion)
81 }
82 }
83
84 function overwriteProperty(name: string, assertion: AssertionMethod) {
85 Assertion.overwriteProperty(
86 name,
87 function (_super?: (...args: any[]) => any) {
88 return wrapOverwriteAssertion(assertion, _super!)
89 }
90 )
91 }
92
93 function overwriteMethod(name: string, assertion: AssertionMethod) {
94 Assertion.overwriteMethod(name, function (_super) {
95 return wrapOverwriteAssertion(assertion, _super)
96 })
97 }
98
99 function addMethod(name: string, assertion: AssertionMethod) {
100 Assertion.addMethod(name, wrapAssertion(assertion))
101 }
102
103 function addChainableMethod(name: string, assertion: AssertionMethod) {
104 Assertion.addChainableMethod(name, wrapAssertion(assertion))
105 }
106
107 function overwriteChainableMethod(name: string, assertion: AssertionMethod) {
108 Assertion.overwriteChainableMethod(
109 name,
110 function (_super) {
111 return wrapOverwriteAssertion(assertion, _super)
112 },
113 function (_super?: () => unknown) {
114 return function (this: unknown) {
115 _super!.call(this)
116 }
117 }
118 )
119 }
120
121 function wrapOverwriteAssertion(
122 assertion: AssertionMethod,
123 _super: (...args: any[]) => any
124 ) {
125 return function (this: any, arg1: any, arg2: any) {
126 const wrapper = wrapObj(flag(this, 'object'))
127
128 if (!wrapper) {
129 // @ts-expect-error TODO: this needs new syntax
130 // eslint-disable-next-line prefer-rest-params
131 return _super.apply(this, arguments)
132 }
133
134 assertion.call(this, {
135 markup: () => wrapper.toString(),
136 sig: inspect(wrapper.getDOMNode()),
137 wrapper,
138 arg1,
139 arg2,
140 flag,
141 inspect
142 })
143 }
144 }
145
146 function wrapAssertion(assertion: AssertionMethod) {
147 return function (this: any, arg1: any, arg2: any) {
148 const wrapper = wrapObj(flag(this, 'object'))
149
150 const config: Partial<AssertionParams> = {
151 wrapper,
152 arg1,
153 flag,
154 inspect
155 }
156
157 if (wrapper) {
158 config.markup = () => wrapper.toString()
159 config.sig = inspect(wrapper.getDOMNode())
160 }
161
162 if (arguments.length > 1) {
163 config.arg2 = arg2
164 }
165
166 assertion.call(this, config as AssertionParams)
167 }
168 }
169
170 overwriteProperty('not', function (this: Record<string, any>) {
171 flag(this, 'negate', true)
172 })
173
174 addChainableMethod(
175 'exactly',
176 function exactly(this: unknown, { flag, arg1 }) {
177 flag(this, 'exactlyCount', arg1)
178 }
179 )
180
181 addAssertion(
182 'text',
183 function text(
184 this: Chai.AssertionPrototype,
185 { wrapper, markup, flag, arg1, arg2, sig }
186 ) {
187 const actual = wrapper!.text() // TODO check if this is this never null
188
189 if (typeof arg1 !== 'undefined') {
190 if (flag(this, 'contains')) {
191 this.assert(
192 actual && actual.indexOf(String(arg1)) > -1,
193 () =>
194 `expected ${sig} to contain text #{exp}, but it has #{act} ${markup()}`,
195 () =>
196 `expected ${sig} not to contain text #{exp}, but it has #{act} ${markup()}`,
197 arg1,
198 actual
199 )
200 } else {
201 this.assert(
202 actual && matches(actual, arg1, arg2),
203 () =>
204 `expected ${sig} to have text #{exp}, but it has #{act} ${markup()}`,
205 () =>
206 `expected ${sig} to not have text #{exp}, but it has #{act} ${markup()}`,
207 arg1,
208 actual
209 )
210 }
211 }
212
213 flag(this, 'object', actual)
214 }
215 )
216
217 overwriteChainableMethod(
218 'contain',
219 function contain(
220 this: Chai.AssertionPrototype,
221 { wrapper, markup, arg1, sig }
222 ) {
223 if (arg1) {
224 this.assert(
225 wrapper && wrapper.contains(arg1),
226 () =>
227 `expected ${sig} to contain ${elementToString(arg1)} ${markup()}`,
228 () =>
229 `expected ${sig} to not contain ${elementToString(
230 arg1
231 )} ${markup()}`,
232 arg1
233 )
234 }
235 }
236 )
237
238 addAssertion(
239 'className',
240 function className(
241 this: Chai.AssertionPrototype,
242 { wrapper, markup, arg1, sig }: AssertionParams
243 ) {
244 const actual = wrapper!.classNames() // TODO check if this is this never null
245
246 this.assert(
247 wrapper && wrapper.hasClass(arg1),
248 () =>
249 `expected ${sig} to have a #{exp} class, but it has #{act} ${markup()}`,
250 () =>
251 `expected ${sig} to not have a #{exp} class, but it has #{act} ${markup()}`,
252 arg1,
253 actual
254 )
255 }
256 )
257
258 addAssertion(
259 'match',
260 function match(
261 this: Chai.AssertionPrototype,
262 { wrapper, markup, arg1, sig }: AssertionParams
263 ) {
264 this.assert(
265 wrapper && wrapper.matches(arg1),
266 () => `expected ${sig} to match #{exp} ${markup()}`,
267 () => `expected ${sig} to not match #{exp} ${markup()}`,
268 arg1
269 )
270 }
271 )
272
273 addAssertion(
274 'descendants',
275 listAndCountAssertion('descendants', 'descendants')
276 )
277 addAssertion('children', listAndCountAssertion('children', 'children'))
278 addAssertion('ancestors', listAndCountAssertion('ancestors', 'ancestors'))
279 addAssertion('parents', listAndCountAssertion('parents', 'parents'))
280 addAssertion('attribute', propAndValueAssertion('attribute', 'attribute'))
281 addAssertion('style', propAndValueAssertion('style', 'computed CSS style'))
282 addAssertion(
283 'bounds',
284 propAndValueAssertion('bounds', 'bounding client rect')
285 )
286 addAssertion('tagName', valueAssertion('tagName', 'tag name'))
287 addAssertion('id', valueAssertion('id', 'id'))
288 addAssertion('visible', booleanAssertion('visible', 'visible'))
289 addAssertion('clickable', booleanAssertion('clickable', 'clickable'))
290 addAssertion(
291 'focus',
292 booleanAssertion('containsFocus', 'focused or contain the focused element')
293 )
294 addAssertion('focused', booleanAssertion('focused', 'focused'))
295 addAssertion('focusable', booleanAssertion('focusable', 'focusable'))
296 addAssertion('tabbable', booleanAssertion('tabbable', 'tabbable'))
297 addAssertion('checked', booleanAssertion('checked', 'checked'))
298 addAssertion('selected', booleanAssertion('selected', 'selected'))
299 addAssertion('disabled', booleanAssertion('disabled', 'disabled'))
300 addAssertion('enabled', booleanAssertion('enabled', 'enabled'))
301 addAssertion('readonly', booleanAssertion('readonly', 'readonly'))
302 addAssertion('accessible', booleanAssertion('accessible', 'accessible'))
303 addAssertion('role', valueAssertion('role', 'role'))
304 addAssertion('title', valueAssertion('title', 'title'))
305 addAssertion('value', valueAssertion('value', 'value'))
306 addAssertion('label', valueAssertion('label', 'label'))
307}
308
309function getActual(
310 wrapper: QueriesHelpersEventsType | undefined,
311 assertion: string,
312 ...args: unknown[]
313) {
314 const methodOrProperty = wrapper
315 ? (wrapper as Record<string, unknown>)[assertion]
316 : undefined
317 if (typeof methodOrProperty === 'function') {
318 return methodOrProperty(...args)
319 } else {
320 return methodOrProperty
321 }
322}
323
324function propAndValueAssertion(assertion: string, desc: string) {
325 return function (this: Chai.AssertionPrototype, args: AssertionParams) {
326 const { wrapper, markup, flag, inspect, arg1, arg2, arg3, sig } = args
327 const actual = getActual(wrapper, assertion, arg1)
328
329 if (arg2) {
330 this.assert(
331 actual && matches(actual, arg2, arg3),
332 () =>
333 `expected ${sig} to have a ${inspect(
334 arg1
335 )} ${desc} with the value #{exp}, but the value was #{act} ${markup()}`,
336 () =>
337 `expected ${sig} to not have a ${inspect(
338 arg1
339 )} ${desc} with the value #{act} ${markup()}`,
340 arg2,
341 actual
342 )
343 } else {
344 this.assert(
345 typeof actual !== 'undefined' && actual !== null,
346 () => `expected ${sig} to have a #{exp} ${desc} ${markup()}`,
347 () => `expected ${sig} to not have a #{exp} ${desc} ${markup()}`,
348 arg1,
349 actual
350 )
351 }
352
353 flag(this, 'object', actual)
354 }
355}
356
357function booleanAssertion(assertion: string, desc: string) {
358 return function (
359 this: Chai.AssertionPrototype,
360 { wrapper, markup, sig }: AssertionParams
361 ) {
362 const actual = getActual(wrapper, assertion)
363 this.assert(
364 actual,
365 () => `expected ${sig} to be ${desc} ${markup()}`,
366 () => `expected ${sig} to not be ${desc} ${markup()}`,
367 undefined
368 )
369 }
370}
371
372function valueAssertion(assertion: string, desc: string) {
373 return function (
374 this: Chai.AssertionPrototype,
375 { wrapper, markup, arg1, arg2, sig }: AssertionParams
376 ) {
377 const actual = getActual(wrapper, assertion)
378
379 this.assert(
380 matches(actual, arg1, arg2),
381 () =>
382 `expected ${sig} to have a #{exp} ${desc}, but it has #{act} ${markup()}`,
383 () =>
384 `expected ${sig} to not have a #{exp} ${desc}, but it has #{act} ${markup()}`,
385 arg1,
386 actual
387 )
388 }
389}
390
391function listAndCountAssertion(assertion: string, desc: string) {
392 return function (
393 this: Chai.AssertionPrototype,
394 { wrapper, markup, arg1, sig, flag }: AssertionParams
395 ) {
396 const exactlyCount = flag(this, 'exactlyCount')
397 const actual = getActual(wrapper, assertion, arg1)
398 const count = actual.length
399
400 if (exactlyCount || exactlyCount === 0) {
401 this.assert(
402 count === exactlyCount,
403 () =>
404 `expected ${sig} to have ${exactlyCount} ${desc} #{exp} but actually found ${count} ${markup()}`,
405 () =>
406 `expected ${sig} to not have ${exactlyCount} ${desc} #{exp} but actually found ${count} ${markup()}`,
407 arg1
408 )
409 } else {
410 this.assert(
411 count > 0,
412 () => `expected ${sig} to have ${desc} #{exp} ${markup()}`,
413 () => `expected ${sig} to not have ${desc} #{exp} ${markup()}`,
414 arg1
415 )
416 }
417 }
418}