--- title: Use qunit-dom for Better Test Assertions impact: MEDIUM impactDescription: More readable and maintainable tests tags: 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):** ```javascript // 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(); 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):** ```javascript // 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(); assert.dom('.greeting').exists('greeting element exists'); assert.dom('.greeting').hasText('Hello, World!', 'shows greeting'); }); }); ``` ### Common Assertions **Existence and Visibility:** ```javascript test('element visibility', async function (assert) { await render( , ); // 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:** ```javascript test('text assertions', async function (assert) { await render(); // 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:** ```javascript test('attribute assertions', async function (assert) { await render(); // 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:** ```javascript test('class assertions', async function (assert) { await render(); // 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:** ```javascript test('form assertions', async function (assert) { await render( , ); // 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: ```javascript test('chained assertions', async function (assert) { await render(); assert.dom('button') .exists() .hasClass('btn-primary') .hasAttribute('type', 'button') .isNotDisabled() .hasText('Submit') .isVisible(); }); ``` ### Custom Error Messages Add custom messages to make failures clearer: ```javascript test('custom messages', async function (assert) { await render(); 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 ```javascript test('list items', async function (assert) { await render(); // 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: ```javascript test('accessibility', async function (assert) { await render(); // 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 1. **Use data-test attributes** for test selectors instead of classes: ```javascript // Good assert.dom('[data-test-submit-button]').exists(); // Avoid - classes can change assert.dom('.btn.btn-primary').exists(); ``` 2. **Make assertions specific**: ```javascript // Better - exact match assert.dom('h1').hasText('Welcome'); // Less specific - could miss issues assert.dom('h1').containsText('Welc'); ``` 3. **Use meaningful custom messages**: ```javascript assert.dom('[data-test-error]').hasText('Invalid email', 'shows correct validation error'); ``` 4. **Combine with @ember/test-helpers**: ```javascript 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(); ``` 5. **Test user-visible behavior**, not implementation: ```javascript // 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:** - [qunit-dom Documentation](https://github.com/mainmatter/qunit-dom) - [qunit-dom API](https://github.com/mainmatter/qunit-dom/blob/master/API.md) - [Ember Testing Guide](https://guides.emberjs.com/release/testing/)