UNPKG

qunit-dom

Version:

High Level DOM Assertions for QUnit

1,444 lines (1,428 loc) 62.8 kB
import { createDescriptor as createDescriptor$1, isDescriptor, resolveDOMElement, resolveDOMElements, resolveDescription } from 'dom-element-descriptors'; function exists(options, message) { let expectedCount = null; if (typeof options === 'string') { message = options; } else if (options) { expectedCount = options.count; } let elements = this.findElements(); if (expectedCount === null) { let result = elements.length > 0; let expected = format$1(this.targetDescription); let actual = result ? expected : format$1(this.targetDescription, 0); if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else if (typeof expectedCount === 'number') { let result = elements.length === expectedCount; let actual = format$1(this.targetDescription, elements.length); let expected = format$1(this.targetDescription, expectedCount); if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else { throw new TypeError(`Unexpected Parameter: ${expectedCount}`); } } function format$1(selector, num) { if (selector === '<unknown>') { selector = '<not found>'; } if (num === undefined || num === null) { return `Element ${selector} exists`; } else if (num === 0) { return `Element ${selector} does not exist`; } else if (num === 1) { return `Element ${selector} exists once`; } else if (num === 2) { return `Element ${selector} exists twice`; } else { return `Element ${selector} exists ${num} times`; } } // imported from https://github.com/nathanboktae/chai-dom function elementToString(el) { if (!el) return '<not found>'; let desc; if (el instanceof NodeList) { if (el.length === 0) { return 'empty NodeList'; } desc = Array.prototype.slice.call(el, 0, 5).map(elementToString).join(', '); return el.length > 5 ? `${desc}... (+${el.length - 5} more)` : desc; } if (!(el instanceof HTMLElement || el instanceof SVGElement)) { return String(el); } desc = el.tagName.toLowerCase(); if (el.id) { desc += `#${el.id}`; } if (el.className && !(el.className instanceof SVGAnimatedString)) { desc += `.${String(el.className).replace(/\s+/g, '.')}`; } Array.prototype.forEach.call(el.attributes, function (attr) { if (attr.name !== 'class' && attr.name !== 'id') { desc += `[${attr.name}${attr.value ? `="${attr.value}"]` : ']'}`; } }); return desc; } function focused(message) { let element = this.findTargetElement(); if (!element) return; let result = document.activeElement === element; let actual = elementToString(document.activeElement); let expected = this.targetDescription; if (!message) { message = `Element ${expected} is focused`; } this.pushResult({ result, actual, expected, message }); } function notFocused(message) { let element = this.findTargetElement(); if (!element) return; let result = document.activeElement !== element; let expected = `Element ${this.targetDescription} is not focused`; let actual = result ? expected : `Element ${this.targetDescription} is focused`; if (!message) { message = expected; } this.pushResult({ result, message, actual, expected }); } function checked(message) { let element = this.findTargetElement(); if (!element) return; let isChecked = element.checked === true; let isNotChecked = element.checked === false; let result = isChecked; let hasCheckedProp = isChecked || isNotChecked; if (!hasCheckedProp) { let ariaChecked = element.getAttribute('aria-checked'); if (ariaChecked !== null) { result = ariaChecked === 'true'; } } let actual = result ? 'checked' : 'not checked'; let expected = 'checked'; if (!message) { message = `Element ${this.targetDescription} is checked`; } this.pushResult({ result, actual, expected, message }); } function notChecked(message) { let element = this.findTargetElement(); if (!element) return; let isChecked = element.checked === true; let isNotChecked = element.checked === false; let result = !isChecked; let hasCheckedProp = isChecked || isNotChecked; if (!hasCheckedProp) { let ariaChecked = element.getAttribute('aria-checked'); if (ariaChecked !== null) { result = ariaChecked !== 'true'; } } let actual = result ? 'not checked' : 'checked'; let expected = 'not checked'; if (!message) { message = `Element ${this.targetDescription} is not checked`; } this.pushResult({ result, actual, expected, message }); } function required(message) { let element = this.findTargetElement(); if (!element) return; if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) { throw new TypeError(`Unexpected Element Type: ${element.toString()}`); } let result = element.required === true; let actual = result ? 'required' : 'not required'; let expected = 'required'; if (!message) { message = `Element ${this.targetDescription} is required`; } this.pushResult({ result, actual, expected, message }); } function notRequired(message) { let element = this.findTargetElement(); if (!element) return; if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) { throw new TypeError(`Unexpected Element Type: ${element.toString()}`); } let result = element.required === false; let actual = !result ? 'required' : 'not required'; let expected = 'not required'; if (!message) { message = `Element ${this.targetDescription} is not required`; } this.pushResult({ result, actual, expected, message }); } function isValid(message, options = {}) { let element = this.findTargetElement(); if (!element) return; if (!(element instanceof HTMLFormElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLButtonElement || element instanceof HTMLOutputElement || element instanceof HTMLSelectElement)) { throw new TypeError(`Unexpected Element Type: ${element.toString()}`); } let validity = element.reportValidity() === true; let result = validity === !options.inverted; let actual = validity ? 'valid' : 'not valid'; let expected = options.inverted ? 'not valid' : 'valid'; if (!message) { message = `Element ${this.targetDescription} is ${actual}`; } this.pushResult({ result, actual, expected, message }); } // Visible logic based on jQuery's // https://github.com/jquery/jquery/blob/4a2bcc27f9c3ee24b3effac0fbe1285d1ee23cc5/src/css/hiddenVisibleSelectors.js#L11-L13 function visible(el) { if (el === null) return false; if (el.offsetWidth === 0 || el.offsetHeight === 0) return false; let clientRects = el.getClientRects(); if (clientRects.length === 0) return false; for (let i = 0; i < clientRects.length; i++) { let rect = clientRects[i]; if (rect.width !== 0 && rect.height !== 0) return true; } return false; } function isVisible(options, message) { let expectedCount = null; if (typeof options === 'string') { message = options; } else if (options) { expectedCount = options.count; } let elements = this.findElements().filter(visible); if (expectedCount === null) { let result = elements.length > 0; let expected = format(this.targetDescription); let actual = result ? expected : format(this.targetDescription, 0); if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else if (typeof expectedCount === 'number') { let result = elements.length === expectedCount; let actual = format(this.targetDescription, elements.length); let expected = format(this.targetDescription, expectedCount); if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else { throw new TypeError(`Unexpected Parameter: ${expectedCount}`); } } function format(selector, num) { if (selector === '<unknown>') { selector = '<not found>'; } if (num === undefined || num === null) { return `Element ${selector} is visible`; } else if (num === 0) { return `Element ${selector} is not visible`; } else if (num === 1) { return `Element ${selector} is visible once`; } else if (num === 2) { return `Element ${selector} is visible twice`; } else { return `Element ${selector} is visible ${num} times`; } } function isDisabled(message, options = {}) { let { inverted } = options; let element = this.findTargetElement(); if (!element) return; if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element instanceof HTMLButtonElement || element instanceof HTMLOptGroupElement || element instanceof HTMLOptionElement || element instanceof HTMLFieldSetElement)) { throw new TypeError(`Unexpected Element Type: ${element.toString()}`); } let result = element.disabled === !inverted; let actual = element.disabled === false ? `Element ${this.targetDescription} is not disabled` : `Element ${this.targetDescription} is disabled`; let expected = inverted ? `Element ${this.targetDescription} is not disabled` : `Element ${this.targetDescription} is disabled`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } function matchesSelector(elements, compareSelector) { let failures = elements.filter(it => !it.matches(compareSelector)); return failures.length; } function collapseWhitespace(string) { return string .replace(/[\t\r\n]/g, ' ') .replace(/ +/g, ' ') .replace(/^ /, '') .replace(/ $/, ''); } /** * @ignore * Descriptor data for creating an {@link IDOMElementDescriptor} from a CSS * selector */ class SelectorData { selector; rootElement; constructor(selector, rootElement) { this.selector = selector; this.rootElement = rootElement; } get element() { return this.rootElement.querySelector(this.selector); } get elements() { return Array.from(this.rootElement.querySelectorAll(this.selector)); } get description() { return this.selector; } } /** * @ignore * Descriptor data for creating an {@link IDOMElementDescriptor} from an * {@link Element} */ class ElementData { element; constructor(element) { this.element = element; } get description() { return elementToString(this.element); } } /** * @ignore * Create an {@link IDOMElementDescriptor} from a target and a root element */ function createDescriptor(target, rootElement) { if (typeof target === 'string') { // selector if (!rootElement) { throw new Error('Cannot do selector-based queries without a root element'); } return createDescriptor$1(new SelectorData(target, rootElement)); } else if (target instanceof Element) { // element return createDescriptor$1(new ElementData(target)); } else if (target === null) { // null, which we treat as an unmatched element, e.g. // `createDescriptor(document.querySelector('.does-not-exist'))` return createDescriptor$1({ element: null, description: '<unknown>' }); } else if (isDescriptor(target)) { // already a descriptor return target; } else { throw new TypeError(`Unexpected Parameter: ${target}`); } } /** * @namespace */ class DOMAssertions { testContext; /** * @ignore * The target of our assertions */ descriptor; /** * @ignore * Whether we were constructed with an element, rather than a selector or * descriptor. Used to make error messages more helpful. */ wasPassedElement; /** * @hideconstructor */ constructor(target, rootElement, testContext) { this.testContext = testContext; this.descriptor = createDescriptor(target, rootElement); this.wasPassedElement = target instanceof Element; } /** * Assert an {@link HTMLElement} (or multiple) matching the `selector` exists. * * @param {object?} options * @param {number?} options.count * @param {string?} message * * @example * assert.dom('#title').exists(); * assert.dom('.choice').exists({ count: 4 }); * * @see {@link #doesNotExist} */ exists(...options) { exists.call(this, ...options); return this; } /** * Assert an {@link HTMLElement} matching the `selector` does not exists. * * @param {string?} message * * @example * assert.dom('.should-not-exist').doesNotExist(); * * @see {@link #exists} */ doesNotExist(message) { exists.call(this, { count: 0 }, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is currently checked. * * Note: This also supports `aria-checked="true/false"`. * * @param {string?} message * * @example * assert.dom('input.active').isChecked(); * * @see {@link #isNotChecked} */ isChecked(message) { checked.call(this, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is currently unchecked. * * Note: This also supports `aria-checked="true/false"`. * * @param {string?} message * * @example * assert.dom('input.active').isNotChecked(); * * @see {@link #isChecked} */ isNotChecked(message) { notChecked.call(this, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is currently focused. * * @param {string?} message * * @example * assert.dom('input.email').isFocused(); * * @see {@link #isNotFocused} */ isFocused(message) { focused.call(this, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is not currently focused. * * @param {string?} message * * @example * assert.dom('input[type="password"]').isNotFocused(); * * @see {@link #isFocused} */ isNotFocused(message) { notFocused.call(this, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is currently required. * * @param {string?} message * * @example * assert.dom('input[type="text"]').isRequired(); * * @see {@link #isNotRequired} */ isRequired(message) { required.call(this, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is currently not required. * * @param {string?} message * * @example * assert.dom('input[type="text"]').isNotRequired(); * * @see {@link #isRequired} */ isNotRequired(message) { notRequired.call(this, message); return this; } /** * Assert that the {@link HTMLElement} passes validation * * Validity is determined by asserting that: * * - `element.reportValidity() === true` * * @param {string?} message * * @example * assert.dom('.input').isValid(); * * @see {@link #isValid} */ isValid(message) { isValid.call(this, message); return this; } /** * Assert that the {@link HTMLElement} does not pass validation * * Validity is determined by asserting that: * * - `element.reportValidity() === true` * * @param {string?} message * * @example * assert.dom('.input').isNotValid(); * * @see {@link #isValid} */ isNotValid(message) { isValid.call(this, message, { inverted: true }); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` exists and is visible. * * Visibility is determined by asserting that: * * - the element's offsetWidth and offsetHeight are non-zero * - any of the element's DOMRect objects have a non-zero size * * Additionally, visibility in this case means that the element is visible on the page, * but not necessarily in the viewport. * * @param {object?} options * @param {number?} options.count * @param {string?} message * * @example * assert.dom('#title').isVisible(); * assert.dom('.choice').isVisible({ count: 4 }); * * @see {@link #isNotVisible} */ isVisible(...options) { isVisible.call(this, ...options); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` does not exist or is not visible on the page. * * Visibility is determined by asserting that: * * - the element's offsetWidth or offsetHeight are zero * - all of the element's DOMRect objects have a size of zero * * Additionally, visibility in this case means that the element is visible on the page, * but not necessarily in the viewport. * * @param {string?} message * * @example * assert.dom('.foo').isNotVisible(); * * @see {@link #isVisible} */ isNotVisible(message) { isVisible.call(this, { count: 0 }, message); return this; } /** * Assert that the {@link HTMLElement} has an attribute with the provided `name` * and optionally checks if the attribute `value` matches the provided text * or regular expression. * * @param {string} name * @param {string|RegExp|object?} value * @param {string?} message * * @example * assert.dom('input.password-input').hasAttribute('type', 'password'); * * @see {@link #doesNotHaveAttribute} */ hasAttribute(name, value, message) { let element = this.findTargetElement(); if (!element) return this; if (arguments.length === 1) { value = { any: true }; } let actualValue = element.getAttribute(name); if (value instanceof RegExp) { let result = typeof actualValue === 'string' && value.test(actualValue); let expected = `Element ${this.targetDescription} has attribute "${name}" with value matching ${value}`; let actual = actualValue === null ? `Element ${this.targetDescription} does not have attribute "${name}"` : `Element ${this.targetDescription} has attribute "${name}" with value ${JSON.stringify(actualValue)}`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else if (value.any === true) { let result = actualValue !== null; let expected = `Element ${this.targetDescription} has attribute "${name}"`; let actual = result ? expected : `Element ${this.targetDescription} does not have attribute "${name}"`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else { let result = value === actualValue; let expected = `Element ${this.targetDescription} has attribute "${name}" with value ${JSON.stringify(value)}`; let actual = actualValue === null ? `Element ${this.targetDescription} does not have attribute "${name}"` : `Element ${this.targetDescription} has attribute "${name}" with value ${JSON.stringify(actualValue)}`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } return this; } /** * Assert that the {@link HTMLElement} has no attribute with the provided `name`. * * **Aliases:** `hasNoAttribute`, `lacksAttribute` * * @param {string} name * @param {string?} message * * @example * assert.dom('input.username').doesNotHaveAttribute('disabled'); * * @see {@link #hasAttribute} */ doesNotHaveAttribute(name, message) { let element = this.findTargetElement(); if (!element) return this; let result = !element.hasAttribute(name); let expected = `Element ${this.targetDescription} does not have attribute "${name}"`; let actual = expected; if (!result) { let value = element.getAttribute(name); actual = `Element ${this.targetDescription} has attribute "${name}" with value ${JSON.stringify(value)}`; } if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); return this; } hasNoAttribute(name, message) { return this.doesNotHaveAttribute(name, message); } lacksAttribute(name, message) { return this.doesNotHaveAttribute(name, message); } /** * Assert that the {@link HTMLElement} has an ARIA attribute with the provided * `name` and optionally checks if the attribute `value` matches the provided * text or regular expression. * * @param {string} name * @param {string|RegExp|object?} value * @param {string?} message * * @example * assert.dom('button').hasAria('pressed', 'true'); * * @see {@link #doesNotHaveAria} */ hasAria(name, ...value) { if (value.length === 0) { return this.hasAttribute(`aria-${name}`); } else { return this.hasAttribute(`aria-${name}`, ...value); } } /** * Assert that the {@link HTMLElement} has no ARIA attribute with the * provided `name`. * * **Aliases:** `hasNoAria`, `lacksAria` * * @param {string} name * @param {string?} message * * @example * assert.dom('button').doesNotHaveAria('pressed'); * * @see {@link #hasAria} */ doesNotHaveAria(name, message) { return this.doesNotHaveAttribute(`aria-${name}`, message); } hasNoAria(name, message) { return this.doesNotHaveAria(name, message); } lacksAria(name, message) { return this.doesNotHaveAria(name, message); } /** * Assert that the {@link HTMLElement} has a property with the provided `name` * and checks if the property `value` matches the provided text or regular * expression. * * @param {string} name * @param {RegExp|any} value * @param {string?} message * * @example * assert.dom('input.password-input').hasProperty('type', 'password'); * * @see {@link #doesNotHaveProperty} */ hasProperty(name, value, message) { let element = this.findTargetElement(); if (!element) return this; let description = this.targetDescription; let actualValue = element[name]; if (value instanceof RegExp) { let result = value.test(String(actualValue)); let expected = `Element ${description} has property "${name}" with value matching ${value}`; let actual = `Element ${description} has property "${name}" with value ${JSON.stringify(actualValue)}`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else { let result = value === actualValue; let expected = `Element ${description} has property "${name}" with value ${JSON.stringify(value)}`; let actual = `Element ${description} has property "${name}" with value ${JSON.stringify(actualValue)}`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is disabled. * * @param {string?} message * * @example * assert.dom('.foo').isDisabled(); * * @see {@link #isNotDisabled} */ isDisabled(message) { isDisabled.call(this, message); return this; } /** * Assert that the {@link HTMLElement} or an {@link HTMLElement} matching the * `selector` is not disabled. * * **Aliases:** `isEnabled` * * @param {string?} message * * @example * assert.dom('.foo').isNotDisabled(); * * @see {@link #isDisabled} */ isNotDisabled(message) { isDisabled.call(this, message, { inverted: true }); return this; } isEnabled(message) { return this.isNotDisabled(message); } /** * Assert that the {@link HTMLElement} has the `expected` CSS class using * [`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList). * * `expected` can also be a regular expression, and the assertion will return * true if any of the element's CSS classes match. * * @param {string|RegExp} expected * @param {string?} message * * @example * assert.dom('input[type="password"]').hasClass('secret-password-input'); * * @example * assert.dom('input[type="password"]').hasClass(/.*password-input/); * * @see {@link #doesNotHaveClass} */ hasClass(expected, message) { let element = this.findTargetElement(); if (!element) return this; let actual = element.classList.toString(); if (expected instanceof RegExp) { let classNames = Array.prototype.slice.call(element.classList); let result = classNames.some((className) => { return expected.test(className); }); if (!message) { message = `Element ${this.targetDescription} has CSS class matching ${expected}`; } this.pushResult({ result, actual, expected, message }); } else { let result = element.classList.contains(expected); if (!message) { message = `Element ${this.targetDescription} has CSS class "${expected}"`; } this.pushResult({ result, actual, expected, message }); } return this; } /** * Assert that the {@link HTMLElement} does not have the `expected` CSS class using * [`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList). * * `expected` can also be a regular expression, and the assertion will return * true if none of the element's CSS classes match. * * **Aliases:** `hasNoClass`, `lacksClass` * * @param {string|RegExp} expected * @param {string?} message * * @example * assert.dom('input[type="password"]').doesNotHaveClass('username-input'); * * @example * assert.dom('input[type="password"]').doesNotHaveClass(/username-.*-input/); * * @see {@link #hasClass} */ doesNotHaveClass(expected, message) { let element = this.findTargetElement(); if (!element) return this; let actual = element.classList.toString(); if (expected instanceof RegExp) { let classNames = Array.prototype.slice.call(element.classList); let result = classNames.every((className) => { return !expected.test(className); }); if (!message) { message = `Element ${this.targetDescription} does not have CSS class matching ${expected}`; } this.pushResult({ result, actual, expected: `not: ${expected}`, message }); } else { let result = !element.classList.contains(expected); if (!message) { message = `Element ${this.targetDescription} does not have CSS class "${expected}"`; } this.pushResult({ result, actual, expected: `not: ${expected}`, message }); } return this; } hasNoClass(expected, message) { return this.doesNotHaveClass(expected, message); } lacksClass(expected, message) { return this.doesNotHaveClass(expected, message); } /** * Assert that the [HTMLElement][] has the `expected` style declarations using * [`window.getComputedStyle`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle). * * @param {object} expected * @param {string?} message * * @example * assert.dom('.progress-bar').hasStyle({ * opacity: 1, * display: 'block' * }); * * @see {@link #hasClass} */ hasStyle(expected, message) { return this.hasPseudoElementStyle(null, expected, message); } /** * Assert that the pseudo element for `selector` of the [HTMLElement][] has the `expected` style declarations using * [`window.getComputedStyle`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle). * * @param {string} selector * @param {object} expected * @param {string?} message * * @example * assert.dom('.progress-bar').hasPseudoElementStyle(':after', { * content: '";"', * }); * * @see {@link #hasClass} */ hasPseudoElementStyle(selector, expected, message) { let element = this.findTargetElement(); if (!element) return this; let computedStyle = window.getComputedStyle(element, selector); let expectedProperties = Object.keys(expected); if (expectedProperties.length <= 0) { throw new TypeError(`Missing style expectations. There must be at least one style property in the passed in expectation object.`); } let result = expectedProperties.every(property => (computedStyle.getPropertyValue(property.toString()) || computedStyle[property]) === expected[property]); let actual = {}; expectedProperties.forEach(property => (actual[property] = computedStyle.getPropertyValue(property.toString()) || computedStyle[property])); if (!message) { let normalizedSelector = selector ? selector.replace(/^:{0,2}/, '::') : ''; message = `Element ${this.targetDescription}${normalizedSelector} has style "${JSON.stringify(expected)}"`; } this.pushResult({ result, actual, expected, message }); return this; } /** * Assert that the [HTMLElement][] does not have the `expected` style declarations using * [`window.getComputedStyle`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle). * * @param {object} expected * @param {string?} message * * @example * assert.dom('.progress-bar').doesNotHaveStyle({ * opacity: 1, * display: 'block' * }); * * @see {@link #hasClass} */ doesNotHaveStyle(expected, message) { return this.doesNotHavePseudoElementStyle(null, expected, message); } /** * Assert that the pseudo element for `selector` of the [HTMLElement][] does not have the `expected` style declarations using * [`window.getComputedStyle`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle). * * @param {string} selector * @param {object} expected * @param {string?} message * * @example * assert.dom('.progress-bar').doesNotHavePseudoElementStyle(':after', { * content: '";"', * }); * * @see {@link #hasClass} */ doesNotHavePseudoElementStyle(selector, expected, message) { let element = this.findTargetElement(); if (!element) return this; let computedStyle = window.getComputedStyle(element, selector); let expectedProperties = Object.keys(expected); if (expectedProperties.length <= 0) { throw new TypeError(`Missing style expectations. There must be at least one style property in the passed in expectation object.`); } let result = expectedProperties.some(property => computedStyle[property] !== expected[property]); let actual = {}; expectedProperties.forEach(property => (actual[property] = computedStyle[property])); if (!message) { let normalizedSelector = selector ? selector.replace(/^:{0,2}/, '::') : ''; message = `Element ${this.targetDescription}${normalizedSelector} does not have style "${JSON.stringify(expected)}"`; } this.pushResult({ result, actual, expected, message }); return this; } /** * Assert that the text of the {@link HTMLElement} or an {@link HTMLElement} * matching the `selector` matches the `expected` text, using the * [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) * attribute and stripping/collapsing whitespace. * * `expected` can also be a regular expression. * * > Note: This assertion will collapse whitespace if the type you pass in is a string. * > If you are testing specifically for whitespace integrity, pass your expected text * > in as a RegEx pattern. * * **Aliases:** `matchesText` * * @param {string|RegExp} expected * @param {string?} message * * @example * // <h2 id="title"> * // Welcome to <b>QUnit</b> * // </h2> * * assert.dom('#title').hasText('Welcome to QUnit'); * * @example * assert.dom('.foo').hasText(/[12]\d{3}/); * * @see {@link #includesText} */ hasText(expected, message) { let element = this.findTargetElement(); if (!element) return this; if (expected instanceof RegExp) { let result = typeof element.textContent === 'string' && expected.test(element.textContent); let actual = element.textContent; if (!message) { message = `Element ${this.targetDescription} has text matching ${expected}`; } this.pushResult({ result, actual, expected, message }); } else if (expected.any === true) { let result = Boolean(element.textContent); let expected = `Element ${this.targetDescription} has a text`; let actual = result ? expected : `Element ${this.targetDescription} has no text`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else if (typeof expected === 'string') { expected = collapseWhitespace(expected); let actual = collapseWhitespace(element.textContent || ''); let result = actual === expected; if (!message) { message = `Element ${this.targetDescription} has text "${expected}"`; } this.pushResult({ result, actual, expected, message }); } else { throw new TypeError(`You must pass a string or Regular Expression to "hasText". You passed ${expected}.`); } return this; } matchesText(expected, message) { return this.hasText(expected, message); } /** * Assert that the `textContent` property of an {@link HTMLElement} is not empty. * * @param {string?} message * * @example * assert.dom('button.share').hasAnyText(); * * @see {@link #hasText} */ hasAnyText(message) { return this.hasText({ any: true }, message); } /** * Assert that the `textContent` property of an {@link HTMLElement} is empty. * * @param {string?} message * * @example * assert.dom('div').hasNoText(); * * @see {@link #hasNoText} */ hasNoText(message) { return this.hasText('', message); } /** * Assert that the text of the {@link HTMLElement} or an {@link HTMLElement} * matching the `selector` contains the given `text`, using the * [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) * attribute. * * > Note: This assertion will collapse whitespace in `textContent` before searching. * > If you would like to assert on a string that *should* contain line breaks, tabs, * > more than one space in a row, or starting/ending whitespace, use the {@link #hasText} * > selector and pass your expected text in as a RegEx pattern. * * **Aliases:** `containsText`, `hasTextContaining` * * @param {string} text * @param {string?} message * * @example * assert.dom('#title').includesText('Welcome'); * * @see {@link #hasText} */ includesText(text, message) { let element = this.findTargetElement(); if (!element) return this; let collapsedText = collapseWhitespace(element.textContent || ''); let result = collapsedText.indexOf(text) !== -1; let actual = collapsedText; let expected = text; if (!message) { message = `Element ${this.targetDescription} has text containing "${text}"`; } if (!result && text !== collapseWhitespace(text)) { console.warn('The `.includesText()`, `.containsText()`, and `.hasTextContaining()` assertions collapse whitespace. The text you are checking for contains whitespace that may have made your test fail incorrectly. Try the `.hasText()` assertion passing in your expected text as a RegExp pattern. Your text:\n' + text); } this.pushResult({ result, actual, expected, message }); return this; } containsText(expected, message) { return this.includesText(expected, message); } hasTextContaining(expected, message) { return this.includesText(expected, message); } /** * Assert that the text of the {@link HTMLElement} or an {@link HTMLElement} * matching the `selector` does not include the given `text`, using the * [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) * attribute. * * **Aliases:** `doesNotContainText`, `doesNotHaveTextContaining` * * @param {string} text * @param {string?} message * * @example * assert.dom('#title').doesNotIncludeText('Welcome'); */ doesNotIncludeText(text, message) { let element = this.findTargetElement(); if (!element) return this; let collapsedText = collapseWhitespace(element.textContent || ''); let result = collapsedText.indexOf(text) === -1; let expected = `Element ${this.targetDescription} does not include text "${text}"`; let actual = expected; if (!result) { actual = `Element ${this.targetDescription} includes text "${text}"`; } if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); return this; } doesNotContainText(unexpected, message) { return this.doesNotIncludeText(unexpected, message); } doesNotHaveTextContaining(unexpected, message) { return this.doesNotIncludeText(unexpected, message); } /** * Assert that the `value` property of an {@link HTMLInputElement} matches * the `expected` text or regular expression. * * If no `expected` value is provided, the assertion will fail if the * `value` is an empty string. * * @param {string|RegExp|object?} expected * @param {string?} message * * @example * assert.dom('input.username').hasValue('HSimpson'); * * @see {@link #includesValue} * @see {@link #hasAnyValue} * @see {@link #hasNoValue} */ hasValue(expected, message) { let element = this.findTargetElement(); if (!element) return this; if (arguments.length === 0) { expected = { any: true }; } let value = element.value; if (expected instanceof RegExp) { let result = expected.test(value); let actual = value; if (!message) { message = `Element ${this.targetDescription} has value matching ${expected}`; } this.pushResult({ result, actual, expected, message }); } else if (expected.any === true) { let result = Boolean(value); let expected = `Element ${this.targetDescription} has a value`; let actual = result ? expected : `Element ${this.targetDescription} has no value`; if (!message) { message = expected; } this.pushResult({ result, actual, expected, message }); } else { let actual = value; let result = actual === expected; if (!message) { message = `Element ${this.targetDescription} has value "${expected}"`; } this.pushResult({ result, actual, expected, message }); } return this; } /** * Assert that the `value` property of an {@link HTMLInputElement} includes * the `expected` text. * * **Aliases:** `containsValue`, `hasValueContaining` * * @param {string} expected * @param {string?} message * * @example * assert.dom('textarea.description').includesValue('https://example.com'); * * @see {@link #doesNotIncludeValue} */ includesValue(expected, message) { let element = this.findTargetElement(); if (!element) return this; let actual = element.value; let result = actual.includes(expected); if (!message) { message = `Element ${this.targetDescription} includes value "${expected}"`; } this.pushResult({ result, actual, expected, message }); return this; } containsValue(expected, message) { return this.includesValue(expected, message); } hasValueContaining(expected, message) { return this.includesValue(expected, message); } /** * Assert that the `value` property of an {@link HTMLInputElement} does not include * the `expected` text. * * **Aliases:** `doesNotContainValue`, `doesNotHaveValueContaining` * * @param {string} expected * @param {string?} message * * @example * assert.dom('textarea.description').doesNotIncludeValue('https://example.com'); * * @see {@link #includesValue} */ doesNotIncludeValue(expected, message) { let element = this.findTargetElement(); if (!element) return this; let actual = element.value; let result = !actual.includes(expected); if (!message) { message = `Element ${this.targetDescription} does not include value "${expected}"`; } this.pushResult({ result, actual, expected, message }); return this; } doesNotContainValue(expected, message) { return this.doesNotIncludeValue(expected, message); } doesNotHaveValueContaining(expected, message) { return this.doesNotIncludeValue(expected, message); } /** * Assert that the `value` property of an {@link HTMLInputElement} is not empty. * * @param {string?} message * * @example * assert.dom('input.username').hasAnyValue(); * * @see {@link #hasValue} * @see {@link #hasNoValue} */ hasAnyValue(message) { return this.hasValue({ any: true }, message); } /** * Assert that the `value` property of an {@link HTMLInputElement} is empty. * * **Aliases:** `lacksValue` * * @param {string?} message * * @example * assert.dom('input.username').hasNoValue(); * * @see {@link #hasValue} * @see {@link #hasAnyValue} */ hasNoValue(message) { return this.hasValue('', message); } lacksValue(message) { return this.hasNoValue(message); } /** * Assert that the target selector selects only Elements that are also selected by * compareSelector. * * @param {string} compareSelector * @param {string?} message * * @example * assert.dom('p.red').matchesSelector('div.wrapper p:last-child') */ matchesSelector(compareSelector, message) { let targetElements = this.findElements(); let targets = targetElements.length; let matchFailures = matchesSelector(targetElements, compareSelector); let singleElement = targets === 1; let selectedByPart = this.selectedBy; let actual; let expected; if (matchFailures === 0) { // no failures matching. if (!message) { message = singleElement ? `The element ${selectedByPart} also matches the selector ${compareSelector}.` : `${targets} elements, ${selectedByPart}, also match the selector ${compareSelector}.`; } actual = expected = message; this.pushResult({ result: true, actual, expected, message }); } else { let difference = targets - matchFailures; // there were failures when matching. if (!message) { message = singleElement ? `The element ${selectedByPart} did not also match the selector ${compareSelector}.` : `${matchFailures} out of ${targets} elements ${selectedByPart} did not also match the selector ${compareSelector}.`; } actual = singleElement ? message : `${difference} elements matched ${compareSelector}.`; expected = singleElement ? `The element should have matched ${compareSelector}.` : `${targets} elements should have matched ${compareSelector}.`; this.pushResult({ result: false, actual, expected, message }); } return this; } /** * Assert that the target selector selects only Elements that are not also selected by * compareSelector. * * @param {string} compareSelector * @param {string?} message * * @example * assert.dom('input').doesNotMatchSelector('input[disabled]') */ doesNotMatchSelector(compareSelector, message) { let targetElements = this.findElements(); let targets = targetElements.length; let matchFailures = matchesSelector(targetElements, compareSelector); let singleElement = targets === 1; let selectedByPart = this.selectedBy; let actual; let expected; if (matchFailures === targets) { // the assertion is successful because no element matched the other selector. if (!message) { message = singleElement ? `The element ${selectedByPart} did not also match the selector ${compareSelector}.` : `${targets} elements, ${selectedByPart}, did not also match the selector ${compareSelector}.`; } actual = expected = message; this.pushResult({ result: true, actual, expected, message }); } else { let difference = targets - matchFailures; // the assertion fails because at least one element matched the other selector. if (!message) { message = singleElement ? `The element ${selectedByPart} must not also match the selector ${compareSelector}.` : `${difference} elements out of ${targets}, ${selectedByPart}, must not also match the selector ${compareSelector}.`; } actual = singleElement ? `The element ${selectedByPart} matched ${compareSelector}.` : `${matchFailures} elements did not match ${compareSelector}.`; expected = singleElement ? message : `${targets} elements should not have matched ${compareSelector}.`; this.pushResult({ result: false, actual, expected,