stevepryde/thirtyfour

Concept: Component Wrappers

stevepryde opened this issue · 2 comments

This is an idea for providing an ergonomic way to test web components, or pages, similar to a page-object-model.

Suppose you have a React component (or similar) that provides a text input field and a button, and you want to create a wrapper to test it.
You have a selector for the HTML element that contains the component, and then individual selectors below that to find the text field and the button. But rather than query every element upfront, you want to only query them when needed, but then you want to cache the elements to avoid repeatedly querying them. Now suppose one of the elements is re-rendered, causing it to go "stale" in selenium terms. We now want to re-query that element from the base element (assuming the base element was not also re-rendered).

The individual selectors can be provided as follows (I haven't tested this):

pub struct ElementSelector {
    base_element: WebElement,
    by: By,
    element: Option<WebElement>
}

impl ElementSelector {
    pub fn new(base_element: WebElement, by: By) -> Self {
        Self { base_element, by }
    }

    pub async fn resolve(&mut self) -> WebDriverResult<&WebElement> {
        if let Some(elem) = &self.element {
            return Ok(elem);
        }

        let elem = base_element.find_element(self.by).await?;
        self.element = Some(elem);
        let Some(elem) = &self.element;
        Ok(elem)
    }

    pub async fn requery(&mut self) -> Option<WebElement> {
        self.element = None;
        self.resolve().await
    }
}

With something like the above, you could create a wrapper that looks like this:

pub struct MyComponentWrapper {
    base: WebElement,
    text_field: ElementSelector,
    button: ElementSelector,
}

impl MyComponentWrapper {
    pub async fn new(base: WebElement) -> Self {
        let text_field = ElementSelector::new(base.clone(), By::Name("text-field"));
        let button = ElementSelector::new(base.clone(), By::Tag("button"));
        Self {
            base,
            text_field,
            button,
        }
    }
}

And now we can use the wrapper by resolving elements and using them. We could even provide wrappers to ElementSelector to expose WebElement methods and automatically resolve the element first.

Now for the best part. The above can be improved via a derive macro, so that it looks more like this:

#[derive(Component)]
pub struct MyComponentWrapper {
    #[base]
    base: WebElement,
    #[by(name = "text-field")]
    text_field: ElementSelector,
    #[by(tag = "button")]
    button: ElementSelector,
}

And now the rest of the code can be auto-generated for us.

Additional features that could be supported include:

  • Selectors that return Vec<WebElement>
  • Ensure a selector only returns 1 element (via unique attribute)
  • Provide TryFrom impl from the parent component? or a trait that provides this functionality
  • Automatically requery the element and retry the operation if element is stale
  • Specify timeout overrides for elements that might require a longer wait

ElementResolver has been pushed to main and some tests added.
In order to add the derive macro the repo will be converted to a workspace with thirtyfour and thirtyfour_macros being child crates.

The manual code version of the component wrapper is now possible. Adding a derive macro will make this really nice.
Being able to say "click this element or if it's stale, refind it then click it" is really powerful. Same with fetching attributes etc.

For anyone curious, this provides similar functionality to the "page object model" paradigm, but you can apply it to any component on the page. If you're dealing with styled buttons, input fields, etc, then you can create a handy wrapper for that component with custom methods, and now you can wrap the base element with your custom type any time you need to interact with that element.

I will provide examples once the macro is working.

This is implemented and working in main and will be part of the next major release (v0.31.0)