8.0 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use qunit-dom for Better Test Assertions | MEDIUM | More readable and maintainable tests | testing, qunit-dom, assertions, best-practices |
Use qunit-dom for Better Test Assertions
Use qunit-dom for DOM assertions in tests. It provides expressive, chainable assertions that make tests more readable and provide better error messages than raw QUnit assertions.
Why qunit-dom:
- More expressive and readable test assertions
- Better error messages when tests fail
- Type-safe with TypeScript
- Reduces boilerplate in DOM testing
Basic DOM Assertions
Incorrect (verbose QUnit assertions):
// tests/integration/components/greeting-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
module('Integration | Component | greeting', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(<template><Greeting @name="World" /></template>);
const element = this.element.querySelector('.greeting');
assert.ok(element, 'greeting element exists');
assert.equal(element.textContent.trim(), 'Hello, World!', 'shows greeting');
assert.ok(element.classList.contains('greeting'), 'has greeting class');
});
});
Correct (expressive qunit-dom):
// tests/integration/components/greeting-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
module('Integration | Component | greeting', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(<template><Greeting @name="World" /></template>);
assert.dom('.greeting').exists('greeting element exists');
assert.dom('.greeting').hasText('Hello, World!', 'shows greeting');
});
});
Common Assertions
Existence and Visibility:
test('element visibility', async function (assert) {
await render(
<template>
<MyComponent />
</template>,
);
// Element exists in DOM
assert.dom('[data-test-output]').exists();
// Element doesn't exist
assert.dom('[data-test-deleted]').doesNotExist();
// Element is visible (not display: none or visibility: hidden)
assert.dom('[data-test-visible]').isVisible();
// Element is not visible
assert.dom('[data-test-hidden]').isNotVisible();
// Count elements
assert.dom('[data-test-item]').exists({ count: 3 });
});
Text Content:
test('text assertions', async function (assert) {
await render(<template><Article @title="Hello World" /></template>);
// Exact text match
assert.dom('h1').hasText('Hello World');
// Contains text (partial match)
assert.dom('p').containsText('Hello');
// Any text exists
assert.dom('h1').hasAnyText();
// No text
assert.dom('.empty').hasNoText();
});
Attributes:
test('attribute assertions', async function (assert) {
await render(<template><Button @disabled={{true}} /></template>);
// Has attribute (any value)
assert.dom('button').hasAttribute('disabled');
// Has specific attribute value
assert.dom('button').hasAttribute('type', 'submit');
// Attribute value matches regex
assert.dom('a').hasAttribute('href', /^https:\/\//);
// Doesn't have attribute
assert.dom('button').doesNotHaveAttribute('aria-hidden');
// Has ARIA attributes
assert.dom('[role="button"]').hasAttribute('aria-label', 'Close dialog');
});
Classes:
test('class assertions', async function (assert) {
await render(<template><Card @status="active" /></template>);
// Has single class
assert.dom('.card').hasClass('active');
// Doesn't have class
assert.dom('.card').doesNotHaveClass('disabled');
// Has no classes at all
assert.dom('.plain').hasNoClass();
});
Form Elements:
test('form assertions', async function (assert) {
await render(
<template>
<form>
<input type="text" value="hello" />
<input type="checkbox" checked />
<input type="radio" disabled />
<select>
<option selected>Option 1</option>
</select>
</form>
</template>,
);
// Input value
assert.dom('input[type="text"]').hasValue('hello');
// Checkbox/radio state
assert.dom('input[type="checkbox"]').isChecked();
assert.dom('input[type="checkbox"]').isNotChecked();
// Disabled state
assert.dom('input[type="radio"]').isDisabled();
assert.dom('input[type="text"]').isNotDisabled();
// Required state
assert.dom('input').isRequired();
assert.dom('input').isNotRequired();
// Focus state
assert.dom('input').isFocused();
assert.dom('input').isNotFocused();
});
Chaining Assertions
You can chain multiple assertions on the same element:
test('chained assertions', async function (assert) {
await render(<template><Button @variant="primary" @disabled={{false}} /></template>);
assert.dom('button')
.exists()
.hasClass('btn-primary')
.hasAttribute('type', 'button')
.isNotDisabled()
.hasText('Submit')
.isVisible();
});
Custom Error Messages
Add custom messages to make failures clearer:
test('custom messages', async function (assert) {
await render(<template><UserProfile @user={{this.user}} /></template>);
assert.dom('[data-test-username]')
.hasText(this.user.name, 'username is displayed correctly');
assert.dom('[data-test-avatar]')
.exists('user avatar should be visible');
});
Testing Counts
test('list items', async function (assert) {
await render(<template>
<TodoList @todos={{this.todos}} />
</template>);
// Exact count
assert.dom('[data-test-todo]').exists({ count: 5 });
// At least one
assert.dom('[data-test-todo]').exists({ count: 1 });
// None
assert.dom('[data-test-todo]').doesNotExist();
});
Accessibility Testing
Use qunit-dom for basic accessibility checks:
test('accessibility', async function (assert) {
await render(<template><Modal @onClose={{this.close}} /></template>);
// ARIA roles
assert.dom('[role="dialog"]').exists();
assert.dom('[role="dialog"]').hasAttribute('aria-modal', 'true');
// Labels
assert.dom('[aria-label="Close modal"]').exists();
// Focus management
assert.dom('[data-test-close-button]').isFocused();
// Required fields
assert.dom('input[name="email"]').hasAttribute('aria-required', 'true');
});
Best Practices
-
Use data-test attributes for test selectors instead of classes:
// Good assert.dom('[data-test-submit-button]').exists(); // Avoid - classes can change assert.dom('.btn.btn-primary').exists(); -
Make assertions specific:
// Better - exact match assert.dom('h1').hasText('Welcome'); // Less specific - could miss issues assert.dom('h1').containsText('Welc'); -
Use meaningful custom messages:
assert.dom('[data-test-error]').hasText('Invalid email', 'shows correct validation error'); -
Combine with @ember/test-helpers:
import { click, fillIn } from '@ember/test-helpers'; await fillIn('[data-test-email]', 'user@example.com'); await click('[data-test-submit]'); assert.dom('[data-test-success]').exists(); -
Test user-visible behavior, not implementation:
// Good - tests what user sees assert.dom('[data-test-greeting]').hasText('Hello, Alice'); // Avoid - tests implementation details assert.ok(this.component.internalState === 'ready');
qunit-dom makes your tests more maintainable and easier to understand. It comes pre-installed with ember-qunit, so you can start using it immediately.
References: