selene icon indicating copy to clipboard operation
selene copied to clipboard

Easiest way to wait for existence of a set of elements, where only one is required to appear

Open vfalco02 opened this issue 9 months ago • 3 comments

I've been trying to figure this out on my own, but I wanted to see if there was a built-in way for Selene to do this. If there is, I could not find anything to definitively tell me what to do.

Basically I have a few elements:

foo = s("#foo")
bar = s("#bar")
baz = s("#baz")

I want to wait until any of them are visible without using my own loop. I also don't want to have to wait for one element to not be present before looking for the other.

What i want to do:

browser.wait_until(foo.should(be.visible) or bar.should(be.visible) or baz.should(be.visible)

or

foo.should(be.visible).or_(bar.should(be.visible).or_(baz.should(be.visible)))

The only thing I can think of uses loops and sleeps:

for _ in range(16):
    if foo.with_(timeout=0).wait_until(be.visible) or bar.with_(timeout=0).wait_until(be.visible) or baz.with_(timeout=0).wait_until(be.visible):
        break
    time.sleep(.25)
else:
    raise Exception("None found")

vfalco02 avatar Mar 17 '25 19:03 vfalco02

hm... Good question... Seems like I have used some kind of Ok-workaround for such cases in the past... But can't recall which... I will try to recall... I will also think what can be a predefined feature in Selene for that.

Meanwhile, you can use something like this as a workaround:

ss("#foo, #bar, #baz").should(have.no.size(0))

yashaka avatar Mar 17 '25 21:03 yashaka

This is the implementation I'm going with:

def wait_until_one_of_visible(elements: List[Element], timeout=4):
    """Waits until one of the elements are visible

    Args:
        elements (List[Element]): The elements to check
        timeout: Amount of time to wait
    """
    for _ in range(int(timeout)*4):
        found_elements = [element for element in elements if element.matching(be.visible)]
        if found_elements:
            return found_elements[0]
        time.sleep(.25)
    else:
        raise TimeoutException(f"None of the elements met the condition within {timeout} seconds")

While your suggestion works, I store my locators as Elements and I'm not storing the locator strings separately...therefore to do what you suggested I would need to pull the locators out of the elements and build out the locator string.

vfalco02 avatar Mar 18 '25 13:03 vfalco02

Yes, my proposed workaround is pretty awkward:) I also don't practice storing selectors separately...

Yet, there should be a better way to implement what you want (wihtout additional wait_until_one_of_visible) logic. I mentioned such way here: https://github.com/yashaka/selene/issues/578, as browser.all(located(foo, bar, baz)) where foo, bar, baz - are Element instances.

for that, you need to implement a located method, that will return a Locator class instance built with a location logic that will be simply calling .locate() on all passed elements and combining results into list of WebElements to return. I will provide an actual implementation later, if you don't find out how to do it on your own...

yashaka avatar Mar 18 '25 14:03 yashaka