stevepryde/thirtyfour

Feature Request | select_by_visible_partial_text method on SelectElement

bcpeinhardt opened this issue · 11 comments

Hello!
I'm testing automations on a dev site before running them on a live site, and of course there are discrepancies between the sites, including Selects which have slightly different text for some reason. I don't really want to have to check the url to Select the web element correctly. Would you consider adding a select_element_by_visible_partial_text method to the SelectElement struct?

How would this work?

Note that the current implementation of SelectElement::select_by_visible_text() will already select options by partial text if no exact match exists. That seems to be what you're asking for.

But you don't even need to use SelectElement at all. You can just query for the element, then query for any option tags below it, with the text you want (XPath will do this, or you can use query() with filters which is less efficient but will still get you there).

So the method set_selection_by_visible_text on SelectElement honestly confuses the hell out of me. I think it's splitting by whitespace though?

I tested my theory about spacing with the following example:

use thirtyfour::prelude::*;
use tokio;
use thirtyfour::components::select::SelectElement;

#[tokio::main]
async fn main() -> WebDriverResult<()> {

     let caps = DesiredCapabilities::firefox();
     let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;

     driver.get("https://www.htmlquick.com/reference/tags/select.html").await?;
     let sport_select_elm = driver.query(By::Name("sport")).first().await?;
     let sport_select_wrapper = SelectElement::new(&sport_select_elm).await?;

     // Doesn't work
     sport_select_wrapper.select_by_visible_text("Ten").await?;

     Ok(())
}

I can absolutely just click the option I'm going for and select it with a custom predicate, but the developers on this website did some wonky z-level something or another so when selenium clicks the select to display the options they come crashing to the top of the screen and I mean TO THE TOP (I keep the program running in another workspace and the options pop up on top of whatever windows I have open, I honestly don't know how they did it but I'd be very interested to find out.)

Anyway, my need is obviously silly but I can see a use case behind a true substring method and a split by whitespace method as separate entities, so I thought it was worth an issue to see what you thought.

As for how it would work which I totally forgot to answer, I think something like .//option[contains(text(), user_text)]

The set_selection_by_visible_text() method (and indeed that whole module) is basically an exact port from the python selenium code. I don't particularly like the logic of it and I don't think it's particularly helpful.

I'm going to add two new methods for exact match and partial match. Hopefully this both solves your issues and provides more value in general.

@bcpeinhardt I've added some new methods on the select-partial branch. Are you able to try these out and see if they meet your needs?

I also included a select_by_query() method as well which allows you to supply any selector you want - but I have mixed feelings about that. It feels too permissive and there's no guarantee the selector you pass in will even match option elements at all. So I'll probably remove that particular method.

But the partial and exact match versions should do what you need.

@stevepryde I got an error trying to use select_by_exact_text, which I think is simply a small typo in the XPath:
.//option[text() = {})] I think needs the closing parentheses removed .//option[text() = {}]. But I certainly find these methods to be a better more explicit solution.
select_by_query also is very convenient! I think performing the query on the select like you are is a good limitation, but god knows I've been saved many times by api designers smart enough to not give me the option to use it incorrectly so I agree with the sentiment that less chance to misuse it is better, especially on a convenience struct like this where if one needs something more complicated they should just use a custom query (Yes I know that I didn't take my own advice there). Maybe something like select_by_option_xpath that does something like format!("./option{}", user_provided_path). Is that too weird? I think it strikes a decent balance between abstracting away having to select the element, ensuring you're selecting a direct child option, and giving the user all the power of xpath to work around their weird needs.

Tangent:
Something to think about for re-usability and a sort of property based testing approach might be incorporating some regex into different convenience methods. I don't know when and how regex has been incorporated into this or other selenium bindings in the past but in the age of component based design I can see it being pretty handy. I have no idea how something like that would be implemented effectively (i.e not making a selenium request for each pattern matching the regex submitted) but its just an "It'd be kinda cool if unicorns were real" sort of thought. Maybe as a predicate on queries? I don't know how the querying works under the hood but my understanding is that there's an original query with the locator then the predicates get evaluated?

@bcpeinhardt I've updated the branch.

  • Removed the by_query methods
  • Added by_xpath_condition methods which does .//option[{}] (where the caller specifies {})

This should hopefully solve the problems while allowing flexibility with less potential to get it wrong.

As for using regexes to filter elements etc, you can already do this with the ElementQuery interface (driver.query() and element.query()). Look for anything that accepts a Needle like this:

pub fn with_text<N>(self, text: N) -> Self
    where
        N: Needle + Clone + Send + Sync + 'static,

Needle comes from the StringMatch crate (which can be used anywhere - it isn't specifically for thirtyfour) and basically allows string matching by substring, exact match, or regex anywhere that you need to do a string match.

I've since learned that Rust's own Pattern trait might actually fill (some/most/all of) the same need, so this is an area to explore some more: https://doc.rust-lang.org/std/str/pattern/trait.Pattern.html

It should be noted that those regexes only work by retrieving all elements and then filtering based on some attribute of each element, which requires an additional WebDriver request per element. This means XPath is potentially far more efficient if you can use it, but I don't think XPath supports regexes, at least with 1.x (and WebDriver does not support XPath 2.x).

So for now the ElementQuery interface with filtering is probably your best bet if you want to use regexes to query/filter elements. Also see ElementWaiter for when you already have an element and want to wait until it disappears or until some attribute changes to a particular value.

This is awesome, thank you! It would shock you how much time these features for the SelectElement are going to save me haha. Also that's great that regex is an option, I know that's going to be really useful for me soon, so I'm glad I asked! Love this crate, please keep up the awesome work!!

Added in v0.27.3