Tutorials

Tutorials

Testing Stealth Against Anti-Bot Detection Sites

AntibotTester walks a list of bot-detection pages, captures a sequence of screenshots on each, and reacts to a few page-specific quirks along the way. It demonstrates nesting BrowserType and AutomationEngine loops with continue-based filtering, a global CallIfNotFound handler with an ignore list, and per-site conditional logic driven by UrlHelper.AreEquivalent.

Complete Program

Here's a trimmed-down version of the workflow, start to finish. Each piece is broken down and explained below.

using GenerallyPositive;

using GenerallyPositive.Browser;

using static GenerallyPositive.Enums;

GPAL

.CallIfNotFound(GenericCallIfNotFound)

.WithUseOttoMagic(@"C:magic")

.WithPublishToConsole()

.WithAutoUpdateWebDriver();

List<string> urls = new List<string>

{

"https://bot-detection.example.com/score",

"https://fingerprint.example.com/check",

"https://example.com/cookie-wall"

};

IBrowser browser = GPAL.Browser

.WithBrowserType(BrowserType.Chrome)

.WithUseStealth(StealthType.DarkMode | StealthType.GoogleReferrer | StealthType.PatchChromeDriver | StealthType.CDP)

.WithLoadImages(true)

.WithWindowSize(new System.Drawing.Rectangle(10, 10, 2000, 2000))

.WithDriverLocation(@"C:drivers")

.WithUseAutomationEngine(AutomationEngine.OttoMagic)

.WithProfileDataDirectory(@"C:ProfilesStealth")

.ToGPALObject();

foreach (string url in urls)

{

browser.GoTo(url);

browser.WaitFor(5_000);

if (true == UrlHelper.AreEquivalent(url, "https://example.com/cookie-wall"))

{

browser.WaitFor(7_000);

browser.LeftClick("#cookie-accept-all");

}

for (int page = 0; page < 10; page++)

{

string fileName = $@"pixpage_{page}.PNG";

GPALFile screenCapFile = GPAL.File.WithFileName(fileName).ToGPALObject();

GPAL.CaptureScreen(browser).SaveToFile(screenCapFile);

_ = browser.PageDown;

if (0 < page && true == browser.IsEndOfPage())

break;

}

}

browser.Close(true);

public static CallIfStatus GenericCallIfNotFound(IBrowser browser, List<GPALElement> foundElements, List<GPALElement> matchedElements, Selector selector, bool matchedAll)

{

string message = $"Unable to locate Selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";

GPAL.PublishSimpleEvent(GPALEventType.ERROR, message, browser, GPALObjectType.Browser);

return CallIfStatus.Handled;

}

A Global CallIfNotFound Handler

GPAL.CallIfNotFound registers a workflow-wide handler that fires whenever a selector fails to match, on top of any per-selector CallIfNotFound. Here it logs an ERROR event with the selector's name and first path, then returns CallIfStatus.Handled so the workflow continues instead of throwing.

GPAL

.CallIfNotFound(GenericCallIfNotFound)

.WithUseOttoMagic(@"C:magic")

.WithPublishToConsole()

.WithAutoUpdateWebDriver();

public static CallIfStatus GenericCallIfNotFound(IBrowser browser, List<GPALElement> foundElements, List<GPALElement> matchedElements, Selector selector, bool matchedAll)

{

string message = $"Unable to locate Selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";

GPAL.PublishSimpleEvent(GPALEventType.ERROR, message, browser, GPALObjectType.Browser);

return CallIfStatus.Handled;

}

TIP

The real test program keeps a List<Selector> of selectors that are expected to be missing on some sites, and checks selector.Name against it before logging - so genuinely optional selectors don't spam the error log.

Stealth, Window Size, and a Persistent Profile

WithUseStealth combines several flags - dark mode, a Google referrer header, a patched chromedriver binary, and CDP leak protection - to make the browser look less like an automated session. WithProfileDataDirectory points at a real Chrome profile directory, so cookies, history, and fingerprint-relevant state persist between runs instead of starting from a blank slate every time.

IBrowser browser = GPAL.Browser

.WithBrowserType(BrowserType.Chrome)

.WithUseStealth(StealthType.DarkMode | StealthType.GoogleReferrer | StealthType.PatchChromeDriver | StealthType.CDP)

.WithLoadImages(true)

.WithWindowSize(new System.Drawing.Rectangle(10, 10, 2000, 2000))

.WithDriverLocation(@"C:drivers")

.WithUseAutomationEngine(AutomationEngine.OttoMagic)

.WithProfileDataDirectory(@"C:ProfilesStealth")

.ToGPALObject();

TIP

StealthType is a flags enum - combine as many as the target site needs with bitwise OR. See Browser Profiles: Signed-In vs Temporary for more on persistent profile directories.

Per-Site Conditional Handling

Different anti-bot pages need different one-off handling: some need extra wait time before a fingerprint check finishes, others need a cookie banner dismissed before screenshots make sense. UrlHelper.AreEquivalent compares the current URL against a known target regardless of trailing slashes or minor formatting differences, so each branch only runs for the site it applies to.

if (true == UrlHelper.AreEquivalent(url, "https://example.com/cookie-wall"))

{

browser.WaitFor(7_000);

browser.LeftClick("#cookie-accept-all");

}

Capture a Scroll-Through Sequence

The inner loop takes up to 10 screenshots, scrolling down with browser.PageDown between each. GPAL.CaptureScreen(browser).SaveToFile writes each frame to a GPALFile-backed PNG, and browser.IsEndOfPage() stops the loop early once scrolling has reached the bottom.

for (int page = 0; page < 10; page++)

{

string fileName = $@"pixpage_{page}.PNG";

GPALFile screenCapFile = GPAL.File.WithFileName(fileName).ToGPALObject();

GPAL.CaptureScreen(browser).SaveToFile(screenCapFile);

_ = browser.PageDown;

if (0 < page && true == browser.IsEndOfPage())

break;

}

WARNING

Programmatic scrolling is itself a signal some anti-bot systems watch for. If a site's detection score changes after scrolling, that's the page reacting to PageDown, not a GPAL bug.

Nested BrowserType / AutomationEngine Loops

The real test program wraps all of this in nested foreach loops over BrowserType and AutomationEngine, using continue inside a switch to skip combinations that aren't relevant to the current test run (for example, skipping every engine except OttoMagic). This is a common pattern for regression-testing a workflow across every supported configuration without duplicating the workflow code.

Array browserTypes = Enum.GetValues(typeof(BrowserType));

foreach (BrowserType bt in browserTypes)

{

switch (bt)

{

case BrowserType.Chrome: break;

default: continue;

}

Array automationEngines = Enum.GetValues(typeof(AutomationEngine));

foreach (AutomationEngine ae in automationEngines)

{

switch (ae)

{

case AutomationEngine.OttoMagic: break;

default: continue;

}

// build browser and run the workflow body here

}

}

TIP

A switch with continue for every value you want to skip keeps the loop body focused on the combinations you actually care about, and makes it obvious at a glance which engines and browser types are in scope for a given test run. See Automation Engines and Conditional Logic.