Core Concepts

Persistent Selectors: Handling Nags and Popups

A persistent selector is checked on every unit of work for the life of the session. Use it for things that can pop up at any time and aren't part of the workflow itself, like cookie-consent banners or session-timeout dialogs.

Selectors That Can Appear at Any Time

WithPersistentSelector adds a selector to a special unit of work that GPAL checks alongside every regular unit of work for the lifetime of the session. Pair it with PersistentCallIfFound and PersistentCallIfNotFound to react when the selector matches or doesn't. This is the right tool for nags not part of your workflow's logic -- cookie banners, rate-our-site popups, session-expired modals -- things that might appear after step 2, or step 20, or never.

// Dismiss a cookie banner whenever it appears, for the rest of the session

var cookieBanner = GPAL.Selector

.WithCSS("#cookie-consent .accept-all")

.WithSelectorName("Cookie Consent Accept")

.CallIfFound((browser, uow, elements) => {

elements[0].Click();

return CallIfStatus.Handled;

})

.ToGPALObject();

GPAL.Browser

.GoTo("https://example.com")

.WithPersistentSelector(cookieBanner);

TIP

Persistent selectors run before the UOW selectors at each step -- they are checked first because a persistent element could get in the way of the UOW selector finding or interacting with the target. This is most pronounced with Selenium, where an element sitting in front of the target can block a click or fail a visibility check. It applies to image matching too: a persistent overlay on top of the target changes what the image matcher sees. They do not run on a timer or in a background thread; they run as part of GPAL's step-to-step bookkeeping.

One-Time Nags vs Recurring Nags

Not every nag needs to stay registered. A cookie banner usually only appears once per session -- once you've dismissed it, there's no reason to keep checking for it. Call selector.Remove() from inside the CallIfFound handler to remove that selector from the persistent unit of work after it's done its job. For handlers that were attached at multiple scopes (selector, UOW, and global), or that you registered as a named method and want gone everywhere at once, call browser.RemoveCallIfHandlerEverywhere(handler) -- it strips that delegate from every CallIfFound and CallIfNotFound list it was added to.

int FoundCookieBanner(Browser browser, UnitOfWork uow, List<GPALElement> elements)

{

elements[0].Click();

// one-time nag - stop checking for it

uow.CurrentSelector.Remove();

// and remove this handler from anywhere else it might be registered

browser.RemoveCallIfHandlerEverywhere(FoundCookieBanner);

return CallIfStatus.Handled;

}

var cookieBanner = GPAL.Selector

.WithCSS("#cookie-consent .accept-all")

.CallIfFound(FoundCookieBanner)

.ToGPALObject();

GPAL.Browser

.GoTo("https://example.com")

.WithPersistentSelector(cookieBanner);

Nags Inside an Iframe: InPersistentIframe

By default, a persistent selector is checked against the top-level document only. If the nag renders inside an iframe -- a third-party cookie-consent widget, an embedded chat popup -- chain InPersistentIframe(iframeSelector) onto WithPersistentSelector. On every check, GPAL first locates that iframe in the main document, then searches for the persistent selector inside it. If the iframe isn't present for a given check, that pass is simply skipped.

// The cookie banner lives inside an iframe, not the main document

var iframeSelector = GPAL.Selector.WithCSS("#consent-frame").ToGPALObject();

var cookieBanner = GPAL.Selector

.WithCSS("#cookie-consent .accept-all")

.CallIfFound((browser, uow, elements) => {

elements[0].Click();

return CallIfStatus.Handled;

})

.ToGPALObject();

GPAL.Browser

.GoTo("https://example.com")

.WithPersistentSelector(cookieBanner)

.InPersistentIframe(iframeSelector);

TIP

With OttoMagic, a persistent selector searches all frames by default -- including the target frame -- so InPersistentIframe is optional and the element will still be found without it. On sites with hundreds of frames, that unbounded search is slow. InPersistentIframe limits OttoMagic to content frame zero, keeping persistent selector checks fast. Other automation engines may handle frame scoping differently.

WARNING

Pass a selector that finds the iframe element itself. Not the element inside it. The iframe selector is always looked up in the main document; only the persistent selector's own search is redirected into that iframe.