Test Methods

Updated:

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

In version 1.37.0 of Tecton, we began introducing a series of test methods to assist developers who desire to write tests against the design system without having to dig into the ShadowDOM to manipulate things from there manually.

These serve a couple of purposes:

  1. Provide a more positive developer experience by exposing a simple API to utilize when writing tests that need to interact with the web components.
  2. Enable the Tecton team to make larger updates to the design system without the added stress of breaking the test suites of other products.

While the utility that these test methods perform is quite simple, they are built to emulate the user's natural behavior to the best of our ability. For the setValue method on <q2-select> we:

  • Click the <input> inside of <q2-select> to open the popover.
  • Click the option(s) that match the provided value(s).
  • If multi-select is enabled, click the <input> inside of the <q2-select> to close the popover again.

In addition to providing these test methods, we have written very exhaustive tests to ensure they behave as you would expect, including emitting the necessary events with the documented structure.

Please see below for information on how to utilize these test methods in your specific test runner. If you are using a library not documented on this page or have recommendations for improving it, please let us know!

Supported components

The following components have test methods built into them and are ready to use:

  • Calendar
  • Checkbox group
  • Editable field
  • Input
  • Radio group
  • Select
  • Textarea

Please look for methods labeled with "Test only." These methods are only meant to be used when writing tests against the design system.

Selenium

Below is an example of how you might utilize the test methods using Selenium in Python. In short, it relies on execute_script to call a method on an HTML element. We've provided a helper called execute_tecton_method that will:

  1. Ensure the HTML element exists. If not, raise a NoSuchElementException.
  2. Ensure the provided method exists on the HTML element. If not, raise an Exception.
  3. Execute the method with the provided arguments and then return True.

tecton.py

from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

check_for_method = """
    const element = arguments[0];
    const methodName = arguments[1];
    return typeof element[methodName] === 'function';
"""

execute_method = """
    const element = arguments[0];
    const methodName = arguments[1];
    const methodArguments = arguments[2];
    element[methodName](...methodArguments);
"""

def execute_tecton_method(driver, selector, method_name, method_arguments):
    """
    Executes a method on a Tecton component if it exists.

    Args:
        driver (WebDriver): The WebDriver instance to interact with the web page.
        selector (str): The CSS selector to find the element on the page.
        method_name (str): The name of the method to execute on the element.
        method_arguments (Union[list, Any]): The arguments to pass to the method.

    Raises:
        NoSuchElementException: If the element with the given selector is not found on the page.
    """

    full_selector = f"{selector}[stencil-hydrated]"

    try:
        element = driver.find_element(By.CSS_SELECTOR, full_selector)
    except NoSuchElementException:
        raise NoSuchElementException(f"Element with selector '{full_selector}' not found on the page")

    if driver.execute_script(check_for_method, element, method_name) is False:
        raise Exception(f"Method '{method_name}' does not exist on the element with selector '{selector}'")

    if not isinstance(method_arguments, list):
        method_arguments = [method_arguments]

    driver.execute_script(
        execute_method,
        element,
        method_name,
        method_arguments
    )

    return True

test_file.py

from selenium import webdriver
from tecton import execute_tecton_method

driver = webdriver.Chrome()
driver.get("https://my-url.com")

method_executed = execute_tecton_method(driver, "main q2-input", "setValue", "My value")
assert method_executed == True, "Method should be executed on the element"

Ember

Within the Ember framework, there are a few different types of tests that you can write, but only Rendering/Integration and Application/Acceptance tests expose a DOM where you may want to interact with Tecton components.

To do so, because we're already using Javascript, you can use the find method defined @ember/test-helpers to find one of the elements and then call the method directly.

my-acceptance-test.js

import { find } from "@ember/test-helpers";

describe("Acceptance: Landing Page Tests", () => {
  it("sets the name field", async () => {
    const nameField = find("q2-input#name");
    expect(nameField.value).to.equal("");

    await nameField.setValue("Tony Stark");

    expect(nameField.value).to.equal("Tony Stark");
  });
});

Rendering/Integration tests

These tests are great for ensuring your components render and behave as expected. However, a common problem has been that your tests can start trying to interact with the Tecton components before they are hydrated and ready for use.

For that reason, we've put together this quick little helper function that allows you to wait until any components on the page are hydrated.

tecton.js

// componentList is as of 4/8/2024
const componentList = [
  "q2-btn",
  "q2-calendar",
  "q2-checkbox-group",
  "q2-checkbox",
  "q2-editable-field",
  "q2-input",
  "q2-optgroup",
  "q2-option",
  "q2-radio-group",
  "q2-radio",
  "q2-select",
  "q2-textarea",
  "q2-avatar",
  "q2-badge",
  "q2-card",
  "q2-carousel",
  "q2-carousel-pane",
  "q2-data-table",
  "q2-icon",
  "q2-pill",
  "q2-tag",
  "q2-dropdown",
  "q2-dropdown-item",
  "q2-pagination",
  "q2-section",
  "q2-tab-container",
  "q2-tab-pane",
  "q2-stepper",
  "q2-stepper-vertical",
  "q2-stepper-pane",
  "q2-loading",
  "q2-message",
  "q2-chart-area",
  "q2-chart-bar",
  "q2-chart-donut",
  "q2-loc",
  "q2-tooltip",
];
const componentSelector = componentList.join(",");

/**
 * Accepts a component element and returns a promise that resolves when the element is hydrated
 * @param componentElement - The element to check and wait for hydration
 * @returns {Promise<void>}
 */
function waitForElementToBeHydrated(componentElement) {
  return new Promise((resolve) => {
    if (componentElement?.hasAttribute("stencil-hydrated") ?? false) {
      resolve();
    } else {
      const observer = new MutationObserver((_, obs) => {
        if (componentElement.hasAttribute("stencil-hydrated")) {
          obs.disconnect();
          resolve();
        }
      });
      observer.observe(componentElement, { attributes: true });
    }
  });
}

/**
 * Finds any elements on the page that are part of the Tecton component library and waits for them to be hydrated
 * @returns {Promise<void>}
 */
export async function waitForTecton() {
  const allElements = document.querySelectorAll(componentSelector);

  const hydrationPromises = Array.from(allElements).map((element) =>
    waitForElementToBeHydrated(element)
  );

  await Promise.all(hydrationPromises);
}

To use this helper in your Integration tests, you can do something like the following:

my-integration-test.js

import { find } from '@ember/test-helpers';
import { waitForTecton } from "/tests/helpers/tecton";

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

    await waitForTecton();

    const nameField = find("q2-input#name");
    expect(nameField.value).to.equal("");

    await nameField.setValue("Tony Stark");

    expect(nameField.value).to.equal("Tony Stark");
  });
});

Cypress

With the cypress as a test framework, there several different types of test you can choose, we introduce two most popular type of test called, End-to-end test and Component test.

End-to-end

Cypress was originally designed to run end-to-end (E2E) tests on anything that runs in a browser. A typical E2E test visits the application in a browser and performs actions via the UI just like a real user would.

context('Actions', () => {
  beforeEach(() => {
    cy.visit('https://your-service-url/signup');
  });

  // https://on.cypress.io/interacting-with-elements
  it('cy.q2-input test', () => {
    const firstName = 'John';
    const input = cy.get('q2-input[name="first-name"]');
    const innerInput = input.shadow().find('input');
    innerInput.type(firstName);
    innerInput.should('have.value', firstName);
  });
});

Component

You can also use Cypress to mount components from supported web frameworks and execute component tests

// CustomButton.tsx
import React from 'react';
import { Q2Btn } from 'q2-tecton-framework-wrappers/dist/react';

const CustomButton: React.FC<any> = (props) => {
  return (
    <Q2Btn intent={props.intent} label={props.label}></Q2Btn>
  );
};
export default CustomButton;
// CustomButton.cy.tsx
import CustomButton from './CustomButton';
import setupDesignSystem from 'q2-design-system';

describe('CustomButton', () => {
  beforeEach(async () => {
    await setupDesignSystem();
  })

  it('should mount with proper label', () => {
    const label = "Click me Q2Button"
    cy.mount(<CustomButton intent="workflow-primary" label={label}></CustomButton>);
    cy.get('q2-btn').should('have.attr', 'label', label);
  });

});

Release Notes

Tecton 1.37.0

Initial release