thirtyfour_macros/
lib.rs

1//! Thirtyfour is a Selenium / WebDriver library for Rust, for automated website UI testing.
2//!
3//! This crate provides proc macros for use with [thirtyfour](https://docs.rs/thirtyfour).
4//!
5//!
6
7use crate::component::expand_component_derive;
8use proc_macro::TokenStream;
9use syn::{parse_macro_input, DeriveInput};
10
11mod component;
12
13/// Derive macro for a wrapped `Component`.
14///
15/// A `Component` contains a base [`WebElement`] from which all element queries will be performed.
16///
17/// All elements in the component are descendents of the base element (or at least the
18/// starting point for an element query, since XPath queries can access parent nodes).
19///
20/// Components perform element lookups via [`ElementResolver`]s, which lazily perform an
21/// element query to resolve a [`WebElement`], and then cache the result for later access.
22///
23/// See the [`ElementResolver`] documentation for more details.
24///
25/// ## Attributes
26///
27/// ### `#[base]`
28/// By default, the base element should be named `base` and be of type `WebElement`.
29/// You can optionally use the `#[base]` attribute if you wish to name the base element
30/// something other than `base`.
31/// If you use this attribute, you cannot also have another
32/// element named `base`.
33///
34/// ### `#[by(..)]`
35/// Components use the `#[by(..)]` attribute to specify all the details of the query.
36///
37/// The only required attribute is the selector attribute, which can be one of the following:
38/// - `id = "..."`: Select element by id.
39/// - `tag = "..."`: Select element by tag name.
40/// - `link = "..."`: Select element by link text.
41/// - `css = "..."`: Select element by CSS.
42/// - `xpath = "..."`: Select element by XPath.
43/// - `name = "..."`: Select element by name.
44/// - `class = "..."`: Select element by class name.
45///
46/// Optional attributes available within `#[by(..)]` include:
47/// - `single`: (default, single element only) Return `NoSuchElement` if the number of elements
48///             found is != 1.
49/// - `first`: (single element only) Select the first element that matches the query.
50///            By default, a query will return `NoSuchElement` if multiple elements match.
51///            This default is designed to catch instances where a query is not specific enough.
52/// - `not_empty`: (default, multi elements only) Return `NoSuchElement` if no elements were found.
53/// - `allow_empty`: (multi elements only) Return an empty Vec if no elements were found.
54///                  By default a multi-element query will return `NoSuchElement` if no
55///                  elements were found.
56/// - `description = "..."`: Set the element description to be displayed in `NoSuchElement` errors.
57/// - `allow_errors`: Ignore errors such as stale elements while polling.
58/// - `wait(timeout_ms = 10000, interval_ms=500)`: Override the default polling options.
59/// - `nowait`: Turn off polling for this element query.
60/// - `custom = "my_resolve_fn"`: Use the specified function to resolve the element or component.
61///                      **NOTE**: The `custom` attribute cannot be specified with any other
62///                      attribute.
63///
64/// See [`ElementQueryOptions`] for more details on how each option is used.
65///
66/// ### Custom resolver functions
67///
68/// When using `custom = "my_resolve_fn"`, your function signature should look something like this:
69///
70/// ```ignore
71/// async fn my_resolve_fn(elem: &WebElement) -> WebDriverResult<T>
72/// ```
73///
74/// where the `T` is the same as type `T` in `ElementResolver<T>`.
75/// Also see the example below.
76///
77/// ## Example:
78/// ```ignore
79/// /// This component shows how to nest components inside others.
80/// #[derive(Debug, Clone, Component)]
81/// pub struct CheckboxSectionComponent {
82///     base: WebElement,
83///     #[by(tag = "label", allow_empty)]
84///     boxes: ElementResolver<Vec<CheckboxComponent>>,
85///     // Other fields will be initialised using `Default::default()`.
86///     my_field: bool,
87/// }
88///
89/// /// This component shows how to wrap a simple web component.
90/// #[derive(Debug, Clone, Component)]
91/// pub struct CheckboxComponent {
92///     base: WebElement,
93///     #[by(css = "input[type='checkbox']", first)]
94///     input: ElementResolver<WebElement>,
95///     #[by(name = "text-label", description = "text label")]
96///     label: ElementResolver<WebElement>
97///     #[by(custom = "my_custom_resolve_fn")]
98///     button: ElementResolver<WebElement>
99/// }
100///
101/// /// Use this function signature for your custom resolvers.
102/// async fn my_custom_resolve_fn(elem: &WebElement) -> WebDriverResult<WebElement> {
103///     // Do something with elem.
104///     elem.query(By::ClassName("my-class")).and_displayed().first().await
105/// }
106///
107/// impl CheckboxComponent {
108///     /// Return true if the checkbox is ticked.
109///     pub async fn is_ticked(&self) -> WebDriverResult<bool> {
110///         // Equivalent to: let elem = self.input.resolve_present().await?;
111///         let elem = resolve_present!(self.input);
112///         let prop = elem.prop("checked").await?;
113///         Ok(prop.unwrap_or_default() == "true")
114///     }
115/// }
116/// ```
117/// [`WebElement`]: https://docs.rs/thirtyfour/latest/thirtyfour/struct.WebElement.html
118/// [`ElementResolver`]: https://docs.rs/thirtyfour/0.31.0-alpha.1/thirtyfour/components/struct.ElementResolver.html
119/// [`ElementQueryOptions`]: https://docs.rs/thirtyfour/0.31.0-alpha.1/thirtyfour/extensions/query/struct.ElementQueryOptions.html
120/// [`ElementQueryFn<T>`]: https://docs.rs/thirtyfour/0.31.0-alpha.1/thirtyfour/common/types/type.ElementQueryFn.html
121
122macro_rules! bail {
123    ($span: expr, $($fmt:tt)*) => {
124        return Err(syn::Error::new($span, format_args!($($fmt)*)))
125    };
126}
127
128pub(crate) use bail;
129#[proc_macro_derive(Component, attributes(base, by))]
130pub fn derive_component_fn(input: TokenStream) -> TokenStream {
131    let ast = parse_macro_input!(input as DeriveInput);
132    expand_component_derive(ast).into()
133}