Files
marco/.agents/skills/ember-best-practices/rules/testing-render-patterns.md

7.9 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Appropriate Render Patterns in Tests MEDIUM Simpler test code and better readability testing, render, component-testing, test-helpers

Use Appropriate Render Patterns in Tests

Choose the right rendering pattern based on whether your component needs arguments, blocks, or attributes in the test.

Incorrect (using template tag unnecessarily):

// tests/integration/components/loading-spinner-test.js
import { render } from '@ember/test-helpers';
import LoadingSpinner from 'my-app/components/loading-spinner';

test('it renders', async function (assert) {
  // ❌ Unnecessary template wrapper for component with no args
  await render(
    <template>
      <LoadingSpinner />
    </template>,
  );

  assert.dom('[data-test-spinner]').exists();
});

Correct (direct component render when no args needed):

// tests/integration/components/loading-spinner-test.js
import { render } from '@ember/test-helpers';
import LoadingSpinner from 'my-app/components/loading-spinner';

test('it renders', async function (assert) {
  // ✅ Simple: pass component directly when no args needed
  await render(LoadingSpinner);

  assert.dom('[data-test-spinner]').exists();
});

Pattern 1: Direct component render (no args/blocks/attributes):

// tests/integration/components/loading-spinner-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import LoadingSpinner from 'my-app/components/loading-spinner';

module('Integration | Component | loading-spinner', function (hooks) {
  setupRenderingTest(hooks);

  test('it renders without arguments', async function (assert) {
    // ✅ Simple: pass component directly when no args needed
    await render(LoadingSpinner);

    assert.dom('[data-test-spinner]').exists();
    assert.dom('[data-test-spinner]').hasClass('loading');
  });
});

Pattern 2: Template tag render (with args/blocks/attributes):

// tests/integration/components/user-card-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import UserCard from 'my-app/components/user-card';

module('Integration | Component | user-card', function (hooks) {
  setupRenderingTest(hooks);

  test('it renders with arguments', async function (assert) {
    const user = { name: 'John Doe', email: 'john@example.com' };

    // ✅ Use template tag when passing arguments
    await render(<template><UserCard @user={{user}} /></template>);

    assert.dom('[data-test-user-name]').hasText('John Doe');
  });

  test('it renders with block content', async function (assert) {
    // ✅ Use template tag when providing blocks
    await render(
      <template>
        <UserCard>
          <:header>Custom Header</:header>
          <:body>Custom Content</:body>
        </UserCard>
      </template>,
    );

    assert.dom('[data-test-header]').hasText('Custom Header');
    assert.dom('[data-test-body]').hasText('Custom Content');
  });

  test('it renders with HTML attributes', async function (assert) {
    // ✅ Use template tag when passing HTML attributes
    await render(<template><UserCard class="featured" data-test-featured /></template>);

    assert.dom('[data-test-featured]').exists();
    assert.dom('[data-test-featured]').hasClass('featured');
  });
});

Complete example showing both patterns:

// tests/integration/components/button-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import Button from 'my-app/components/button';

module('Integration | Component | button', function (hooks) {
  setupRenderingTest(hooks);

  test('it renders default button', async function (assert) {
    // ✅ No args needed - use direct render
    await render(Button);

    assert.dom('button').exists();
    assert.dom('button').hasText('Click me');
  });

  test('it renders with custom text', async function (assert) {
    // ✅ Needs block content - use template tag
    await render(
      <template>
        <Button>Submit Form</Button>
      </template>,
    );

    assert.dom('button').hasText('Submit Form');
  });

  test('it handles click action', async function (assert) {
    assert.expect(1);

    const handleClick = () => {
      assert.ok(true, 'Click handler called');
    };

    // ✅ Needs argument - use template tag
    await render(
      <template>
        <Button @onClick={{handleClick}}>Click me</Button>
      </template>,
    );

    await click('button');
  });

  test('it applies variant styling', async function (assert) {
    // ✅ Needs argument - use template tag
    await render(
      <template>
        <Button @variant="primary">Primary Button</Button>
      </template>,
    );

    assert.dom('button').hasClass('btn-primary');
  });
});

Testing template-only components:

// tests/integration/components/icon-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import Icon from 'my-app/components/icon';

module('Integration | Component | icon', function (hooks) {
  setupRenderingTest(hooks);

  test('it renders default icon', async function (assert) {
    // ✅ Template-only component with no args - use direct render
    await render(Icon);

    assert.dom('[data-test-icon]').exists();
  });

  test('it renders specific icon', async function (assert) {
    // ✅ Needs @name argument - use template tag
    await render(<template><Icon @name="check" @size="large" /></template>);

    assert.dom('[data-test-icon]').hasAttribute('data-icon', 'check');
    assert.dom('[data-test-icon]').hasClass('icon-large');
  });
});

Decision guide:

Scenario Pattern Example
No arguments, blocks, or attributes render(Component) render(LoadingSpinner)
Component needs arguments render(<template>...</template>) render(<template><Card @title="Hello" /></template>)
Component receives block content render(<template>...</template>) render(<template><Card>Content</Card></template>)
Component needs HTML attributes render(<template>...</template>) render(<template><Card class="featured" /></template>)
Multiple test context properties render(<template>...</template>) render(<template><Card @data={{this.data}} /></template>)

Why this matters:

  • Simplicity: Direct render reduces boilerplate for simple cases
  • Clarity: Template syntax makes data flow explicit when needed
  • Consistency: Clear pattern helps teams write maintainable tests
  • Type Safety: Both patterns work with TypeScript for component types

Common patterns:

// ✅ Simple component, no setup needed
await render(LoadingSpinner);
await render(Divider);
await render(Logo);

// ✅ Component with arguments from test context
await render(
  <template><UserList @users={{this.users}} @onSelect={{this.handleSelect}} /></template>,
);

// ✅ Component with named blocks
await render(
  <template>
    <Modal>
      <:header>Title</:header>
      <:body>Content</:body>
      <:footer><button>Close</button></:footer>
    </Modal>
  </template>,
);

// ✅ Component with splattributes
await render(
  <template>
    <Card class="highlighted" data-test-card role="article">
      Card content
    </Card>
  </template>,
);

Using the appropriate render pattern keeps tests clean and expressive.

Reference: Ember Testing Guide