1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | import React, { ReactInstance } from 'react'
|
26 | import { findDOMNode } from 'react-dom'
|
27 |
|
28 | import {
|
29 | elementToString,
|
30 | wrapQueryResult,
|
31 | isElement,
|
32 | matches
|
33 | } from '@instructure/ui-test-queries'
|
34 | import { WrappedRef } from '@instructure/ui-test-sandbox/src/utils/reactComponentWrapper'
|
35 | import { QueriesHelpersEventsType } from '@instructure/ui-test-queries/src/utils/bindElementToUtilities'
|
36 |
|
37 | type 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
|
46 | }
|
47 | type AssertionMethod = (arg: AssertionParams) => void
|
48 |
|
49 | export 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 |
|
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 |
|
130 |
|
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()
|
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()
|
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 |
|
309 | function 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 |
|
324 | function 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 |
|
357 | function 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 |
|
372 | function 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 |
|
391 | function 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 | }
|