/* * 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 = { 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) { 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)[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 ) } } }