qunit-dom
Version:
High Level DOM Assertions for QUnit
1,444 lines (1,428 loc) • 62.8 kB
JavaScript
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,