Test Methods

Updated:

Instructions and examples of how to use the test methods defined in the design system.

Tecton exposes async, test methods on its web components so tests can drive them without reaching into the shadow DOM. This guide covers when to use them, the rules that apply across every framework, and framework-specific recipes.

Overview

Certain Tecton components ship with methods like setValue() that emulate real user interaction. For example, on <q2-select>, setValue opens the popover, clicks the matching option, and closes it again. These methods are called out as being intended for test environments only in the documentation, and tagged as @testOnly in our JSDoc. They exist so your test suite doesn't have to duplicate that choreography or crack open the shadow DOM.

Use them for E2E, acceptance, integration, or rendering tests. Anywhere the component is rendered in a real DOM.

The universal pattern

Every framework recipe below is a spelling of the same four steps. Understand these once and the differences below are surface-level.

1. Wait for hydration with componentOnReady(). Tecton components are made with Stencil. The library adds componentOnReady() to every component element, which returns a promise that resolves once the component has finished hydrating, and resolves immediately if it already has. Prefer this over polling for the stencil-hydrated attribute; the attribute is an implementation detail, componentOnReady() is the officially supported API.

2. Await the returned promise. Every test method returns Promise<void>. If your framework doesn't have a first-class way to await a promise inside a command (Selenium's execute_script, jQuery-style chains), use the async variant (execute_async_script) or wrap the call so the promise settles before assertions run.

3. Assert on the property, not the attribute. After you invoke a test method, if you are expecting the value of a property to be changed, check the updated value on element.<PROPERTY>, not on a DOM attribute. This is because not all property updates are reflected in the HTML.

4. Move focus if you need change. setValue leaves focus on the internal <input>, which is intentional so it matches a real user typing. As a result the change event doesn't fire until focus moves off. If your test depends on change, call .blur(), focus another field, or dispatch the event manually after setValue.

Supported components

The following components ship with test methods. See each component's documentation or JSDoc for the full list of methods and their signatures.

  • q2-calendar
  • q2-carousel
  • q2-chart-area
  • q2-chart-donut
  • q2-checkbox-group
  • q2-currency
  • q2-data-table
  • q2-dropdown
  • q2-editable-field
  • q2-input
  • q2-link
  • q2-pagination
  • q2-pill
  • q2-radio-group
  • q2-search
  • q2-section
  • q2-select
  • q2-stepper
  • q2-stepper-vertical
  • q2-tab-container
  • q2-textarea

Framework recipes

Each recipe assumes the app under test has already loaded the Tecton Design System. Every example does the same thing: locate a <q2-input>, await componentOnReady(), await setValue("Tony Stark"), assert the resulting property.

Playwright

Prefer locators since they retry until the element is present, and toHaveJSProperty retries the assertion window. You can wrap the componentOnReady() + evaluate boilerplate in a small helper so tests stay focused on intent:

// tests/tecton.ts
import type { Locator } from '@playwright/test';

/**
 * Awaits componentOnReady() on the located Tecton element, then invokes the
 * named test method with the given arguments.
 */
export async function callTectonMethod<T = unknown>(
  locator: Locator,
  method: string,
  ...args: unknown[]
): Promise<T> {
  return locator.evaluate(
    async (el, payload) => {
      await (el as any).componentOnReady();
      return (el as any)[payload.method](...payload.args);
    },
    { method, args },
  ) as Promise<T>;
}

Then the test is just:

import { test, expect } from '@playwright/test';
import { callTectonMethod } from './tecton';

test('sets the name field', async ({ page }) => {
  await page.goto('/signup');

  const nameField = page.locator('q2-input#name');
  await callTectonMethod(nameField, 'setValue', 'Tony Stark');

  await expect(nameField).toHaveJSProperty('value', 'Tony Stark');
});

The helper returns whatever the method returns, so methods that produce a value work too (e.g., const foo = await callTectonMethod<Foo>(locator, 'getSomething');).

Cypress

Cypress commands are not promises, so never assign cy.get(...) to a variable expecting a resolved element. Use aliases (.as('...')) and let .invoke() await the promise each method returns.

describe('signup', () => {
  it('sets the name field', () => {
    cy.visit('/signup');

    // Alias the raw DOM element (not the jQuery wrapper).
    cy.get('q2-input#name').its(0).as('nameField');

    // .invoke() auto-awaits the promise each method returns.
    cy.get('@nameField').invoke('componentOnReady');
    cy.get('@nameField').invoke('setValue', 'Tony Stark');

    // .should() retries until the assertion passes.
    cy.get('@nameField').its('value').should('equal', 'Tony Stark');
  });
});

Selenium (Python)

Use execute_async_script, not execute_script. The synchronous version fires and forgets, meaning subsequent assertions can run before the promise settles. You can wrap the componentOnReady() + async-script boilerplate in a small helper so tests stay focused on intent:

# tecton.py
from selenium.common.exceptions import NoSuchElementException


CALL_METHOD = """
    const done = arguments[arguments.length - 1];
    const [selector, methodName, args] = [arguments[0], arguments[1], arguments[2]];
    (async () => {
      const el = document.querySelector(selector);
      if (!el) return done({ error: 'element-missing' });
      if (typeof el.componentOnReady === 'function') {
        await el.componentOnReady();
      }
      if (typeof el[methodName] !== 'function') {
        return done({ error: 'method-missing' });
      }
      try {
        const result = await el[methodName](...args);
        done({ ok: true, result });
      } catch (e) {
        done({ error: 'threw', message: e.message });
      }
    })();
"""


def execute_tecton_method(driver, selector, method_name, *args):
    """Await a Tecton test method on the first matching element."""
    driver.set_script_timeout(15)
    outcome = driver.execute_async_script(
        CALL_METHOD, selector, method_name, list(args)
    )

    if outcome.get("error") == "element-missing":
        raise NoSuchElementException(f"No element matched '{selector}'")
    if outcome.get("error") == "method-missing":
        raise AttributeError(
            f"'{method_name}' is not a function on '{selector}'"
        )
    if outcome.get("error") == "threw":
        raise RuntimeError(f"'{method_name}' threw: {outcome.get('message')}")

    return outcome["result"]

Then the test is just:

from selenium.webdriver.common.by import By

from tecton import execute_tecton_method


def test_sets_name(driver):
    driver.get("https://your-app.example/signup")

    execute_tecton_method(driver, "q2-input#name", "setValue", "Tony Stark")

    field = driver.find_element(By.CSS_SELECTOR, "q2-input#name")
    assert field.get_property("value") == "Tony Stark"

The helper returns whatever the method returns, and raises typed errors when the element is missing, the method doesn't exist, or the method throws, resulting in a clearer signal than a raw execute_async_script failure.

Jest + Puppeteer

jest-puppeteer exposes a global page. page.evaluate awaits the promise you return from the callback, so wrapping the document.querySelector + componentOnReady() + method-call sequence in a helper keeps every test one line:

// tests/tecton.js

/**
 * Awaits componentOnReady() on the first element matching the selector,
 * then invokes the named test method with the given arguments.
 */
async function callTectonMethod(page, selector, method, ...args) {
  return page.evaluate(
    async (sel, name, callArgs) => {
      const el = document.querySelector(sel);
      await el.componentOnReady();
      return el[name](...callArgs);
    },
    selector,
    method,
    args,
  );
}

module.exports = { callTectonMethod };

Then the test is just:

/* global page */
const { callTectonMethod } = require('./tecton');

describe('signup', () => {
  test('sets the name field', async () => {
    await page.goto('https://your-app.example/signup');

    await callTectonMethod(page, 'q2-input#name', 'setValue', 'Tony Stark');

    const value = await page.$eval('q2-input#name', (el) => el.value);
    expect(value).toBe('Tony Stark');
  });
});

page.$eval is Puppeteer's shorthand for querySelector + evaluate, which is a clean way to read a property back for the assertion. The helper itself returns whatever the method returns, so methods that produce a value work too.

Ember

find() from @ember/test-helpers is a synchronous query which returns null if the element isn't there yet. Pair it with waitUntil to poll for the element, then await componentOnReady() before calling any test methods.

Acceptance test:

import { find, visit, waitUntil } from '@ember/test-helpers';

describe('Acceptance: Landing Page', () => {
  it('sets the name field', async () => {
    await visit('/signup');

    const nameField = await waitUntil(() => find('q2-input#name'));
    await nameField.componentOnReady();

    await nameField.setValue('Tony Stark');
    expect(nameField.value).to.equal('Tony Stark');
  });
});

Integration/rendering test:

render(hbs\...`)returns before Tecton components finish hydrating, so waiting is required. For pages with multiple Tecton components, drop this helper intotests/helpers/tecton.jsand use it afterrender`:

// tests/helpers/tecton.js
/**
 * Waits for every Tecton element on the page to be hydrated. Matches by tag
 * prefix rather than a hard-coded list so it stays correct as new components
 * ship.
 */
export async function waitForTecton() {
  const tectonElements = [...document.getElementsByTagName('*')].filter(
    (el) => el.tagName.toLowerCase().startsWith('q2-'),
  );

  await Promise.all(
    tectonElements.map((el) =>
      typeof el.componentOnReady === 'function'
        ? el.componentOnReady()
        : Promise.resolve(),
    ),
  );
}

Then the test is just:

import { find, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { waitForTecton } from '../helpers/tecton';

describe('Integration | Component | MyComponent', () => {
  it('sets the name field', async () => {
    await render(hbs`<MyComponent/>`);
    await waitForTecton();

    const nameField = find('q2-input#name');
    await nameField.setValue('Tony Stark');
    expect(nameField.value).to.equal('Tony Stark');
  });
});