@instructure/ui-test-utils
Version:
A UI testing library made by Instructure Inc.
419 lines (377 loc) • 12.6 kB
text/typescript
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 - present Instructure, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ReactInstance } from 'react'
import { findDOMNode } from 'react-dom'
import {
elementToString,
wrapQueryResult,
isElement,
matches
} from '@instructure/ui-test-queries'
import { WrappedRef } from '@instructure/ui-test-sandbox/src/utils/reactComponentWrapper'
import { QueriesHelpersEventsType } from '@instructure/ui-test-queries/src/utils/bindElementToUtilities'
type AssertionParams = {
wrapper?: QueriesHelpersEventsType
markup: () => string
flag?: any
arg1?: any
arg2?: any
arg3?: any
sig?: string
inspect?: any //ChaiUtils.inspect
}
type AssertionMethod = (arg: AssertionParams) => void
export default function assertions(
chai: Chai.ChaiStatic,
utils: Chai.ChaiUtils
) {
const { flag, inspect } = utils
const { Assertion } = chai
function wrapObj(obj: ReactInstance | undefined | Element | WrappedRef) {
if (obj && typeof (obj as WrappedRef).getDOMNode === 'function') {
return (obj as unknown) as QueriesHelpersEventsType
}
let node
if (isElement(obj)) {
node = obj as Element
} else if (React.isValidElement(obj)) {
node = findDOMNode(obj as ReactInstance)
}
if (node) {
return wrapQueryResult(node as Element)
}
return undefined
}
function addAssertion(name: string, assertion: AssertionMethod) {
// @ts-expect-error don't know how to type this..
if (Assertion.prototype[name]) {
overwriteMethod(name, assertion)
} else {
addMethod(name, assertion)
}
}
function overwriteProperty(name: string, assertion: AssertionMethod) {
Assertion.overwriteProperty(
name,
function (_super?: (...args: any[]) => any) {
return wrapOverwriteAssertion(assertion, _super!)
}
)
}
function overwriteMethod(name: string, assertion: AssertionMethod) {
Assertion.overwriteMethod(name, function (_super) {
return wrapOverwriteAssertion(assertion, _super)
})
}
function addMethod(name: string, assertion: AssertionMethod) {
Assertion.addMethod(name, wrapAssertion(assertion))
}
function addChainableMethod(name: string, assertion: AssertionMethod) {
Assertion.addChainableMethod(name, wrapAssertion(assertion))
}
function overwriteChainableMethod(name: string, assertion: AssertionMethod) {
Assertion.overwriteChainableMethod(
name,
function (_super) {
return wrapOverwriteAssertion(assertion, _super)
},
function (_super?: () => unknown) {
return function (this: unknown) {
_super!.call(this)
}
}
)
}
function wrapOverwriteAssertion(
assertion: AssertionMethod,
_super: (...args: any[]) => any
) {
return function (this: any, arg1: any, arg2: any) {
const wrapper = wrapObj(flag(this, 'object'))
if (!wrapper) {
// @ts-expect-error TODO: this needs new syntax
// eslint-disable-next-line prefer-rest-params
return _super.apply(this, arguments)
}
assertion.call(this, {
markup: () => wrapper.toString(),
sig: inspect(wrapper.getDOMNode()),
wrapper,
arg1,
arg2,
flag,
inspect
})
}
}
function wrapAssertion(assertion: AssertionMethod) {
return function (this: any, arg1: any, arg2: any) {
const wrapper = wrapObj(flag(this, 'object'))
const config: Partial<AssertionParams> = {
wrapper,
arg1,
flag,
inspect
}
if (wrapper) {
config.markup = () => wrapper.toString()
config.sig = inspect(wrapper.getDOMNode())
}
if (arguments.length > 1) {
config.arg2 = arg2
}
assertion.call(this, config as AssertionParams)
}
}
overwriteProperty('not', function (this: Record<string, any>) {
flag(this, 'negate', true)
})
addChainableMethod(
'exactly',
function exactly(this: unknown, { flag, arg1 }) {
flag(this, 'exactlyCount', arg1)
}
)
addAssertion(
'text',
function text(
this: Chai.AssertionPrototype,
{ wrapper, markup, flag, arg1, arg2, sig }
) {
const actual = wrapper!.text() // TODO check if this is this never null
if (typeof arg1 !== 'undefined') {
if (flag(this, 'contains')) {
this.assert(
actual && actual.indexOf(String(arg1)) > -1,
() =>
`expected ${sig} to contain text #{exp}, but it has #{act} ${markup()}`,
() =>
`expected ${sig} not to contain text #{exp}, but it has #{act} ${markup()}`,
arg1,
actual
)
} else {
this.assert(
actual && matches(actual, arg1, arg2),
() =>
`expected ${sig} to have text #{exp}, but it has #{act} ${markup()}`,
() =>
`expected ${sig} to not have text #{exp}, but it has #{act} ${markup()}`,
arg1,
actual
)
}
}
flag(this, 'object', actual)
}
)
overwriteChainableMethod(
'contain',
function contain(
this: Chai.AssertionPrototype,
{ wrapper, markup, arg1, sig }
) {
if (arg1) {
this.assert(
wrapper && wrapper.contains(arg1),
() =>
`expected ${sig} to contain ${elementToString(arg1)} ${markup()}`,
() =>
`expected ${sig} to not contain ${elementToString(
arg1
)} ${markup()}`,
arg1
)
}
}
)
addAssertion(
'className',
function className(
this: Chai.AssertionPrototype,
{ wrapper, markup, arg1, sig }: AssertionParams
) {
const actual = wrapper!.classNames() // TODO check if this is this never null
this.assert(
wrapper && wrapper.hasClass(arg1),
() =>
`expected ${sig} to have a #{exp} class, but it has #{act} ${markup()}`,
() =>
`expected ${sig} to not have a #{exp} class, but it has #{act} ${markup()}`,
arg1,
actual
)
}
)
addAssertion(
'match',
function match(
this: Chai.AssertionPrototype,
{ wrapper, markup, arg1, sig }: AssertionParams
) {
this.assert(
wrapper && wrapper.matches(arg1),
() => `expected ${sig} to match #{exp} ${markup()}`,
() => `expected ${sig} to not match #{exp} ${markup()}`,
arg1
)
}
)
addAssertion(
'descendants',
listAndCountAssertion('descendants', 'descendants')
)
addAssertion('children', listAndCountAssertion('children', 'children'))
addAssertion('ancestors', listAndCountAssertion('ancestors', 'ancestors'))
addAssertion('parents', listAndCountAssertion('parents', 'parents'))
addAssertion('attribute', propAndValueAssertion('attribute', 'attribute'))
addAssertion('style', propAndValueAssertion('style', 'computed CSS style'))
addAssertion(
'bounds',
propAndValueAssertion('bounds', 'bounding client rect')
)
addAssertion('tagName', valueAssertion('tagName', 'tag name'))
addAssertion('id', valueAssertion('id', 'id'))
addAssertion('visible', booleanAssertion('visible', 'visible'))
addAssertion('clickable', booleanAssertion('clickable', 'clickable'))
addAssertion(
'focus',
booleanAssertion('containsFocus', 'focused or contain the focused element')
)
addAssertion('focused', booleanAssertion('focused', 'focused'))
addAssertion('focusable', booleanAssertion('focusable', 'focusable'))
addAssertion('tabbable', booleanAssertion('tabbable', 'tabbable'))
addAssertion('checked', booleanAssertion('checked', 'checked'))
addAssertion('selected', booleanAssertion('selected', 'selected'))
addAssertion('disabled', booleanAssertion('disabled', 'disabled'))
addAssertion('enabled', booleanAssertion('enabled', 'enabled'))
addAssertion('readonly', booleanAssertion('readonly', 'readonly'))
addAssertion('accessible', booleanAssertion('accessible', 'accessible'))
addAssertion('role', valueAssertion('role', 'role'))
addAssertion('title', valueAssertion('title', 'title'))
addAssertion('value', valueAssertion('value', 'value'))
addAssertion('label', valueAssertion('label', 'label'))
}
function getActual(
wrapper: QueriesHelpersEventsType | undefined,
assertion: string,
...args: unknown[]
) {
const methodOrProperty = wrapper
? (wrapper as Record<string, unknown>)[assertion]
: undefined
if (typeof methodOrProperty === 'function') {
return methodOrProperty(...args)
} else {
return methodOrProperty
}
}
function propAndValueAssertion(assertion: string, desc: string) {
return function (this: Chai.AssertionPrototype, args: AssertionParams) {
const { wrapper, markup, flag, inspect, arg1, arg2, arg3, sig } = args
const actual = getActual(wrapper, assertion, arg1)
if (arg2) {
this.assert(
actual && matches(actual, arg2, arg3),
() =>
`expected ${sig} to have a ${inspect(
arg1
)} ${desc} with the value #{exp}, but the value was #{act} ${markup()}`,
() =>
`expected ${sig} to not have a ${inspect(
arg1
)} ${desc} with the value #{act} ${markup()}`,
arg2,
actual
)
} else {
this.assert(
typeof actual !== 'undefined' && actual !== null,
() => `expected ${sig} to have a #{exp} ${desc} ${markup()}`,
() => `expected ${sig} to not have a #{exp} ${desc} ${markup()}`,
arg1,
actual
)
}
flag(this, 'object', actual)
}
}
function booleanAssertion(assertion: string, desc: string) {
return function (
this: Chai.AssertionPrototype,
{ wrapper, markup, sig }: AssertionParams
) {
const actual = getActual(wrapper, assertion)
this.assert(
actual,
() => `expected ${sig} to be ${desc} ${markup()}`,
() => `expected ${sig} to not be ${desc} ${markup()}`,
undefined
)
}
}
function valueAssertion(assertion: string, desc: string) {
return function (
this: Chai.AssertionPrototype,
{ wrapper, markup, arg1, arg2, sig }: AssertionParams
) {
const actual = getActual(wrapper, assertion)
this.assert(
matches(actual, arg1, arg2),
() =>
`expected ${sig} to have a #{exp} ${desc}, but it has #{act} ${markup()}`,
() =>
`expected ${sig} to not have a #{exp} ${desc}, but it has #{act} ${markup()}`,
arg1,
actual
)
}
}
function listAndCountAssertion(assertion: string, desc: string) {
return function (
this: Chai.AssertionPrototype,
{ wrapper, markup, arg1, sig, flag }: AssertionParams
) {
const exactlyCount = flag(this, 'exactlyCount')
const actual = getActual(wrapper, assertion, arg1)
const count = actual.length
if (exactlyCount || exactlyCount === 0) {
this.assert(
count === exactlyCount,
() =>
`expected ${sig} to have ${exactlyCount} ${desc} #{exp} but actually found ${count} ${markup()}`,
() =>
`expected ${sig} to not have ${exactlyCount} ${desc} #{exp} but actually found ${count} ${markup()}`,
arg1
)
} else {
this.assert(
count > 0,
() => `expected ${sig} to have ${desc} #{exp} ${markup()}`,
() => `expected ${sig} to not have ${desc} #{exp} ${markup()}`,
arg1
)
}
}
}