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:
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:
<input> inside of <q2-select> to open the popover.<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!
The following components have test methods built into them and are ready to use:
Please look for methods labeled with "Test only." These methods are only meant to be used when writing tests against the design system.
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:
NoSuchElementException.Exception.True.tecton.pyfrom 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 Truetest_file.pyfrom 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"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.jsimport { 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");
});
});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.jsimport { 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");
});
});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.
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);
});
});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);
});
});
Initial release