diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 6a3c761..942f718 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -6,29 +6,29 @@ - [About Queries](./core/queries/about-queries.md) - [By Role](./core/queries/by-role.md) - [By Label Text](./core/queries/by-label-text.md) - - [By Placeholder Text]() - - [By Text]() - - [By Display Value]() - - [By Alt Text]() - - [By Title]() - - [By Test ID]() + - [By Placeholder Text](./core/queries/by-placeholder-text.md) + - [By Text](./core/queries/by-text.md) + - [By Display Value](./core/queries/by-display-value.md) + - [By Alt Text](./core/queries/by-alt-text.md) + - [By Title](./core/queries/by-title.md) + - [By Test ID](./core/queries/by-test-id.md) - [User Actions](./core/user-actions/README.md) - [Firing Events](./core/user-actions/firing-events.md) - [Async Methods]() - [Appearance and Disappearance]() - [Considerations]() - [Advanced](./core/advanced/README.md) - - [Accessibility]() + - [Accessibility](./core/advanced/accessibility.md) - [Custom Queries]() - [Debugging]() - [Querying Within Elements]() - - [Configuration Options]() + - [Configuration Options](./core/advanced/configuration-options.md) - [Frameworks](./frameworks/README.md) - - [DOM Testing Library](./frameworks/dom.md) + - [DOM Testing Library](./frameworks/dom/README.md) - [Introduction]() - - [Install]() + - [Install](./frameworks/dom/install.md) - [Example]() - [Setup]() - - [API]() + - [API](./frameworks/dom/api.md) - [Cheatsheet]() - [Contributing]() diff --git a/book/src/core/advanced/README.md b/book/src/core/advanced/README.md index da10b4a..cdb55ad 100644 --- a/book/src/core/advanced/README.md +++ b/book/src/core/advanced/README.md @@ -1,3 +1,7 @@ # Advanced -TODO +- [Accessibility](./accessibility.md) +- Custom Queries +- Debugging +- Querying Within Elements +- [Configuration Options](./configuration-options.md) diff --git a/book/src/core/advanced/accessibility.md b/book/src/core/advanced/accessibility.md new file mode 100644 index 0000000..b919f21 --- /dev/null +++ b/book/src/core/advanced/accessibility.md @@ -0,0 +1,109 @@ +# Accessibility + +## Testing for Accessibility + +One of the guiding principles of the Testing Library APIs is that they should enable you to test your app the way your users use it, including through accessibility interfaces like screen readers. + +See the page on [queries](../queries/about-queries.md#priority) for details on how using a semantic HTML query can make sure your app works with browser accessibility APIs. + +## `get_roles` + +```rust,ignore +use testing_library_dom::{get_roles, GetRolesOptions}; +use wasm_bindgen_test::console_log; +use web_sys::window; + +let document = window() + .expect("Window should exist.") + .document() + .expect("Document should exist."); + +let nav = document.create_element("nav")?; +nav.set_inner_html("\ +\ +"); + +console_log!("{:#?}", get_roles(nav, GetRolesOptions::default())); + +// { +// Navigation: [ +// Element { +// obj: Node { +// obj: EventTarget { +// obj: Object { +// obj: JsValue(HTMLElement), +// }, +// }, +// }, +// }, +// ], +// List: [ +// Element { +// obj: Node { +// obj: EventTarget { +// obj: Object { +// obj: JsValue(HTMLUListElement), +// }, +// }, +// }, +// }, +// ], +// Listitem: [ +// Element { +// obj: Node { +// obj: EventTarget { +// obj: Object { +// obj: JsValue(HTMLLIElement), +// }, +// }, +// }, +// }, +// Element { +// obj: Node { +// obj: EventTarget { +// obj: Object { +// obj: JsValue(HTMLLIElement), +// }, +// }, +// }, +// }, +// ], +// } +``` + +## `is_inaccessible` + +This function will compute if the given element should be excluded from the accessibility API by the browser. It implements every **MUST** criteria from the [Excluding Elements from the Accessibility Tree](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion) section in WAI-ARIA 1.2 with the exception of checking the `role` attribute. + +It is defined as: + +```rust,ignore +fn is_inaccessible(element: &Element) -> bool; +``` + +## `log_roles` + +This helper function can be used to print out a list of all the implicit ARIA roles within a tree of DOM nodes, each role containing a list of all of the nodes which match that role. This can be helpful for finding ways to query the DOM under test with [By Role](../queries/by-role.md). + +```rust,ignore +use testing_library_dom::{log_roles, PrettyRolesOptions}; +use web_sys::window; + +let document = window() + .expect("Window should exist.") + .document() + .expect("Document should exist."); + +let nav = document.create_element("nav")?; +nav.set_inner_html("\ +\ +"); + +log_roles(nav, PrettyRolesOptions::default()); +``` diff --git a/book/src/core/advanced/configuration-options.md b/book/src/core/advanced/configuration-options.md new file mode 100644 index 0000000..45d374d --- /dev/null +++ b/book/src/core/advanced/configuration-options.md @@ -0,0 +1,60 @@ +# Configuration Options + +## Introduction + +## Options + +### `default_hidden` + +The default value for the [`hidden` option](../queries/by-role.md#hidden) used by `get_by_role`. Defaults to `false`. + +### `default_ignore` + +The default value for the [`ignore` option](../queries/by-text.md#ignore) used by `get_by_text`. Also determines the nodes that are being ignored when errors are printed. + +Defaults to `script, style`. + +### `throw_suggestions` (experimental) + +When enabled, if [better queries](../queries/about-queries.md#priority) are available, the test will fail and provide a suggested query to use instead. Defaults to `false`. + +To disable a suggestion for a single query just add `.suggest(false)` as an option. + +```rust,ignore +// Will not throw a suggestion. +screen.get_by_test_id("foo", MatcherOptions::default().suggest(false)) +``` + +> **Note** +> +> When this option is enabled, it may provide suggestions that lack an intuitive implementation. Typically this happens for [roles which cannot be named](https://w3c.github.io/aria/#namefromprohibited), most notably paragraphs. For instance, if you attempt to use `get_by_text`, you may encounter the following error: +> +> ```text +> A better query is available, try this: +> get_by_role(AriaRole::Paragraph) +> ``` +> +> However, there is no direct way to query paragraphs using the config parameter, such as in `get_by_role(AriaRole::Paragraph, ByRoleOptions::default().name("Hello World"))`. +> +> To address this issue, you can leverage a custom function to validate the element's structure, as shown in the example below. More information can be found in the [GitHub issue](https://github.com/testing-library/dom-testing-library/issues/1306). +> +> ```rust,ignore +> get_by_role( +> AriaRole::Paragraph, +> ByRoleOptions::default().name(Rc::new(|_, element| { +> element.map(|element| element.text_content()).is_some_and(|content| content == "Hello World") +> })) +> ) +> ``` + +### `test_id_attribute` + +The attribute used by `get_by_test_id` and related queries. Defaults to `data-testid`. + +### `get_element_error` + +A function that returns the error used when [get or find queries](../queries/about-queries.md#types-of-queries) fail. Takes the error message and container as arguments. + +### `async_util_timeout` + +The global timeout value in milliseconds used by `wait_for` utilities. Defaults to 1000ms. diff --git a/book/src/core/queries/README.md b/book/src/core/queries/README.md index 0da7501..cf41e65 100644 --- a/book/src/core/queries/README.md +++ b/book/src/core/queries/README.md @@ -2,10 +2,10 @@ - [About Queries](./about-queries.md) - [By Role](./by-role.md) - +- [By Label Text](./by-label-text.md) +- [By Placeholder Text](./by-placeholder-text.md) +- [By Text](./by-label-text.md) +- [By Display Value](./by-display-value.md) +- [By Alt Text](./by-alt-text.md) +- [By Title](./by-title.md) +- [By Test ID](./by-test-id.md) diff --git a/book/src/core/queries/about-queries.md b/book/src/core/queries/about-queries.md index 702f139..dd1bfb0 100644 --- a/book/src/core/queries/about-queries.md +++ b/book/src/core/queries/about-queries.md @@ -1,3 +1,208 @@ # About Queries +## Overview + +Queries are the methods that Testing Library gives you to find elements on the page. There are several [types of queries](#types-of-queries) ("get", "find", "query"); the difference between them is whether the query will return an error if no element is found or if it will return a future and retry. Depending on what page content you are selecting, different queries may be more or less appropriate. See the [priority guide](#priority) for recommendations on how to make use of semantic queries to test your page in the most accessible way. + +After selecting an element, you can use the [Events API](../user-actions/firing-events.md) or [user-event](#) to fire events and simulate user interactions with the page, or make assertions about the element. + +There are Testing Library helper methods that work with queries. As elements appear and disappear in response to actions, [Async APIs](#) like [`wait_for`](#) or [`find_by` queries](#) can be used to await the changes in the DOM. To find only elements that are children of a specific element, you can use [`within`](#). If necessary, there are also a few options you can configure, like the timeout for retries and the default test ID attribute. + +## Example + TODO + +## Types of Queries + +- Single Elements + - `get_by...`: Returns the matching node for a query, and returns a descriptive error if no elements match or if more than one match is found (use `get_all_by` instead if more than one element is expected). + - `query_by...`: Returns the matching node for a query, and returns `None` if no elements match. This is useful for asserting an element that is not present. Returns an error if more than one match is found (use `query_all_by` instead if this is OK). + - `find_by...`: Returns a future which resolves when an element is found which matches the given query. The future is rejected if no element is found or if more than one element is found after a default timeout of 1000ms (use `find_all_by` if you need to find more than one element). + - `find_by` methods are a combination of `get_by` queries and [`wait_for`](#). They accept the `wait_for` options as the last argument (i.e. `screen.find_by_text("text", query_options, wait_for_options).await`). +- Multiple Elements + - `get_all_by...`: Returns a vector of all matching nodes for a query, and returns an error if no elements match. + - `query_all_by...`: Returns a vector of all matching nodes for a query, and returns an empty vector (`vec![]`) if no elements match. + - `find_all_by...`: Returns a future which resolves to a vector of elements when any elements are found which match the given query. The future is rejected if no elements are found after a default timeout of 1000ms. + +| Type of Query | 0 Matches | 1 Match | >1 Matches | Retry | +| --------------------- | --------- | ------- | ---------- | ----- | +| **Single Element** | | | | | +| `get_by` | Error | Element | Error | No | +| `query_by` | `None` | Element | Error | No | +| `find_by` | Error | Element | Error | Yes | +| **Multiple Elements** | | | | | +| `get_all_by` | Error | Vector | Vector | No | +| `query_all_by` | `vec![]` | Vector | Vector | No | +| `find_all_by` | Error | Vector | Vector | Yes | + +## Priority + +Based on the [Guiding Principles](https://testing-library.com/docs/guiding-principles/), your test should resemble how users interact with your code (component, page, etc.) as much as possible. With this in mind, we recommend this order of priority: + +1. **Queries Accessible to Everyone** Queries that reflect the experience of visual/mouse users as well as those that use assistive technology. + 1. `get_by_role`: This can be used to query every element that is exposed in the [accessibility tree](https://developer.mozilla.org/en-US/docs/Glossary/Accessibility_tree). With the `name` option you can filter the returned elements by their [accessible name](https://www.w3.org/TR/accname-1.1/). This should be your top preference for just about everything. There's not much you can't get with this (if you can't, it's possible your UI is inaccessible). Most often, this will be used with the `name` option like so: `get_by_role(AriaRole::Button, ByRoleOptions::default().name("submit"))`. Check the list of roles. + 2. `get_by_label_text`: This method is really good for form fields. When navigating through a website form, users find elements using label text. This method emulates that behavior, so it should be your top preference. + 3. `get_by_placeholder_text`: [A placeholder is not a substitute for a label](https://www.nngroup.com/articles/form-design-placeholders/). But if that's all you have, then it's better than alternatives. + 4. `get_by_text`: Outside of forms, text content is the main way users find elements. This method can be used to find non-interactive elements (like divs, spans, and paragraphs). + 5. `get_by_display_value`: The current value of a form element can be useful when navigating a page with filled-in values. +2. **Semantic Queries** HTML5 and ARIA compliant selectors. Note that the user experience of interacting with these attributes varies greatly across browsers and assistive technology. + 1. `get_by_alt_text`: If your element is one which supports `alt` text (`img`, `area`, `input`, and any custom element), then you can use this to find that element. + 2. `get_by_title`: The title attribute is not consistently read by screenreaders, and is not visible by default for sighted users +3. **Test IDs** + 1. `get_by_test_id`: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense (e.g. the text is dynamic). + +## Using Queries + +The base queries from DOM Testing Library require you to pass a `container` as the first argument. Most framework-implementations of Testing Library provide a pre-bound version of these queries when you render your components with them, which means you do not have to provide a container. In addition, if you just want to query `document.body` then you can use the [`screen`](#screen) export as demonstrated below (using `screen` is recommended). + +The primary argument to a query can be a _string_, _regular expression_, or _function_. There are also options to adjust how node text is parsed. See [`Matcher`](#matcher) for documentation on what can be passed to a query. + +Given the following DOM elements (which can be rendered by Dioxus, Leptos, Yew, or plain HTML code): + +```html + +
+ + +
+ +``` + +You can use a query to find an element (by label text, in this case): + +```rust,ignore +use testing_library_dom::{get_by_label_text, Screen, SelectorMatcherOptions}; + +// With screen: +let input_node_1 = Screen::get_by_label_text("Username", SelectorMatcherOptions::default()).expect("Get should succeed."); + +// Without screen, you need to provide a container: +let container = document.query_selector("#app").expect("Query should succeed.").expect("Element should exist."); +let input_node_2 = get_by_label_text(&container, "Username", SelectorMatcherOptions::default()).expect("Get should succeed."); +``` + +### `Options` + +You can pass an `Options` struct instance to the query. See the docs for each query to see available options, e.g. [By Role API](./by-role.md). + +### `screen` + +All of the queries exported by DOM Testing Library accept a `container` as the first argument. Because querying the entire `document.body` is very common, DOM Testing Library also exports a `screen` function which returns a struct that has every query that is pre-bound to `document.body` (using the [`within`](#) functionality). + +Here's how you use it: + + + +```rust,ignore +use testing_library_dom::screen; + +let screen = screen(); +let example_input = screen.get_by_label_text("Example").expect("Get should succeed."); +``` + +## `Matcher` + +Most of the query APIs take a `Matcher` as an argument, which means the argument can be either a _string_, _regex_, or a _function_ of signature `Fn(String, Option<&Element>) -> bool` which returns `true` for a match and `false` for a mismatch. + +### Examples + +Given the following HTML: + +```html +
Hello World
+``` + +**_Will_ find the div:** + +```rust,ignore +// Matching a string: +screen.get_by_text("Hello World"); // Full string match +screen.get_by_text("llo Worl", SelectorMatcherOptions::default().exact(false)); // Substring match +screen.get_by_text("hello world", SelectorMatcherOptions::default().exact(false)); // Ignore case + +// Matching a regex: +screen.get_by_text(Regex::new(r"World")?); // Substring match +screen.get_by_text(Regex::new(r"(?i)world")?); // Substring match, ignore case +screen.get_by_text(Regex::new(r"(?i)^hello world$")?); // Full string match, ignore case +screen.get_by_text(Regex::new(r"(?i)Hello W?oRlD")?); // Substring match, ignore case, searches for "hello world" or "hello orld" + +// Matching with a custom function: +screen.get_by_text(|content, element| => content.starts_with("Hello")); +``` + +**_Will not_ find the div:** + +```rust,ignore +// Full string does not match +screen.get_by_text("Goodbye World"); + +// Case-sensitive regex with different case +screen.get_by_text(Regex::new("hello world")?); + +// Function looking for a span when it's actually a div +screen.get_by_text(|content, element| { + element.is_some_and(|element| element.tag_name().to_lower_case() == "span") && content.starts_with("Hello") +}); +``` + +### Precision + +Queries that take a `Matcher` also accept a struct instance as the final argument that can contain options that affect the precision of string matching: + +- `exact`: Defaults to `true`; matches full strings, case-sensitive. When false, matches substrings and is not case-sensitive. + - It has no effect when used together with regex or function arguments. + - In most cases, using a regex instead of a string combined with `.exact(false)` gives you more control over fuzzy matching so it should be preferred. +- `normalizer`: An optional function which overrides normalization behavior. See [Normalization](#normalization). + +### Normalization + +Before running any matching logic against text in the DOM, Testing Library automatically normalizes that text. By default, normalization consists of trimming whitespace from the start and end of text, and **collapsing multiple adjacent whitespace characters within the string into a single space**. + +If you want to prevent that normalization, or provide alternative normalization (e.g. to remove Unicode control characters), you can provide a `normalizer` function in the options object. This function will be given a string and is expected to return a normalized version of that string. + +> **Note** +> +> Specifying a value for `normalizer` replaces the built-in normalization, but you can call `get_default_normalizer` to obtain a built-in normalizer, either to adjust that normalization or to call it from your own normalizer. + +`get_default_normalizer` takes an options object which allows the selection of behaviour: + +- `trim`: Defaults to `true`. Trims leading and trailing whitespace. +- `collapse_whitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space. + +#### Normalization Examples + +To perform a match against text without trimming: + +```rust,ignore +screen.get_by_text( + "text", + SelectorMatcherOptions::default().normalizer(get_default_normalizer( + DefaultNormalizerOptions::default().trim(false), + )), +); +``` + +To override normalization to remove some Unicode characters whilst keeping some (but not all) of the built-in normalization behavior: + +```rust,ignore +screen.get_by_text( + "text", + SelectorMatcherOptions::default().normalizer({ + let regex = Regex::new(r"[\u200E-\u200F]*")?; + let normalizer = + get_default_normalizer(DefaultNormalizerOptions::default().trim(false)); + + Rc::new(move |text| regex.replace_all(&normalizer(text), "").to_string()) + }), +); +``` + +## Manual Queries + +On top of the queries provided by the testing library, you can use the regular [`query_selector` DOM API](https://docs.rs/web-sys/latest/web_sys/struct.Document.html#method.query_selector) to query elements. Note that using this as an escape hatch to query by `class` or `id` is not recommended because they are invisible to the user. Use a `testid` if you have to, to make your intention to fall back to non-semantic queries clear and establish a stable API contract in the HTML. + + + +```rust,ignore +let foo = document.query_selector('[data-foo="bar"]').expect("Query should succeed.").expect("Element should exist."); +``` diff --git a/book/src/core/queries/by-alt-text.md b/book/src/core/queries/by-alt-text.md new file mode 100644 index 0000000..4be738c --- /dev/null +++ b/book/src/core/queries/by-alt-text.md @@ -0,0 +1,43 @@ +# By Alt Text + +> `get_by_alt_text`, `query_by_alt_text`, `get_all_by_alt_text`, `query_all_by_alt_text`, `find_by_alt_text`, `find_all_by_alt_text` + +## API + +```rust,ignore +use testing_library_dom::{Matcher, QueryError}; +use web_sys::HtmlElement; + +fn get_by_alt_text>( + // If you're using `screen`, then skip the container argument: + container: &HtmlElement, + matcher: M, + options: MatcherOptions, +) -> Result; + +struct MatcherOptions { + exact: Option, + normalizer: Option>, +} + +type NormalizerFn = dyn Fn(String) -> String; +``` + +This will return the element (normally an ``) that has the given `alt` text. Note that it only supports elements which accept an `alt` attribute or [custom elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) (since we don't know if a custom element implements `alt` or not): ``, ``, and `` (intentionally excluding `` as it's deprecated). + +```html +Incredibles 2 Poster +``` + + + +```rust,ignore +use testing_library_dom::screen; + +let screen = screen(); +let incredibles_poster_img = screen.get_by_alt_text(Regex::new(r"(?i)incredibles.*? poster")?); +``` + +## Options + +[`Matcher` options](./about-queries.md#precision). diff --git a/book/src/core/queries/by-display-value.md b/book/src/core/queries/by-display-value.md new file mode 100644 index 0000000..24679e9 --- /dev/null +++ b/book/src/core/queries/by-display-value.md @@ -0,0 +1,90 @@ +# By Display Value + +> `get_by_display_value`, `query_by_display_value`, `get_all_by_display_value`, `query_all_by_display_value`, `find_by_display_value`, `find_all_by_display_value` + +## API + +```rust,ignore +use testing_library_dom::{Matcher, QueryError}; +use web_sys::HtmlElement; + +fn get_by_display_value>( + // If you're using `screen`, then skip the container argument: + container: &HtmlElement, + matcher: M, + options: MatcherOptions, +) -> Result; + +struct MatcherOptions { + exact: Option, + normalizer: Option>, +} + +type NormalizerFn = dyn Fn(String) -> String; +``` + +Returns the `input`, `textarea`, or `select` element that has the matching display value. + +### `input` tags + +```html + +``` + +```rust,ignore +document.get_element_by_id("lastName")?.set_value("Norris"); +``` + + + +```rust,ignore +use testing_library_dom::screen; + +let screen = screen(); +let last_name_input = screen.get_by_display_value("Norris"); +``` + +### `textarea` tags + +```html + + +``` + +```rust,ignore +let input_node = screen.get_by_label_text("Username", SelectorMatcherOptions::default().selector("input")); +``` + +> \*\*Note +> +> `get_by_label_text` will not work in the case where a `for` attribute on a `