selenium icon indicating copy to clipboard operation
selenium copied to clipboard

[🐛 Bug]: [dotnet] Unable to set dictionary to the UnhandledPromptBehavior option

Open JuliaNaumetsss opened this issue 5 months ago • 27 comments

Description

Starting from Chrome 137 it's unable to manage browser prompts (alerts) using BiDi driver. Issues on ChromeDriver were created, but they were closed as intendent behavior: https://issues.chromium.org/issues/425390962 https://issues.chromium.org/issues/428153658

Based on the specification we should provide unhandledPromptBehavior as a dictionary. Based on the comment in the issue it's possible to do it for java, using MutableCapabilities, but I didn't find any solution for c#.

In my project I use Selenium 4.23 + .net 4.7.2. I tried the following code:

var promptBehavior = new Dictionary<string, string>
{
				{"alert", "ignore"},
				{"confirm", "ignore"},
				{"prompt", "ignore"},
				{"beforeUnload", "ignore"},
				{"default", "ignore"}
};
options.AddAdditionalOption("unhandledPromptBehavior", promptBehavior);

But exception is thrown : System.ArgumentException: 'There is already an option for the unhandledPromptBehavior capability. Please use the UnhandledPromptBehavior property instead. Parameter name: capabilityName'

I can't set dictionary to the options.UnhandledPromptBehavior either, because it's waiting for the UnhandledPromptBehavior map (for example, UnhandledPromptBehavior.Ignore).

Before Selenium 4 we were able to set capabilities via DesiredCapabilities, but in Selenium 4.23 this class is internal.

So, it seems, that there are no possibility to set dictionary to the unhandledPromptBehavior option when starting Chrome Driver.

Note: everything worked fine till Chrome 137.

Reproducible Code

1. Start Chrome 137+ using BiDi ChromeDriver and use the following Chrome options:
options.UseWebSocketUrl = true;
options.UnhandledPromptBehavior = UnhandledPromptBehavior.Ignore;

Actual result: browser alert is not shown
Expected result: alert is shown, because UnhandledPromptBehavior was set to Ignore. 

Possible reason: value for UnhandledPromptBehavior is not a dictionary.

JuliaNaumetsss avatar Jul 29 '25 11:07 JuliaNaumetsss

@JuliaNaumetsss, thank you for creating this issue. We will troubleshoot it as soon as we can.

Selenium Triage Team: remember to follow the Triage Guide

selenium-ci avatar Jul 29 '25 11:07 selenium-ci

⚠️ You reported using Selenium version 4.23.0, but the latest release is 4.34.

Please verify that this issue still occurs with the latest version. If it no longer applies, you can close this issue or update your comment.

This issue will be marked "awaiting answer" and may be closed automatically if no response is received.

github-actions[bot] avatar Jul 29 '25 11:07 github-actions[bot]

Please also add a title to this issue.

cgoldberg avatar Jul 29 '25 23:07 cgoldberg

Interesting, just subscribing on it..

nvborisenko avatar Jul 30 '25 20:07 nvborisenko

Hi Selenium team,

Since Chrome 137, alerts don’t show when using BiDi driver because Selenium C# bindings don’t allow setting unhandledPromptBehavior as a dictionary (only a single enum value). This breaks our project’s alert handling completely.

The existing workaround for Java (using MutableCapabilities) doesn’t exist in C#. Without this fix, we can’t properly automate browser prompts on the latest Chrome.

Please prioritize a fix or support for dictionary-style unhandledPromptBehavior in Selenium C#. Alternatively, please consider the existing third-party PR as a possible fix for this issue — it would help unblock our work.

Thanks!

tgolovach93-png avatar Oct 14 '25 17:10 tgolovach93-png

The prompt handler type is indeed a dictionary, and it can be used with session.new or browser.CreateUserContext commands.

When starting a session with a driver (as Selenium does), the driver is executing the session.new command on the user's behalf. The driver is taking the prompt behavior specified in the webdriver-classic compliant options and converting it to the structure required by bidi on the user's behalf. The Options class is compatible with WebDriver-classic spec where UnhandledPromptBehavior Capabilities takes a string object.

I haven't tried browser.CreateUserContext to see if the dictionary works, you might give that a go, but the Options class is taking the right parameter and doing the right thing with it.

titusfortner avatar Oct 24 '25 19:10 titusfortner

https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior

@titusfortner the spec has been changed, now UnhandledPromptBehavior might be a string or map.

UPD: In Java it is possible to provide UnhandledPromptBehavior as Dictionary, but not in .NET/C#. This issue is about it.

nvborisenko avatar Oct 24 '25 20:10 nvborisenko

The issue raised is that they can't handle events with a String in Options, and they can. Here are logs for it: https://gist.github.com/titusfortner/4ae0f7c5e17a93dbf6d7acceb38581e3

The classic spec says "string" so I don't think think we should support Dictionary. If someone wants to use the dictionary, I think they need to browser.CreateUserContext.

titusfortner avatar Oct 24 '25 20:10 titusfortner

If you want to create a MutableOptions instance in .NET for this implementation I guess go for it, but I don't think it should be allowed by a direct method in the standard Options class so long as that class must also support webdriver-classic.

titusfortner avatar Oct 24 '25 20:10 titusfortner

The confusion may be that the chrome implementation of unhandledpromptbehavior isn't actually correct, but it should work for ignore behavior when explicitly setting it to "ignore" so I'm again not sure what the problem is with what can be done right now.

titusfortner avatar Oct 24 '25 20:10 titusfortner

If value is not a string, an implementation that does not also support [WebDriver-BiDi] may return error with error code invalid argument.

If value is a string set value to the map «["fallbackDefault" → value]» and set is string value to true.

Let user prompt handler be an empty map.

W3C WebDriver spec supports both string and map. That's it.

nvborisenko avatar Oct 24 '25 20:10 nvborisenko

Selenium isn't responsible for deserializing. Selenium is responsible for providing the correct types to the webdriver-classic capabilities Which is a string. If BiDi supports it, that's great, but unless you want to create a BiDiChromeOptions class, don't put it directly in Options or else it will cause other problems.

titusfortner avatar Oct 24 '25 20:10 titusfortner

The issue I have here is that the reporter of the issue isn't complaining that there is something in the dictionary functionality that they want, just that they can't get the behavior they expect. And the behavior they expect is working as intended.

titusfortner avatar Oct 24 '25 20:10 titusfortner

Why in Java it is still possible to put Map as UnhandledPromptBehavior? In .NET it is impossible. I thought we should be aligned.

nvborisenko avatar Oct 24 '25 20:10 nvborisenko

Java has a MutableCapabilities object that lets you add literally whatever you want. .NET has always been much more protective. If you want to create an object that lets a user literally pass in whatever they want, go for it. I don't think it is necessary or the most important thing. Maybe there will be other places that need more flexibility at some point, I just don't see much value right now so long as the classic way is working. (at least first double check it is actually working in .NET with latest Chrome). It's working in Ruby, so I know Chrome 141 is doing the right thing.

titusfortner avatar Oct 24 '25 20:10 titusfortner

Users complain. You say everything is good. Hm.

nvborisenko avatar Oct 24 '25 22:10 nvborisenko

I think it's probable that Chrome has fixed something if it is working for me now and there are multiple issues that it isn't. Or there's something special about what Java/.NET are doing, but that's less likely.

titusfortner avatar Oct 24 '25 23:10 titusfortner

I think it's probable that Chrome has fixed something if it is working for me now and there are multiple issues that it isn't. Or there's something special about what Java/.NET are doing, but that's less likely.

Hi everyone, just wanted to confirm that the same issue still occurs on Chrome 141.

When using the .NET binding, unhandledPromptBehavior still doesn’t accept a dictionary — the same exception is thrown:

System.ArgumentException: 'There is already an option for the unhandledPromptBehavior capability. Please use the UnhandledPromptBehavior property instead.'

Behavior is identical to what was reported earlier on Chrome 137+. So it looks like the problem hasn’t been resolved yet in the latest Chrome/ChromeDriver release.

tgolovach93-png avatar Oct 27 '25 10:10 tgolovach93-png

@tgolovach93-png, sending a dictionary is not compliant with the spec we are still using for that class. A string should work for what you want to do. If a string isn't working, please provide logs.

titusfortner avatar Oct 27 '25 14:10 titusfortner

We need more information about this issue in order to troubleshoot.

Please turn on logging and re-run your code. Information on how to adjust logs for your language can be found in our Troubleshooting documentation.

selenium-ci avatar Oct 27 '25 14:10 selenium-ci

Hello,

Below is a log file for a test run with chrome driver options:

options.UseWebSocketUrl = true;
options.UnhandledPromptBehavior = UnhandledPromptBehavior.Ignore;

selenium_driver_logs.txt

In test I just open test html, fill in an input and trying to navigate to "about:blank".

As far as I can see from the log, the alert is opening and closing automatically, it is not visible to the user.

-> "method": "Page.javascriptDialogOpening", -> "method": "Page.javascriptDialogClosed",

There were a lot of bugs for chrome driver that are all closed as "Intended behavior". The description of this bug shows similar behavior to what we can reproduce and the comment from the chrome driver team states that:

"According to the specification, if the unhandledPromptBehavior is set as a string (which is stored in fallbackDefault) or not set, for the beforeunload it will be "accept and notify". In order to ignore the beforeunload prompt, please provide unhandledPromptBehavior as a dictionary."

which leads me to believe that there is a difference between Ruby and .NET and we indeed need to provide a dictionary.

It is possible that we are missing something here, but from all our previous testing we couldn't find a way to enable alerts in our tests.

Chrome version used: 141.0.7390.109 Selenium.WebDriver version used: 4.38.0

KaterinaDailid avatar Oct 27 '25 20:10 KaterinaDailid

sending a dictionary is not compliant with the spec we are still using for that class

@titusfortner Then we need to fix the spec (partially)?

nvborisenko avatar Oct 27 '25 21:10 nvborisenko

@KaterinaDailid thank you for providing the additional context and logs. I missed that this is specifically about beforeUnload, which is handled as a special case in the classic spec, but not in the bidi spec.

In the classic spec beforeUnload alerts are never shown, they are automatically accepted as part of whatever navigation is being attempted. So Google is trying to do the correct backwards compatible thing by setting this on your behalf:

            "unhandledPromptBehavior": {
               "beforeUnload": "accept",
               "default": "ignore"
            },

(I'll skip past how this isn't actually the backwards compatible behavior because it is out of scope and I'm having that conversation elsewhere).

What this means is that this feature was not something Selenium has ever officially supported. Whether this is a bug or a feature request, though doesn't matter to you. It is true that .NET is the only Selenium language that doesn't have a backdoor to allow you to just pass in whatever. We (@nvborisenko) would have to overhaul that whole approach or do something weird to support sending either a dictionary or a string in a backwards compatible way.

I think the right answer is to change the unhandledPrompBehavior after the session has started. This code should just work for you:

var handler = new UserPromptHandler
{
    Alert = UserPromptHandlerType.Ignore,
    Confirm = UserPromptHandlerType.Ignore,
    Prompt = UserPromptHandlerType.Ignore,
    BeforeUnload = UserPromptHandlerType.Ignore,
    Default = UserPromptHandlerType.Ignore,
};

var userContextOptions = new CreateUserContextOptions
{
    UnhandledPromptBehavior = handler
};

var bidi = await driver.AsBiDiAsync();
await bidi.Browser.CreateUserContextAsync(userContextOptions);

@nvborisenko not sure how this kind of thing fits into our high level API discussion. I kind of want to see something as simple as driver.Manage().UserPromptHandler = handler that would wrap the above. I think it is the only capability that can be updated (well, plus timeouts, but I don't think .NET ever ended up adding support for setting timeouts in the capabilities, so it doesn't matter). 😂 But we can't exactly add it to OptionsManager, either since it is BiDi only.

titusfortner avatar Oct 28 '25 03:10 titusfortner

Ok, looking more, you would need to create a new browsingcontext with that usercontext as the default and switch to it, so definitely not as simple.

        var window = await bidi.BrowsingContext.CreateAsync(ContextType.Window, new()
        {
            UserContext = userContext.UserContext,
        });
        await window.ActivateAsync();

titusfortner avatar Oct 28 '25 03:10 titusfortner

Ok, looking more, you would need to create a new browsingcontext with that usercontext as the default and switch to it, so definitely not as simple.

    var window = await bidi.BrowsingContext.CreateAsync(ContextType.Window, new()
    {
        UserContext = userContext.UserContext,
    });
    await window.ActivateAsync();

Hi @titusfortner , Thanks for your suggestion.

I tried your solution, it navigates to blank page but the alert still doesn’t appear in the browser. Could you let me know if I might have implemented something incorrectly?

I used the following page: index.html

These are my logs: log_20251104_170820.txt log_20251104_170838.txt

This is my code:

Click to expand
using OpenQA.Selenium;
using OpenQA.Selenium.BiDi;
using OpenQA.Selenium.BiDi.Browser;
using OpenQA.Selenium.BiDi.BrowsingContext;
using OpenQA.Selenium.BiDi.Session;
using OpenQA.Selenium.Chrome;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace SimpleBiDi
{
	internal class Program
	{
		public static bool UseBiDiDriver = true;

		static void Main(string[] args)
		{
			TestOne().GetAwaiter().GetResult();

			TestTwo().GetAwaiter().GetResult();
		}

		static async Task TestOne()
		{
			IWebDriver driver = null;

			try
			{
				var driverOptions = InitOptions();

				driver = new ChromeDriver(driverOptions);

				if (UseBiDiDriver)
				{
					var bidi = await driver.AsBiDiAsync();

					var handler = new UserPromptHandler
					{
						Alert = UserPromptHandlerType.Ignore,
						Confirm = UserPromptHandlerType.Ignore,
						Prompt = UserPromptHandlerType.Ignore,
						BeforeUnload = UserPromptHandlerType.Ignore,
						Default = UserPromptHandlerType.Ignore,
					};

					var userContextOptions = new CreateUserContextOptions
					{
						UnhandledPromptBehavior = handler
					};

					var userContext = await bidi.Browser.CreateUserContextAsync(userContextOptions);

					var createResult = await bidi.BrowsingContext.CreateAsync(ContextType.Window, new CreateOptions
					{
						UserContext = userContext.UserContext,
					});

					await bidi.BrowsingContext.ActivateAsync(createResult.Context);
					// also tried: await createResult.Context.ActivateAsync();

					var allHandles = driver.WindowHandles;
					string newWindowHandle = allHandles[allHandles.Count - 1];
					driver.SwitchTo().Window(newWindowHandle);
				}

				driver.Navigate().GoToUrl("file:///C:/Temp/20251104/index.html");
				Thread.Sleep(5000);
				driver.Navigate().GoToUrl("about:blank");

				Thread.Sleep(10000);
			}
			finally
			{
				var logs = driver?.Manage().Logs.GetLog(LogType.Driver);
				SaveLogsToFile(logs);

				driver?.Close();
				driver?.Quit();
			}
		}

		static async Task TestTwo()
		{
			IWebDriver driver = null;

			try
			{
				var driverOptions = InitOptions();

				driver = new ChromeDriver(driverOptions);

				if (UseBiDiDriver)
				{
					var bidi = await driver.AsBiDiAsync();

					var handler = new UserPromptHandler
					{
						Alert = UserPromptHandlerType.Ignore,
						Confirm = UserPromptHandlerType.Ignore,
						Prompt = UserPromptHandlerType.Ignore,
						BeforeUnload = UserPromptHandlerType.Ignore,
						Default = UserPromptHandlerType.Ignore,
					};

					var userContextOptions = new CreateUserContextOptions
					{
						UnhandledPromptBehavior = handler
					};

					var userContext = await bidi.Browser.CreateUserContextAsync(userContextOptions);

					var createResult = await bidi.BrowsingContext.CreateAsync(ContextType.Window, new CreateOptions
					{
						UserContext = userContext.UserContext,
					});

					await bidi.BrowsingContext.ActivateAsync(createResult.Context);
					// also tried: await createResult.Context.ActivateAsync();

					var bidiContext = bidi.BrowsingContext;

					await bidiContext.NavigateAsync(createResult.Context, "file:///C:/Temp/20251104/index.html");

					Thread.Sleep(5000);

					await bidiContext.NavigateAsync(createResult.Context, "about:blank");

					Thread.Sleep(10000);
				}
			}
			finally
			{
				var logs = driver?.Manage().Logs.GetLog(LogType.Driver);
				SaveLogsToFile(logs);

				driver?.Close();
				driver?.Quit();
			}
		}

		private static ChromeOptions InitOptions()
		{
			var options = new ChromeOptions();

			options.SetLoggingPreference(LogType.Driver, LogLevel.All);
			options.AddUserProfilePreference("safebrowsing.enabled", true);
			options.AddUserProfilePreference("profile.default_content_setting_values.automatic_downloads", 1);
			options.AddAdditionalOption("useAutomationExtension", false);
			options.AddUserProfilePreference("credentials_enable_service", false);
			options.AddUserProfilePreference("password_manager_enabled", false);
			options.AddExcludedArgument("enable-automation");
			options.AddArguments("--remote-allow-origins=*");

			if (UseBiDiDriver)
			{
				options.UseWebSocketUrl = true;
				options.UnhandledPromptBehavior = UnhandledPromptBehavior.Ignore;
			}

			return options;
		}

		private static void SaveLogsToFile(ReadOnlyCollection<LogEntry> logs)
		{
			if (logs == null || logs.Count == 0)
			{
				Console.WriteLine("No logs captured.");
			}
			else
			{
				var logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs",
					$"log_{DateTime.Now:yyyyMMdd_HHmmss}.txt");
				Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));

				using (var writer = new StreamWriter(logFilePath))
				{
					foreach (var log in logs)
					{
						writer.WriteLine($"{log.Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{log.Level}]: {log.Message}");
					}
				}
			}
		}
	}
}

I used Selenium 4.38.0.

lastas avatar Nov 04 '25 16:11 lastas

Thank you for trying my suggestion, but I no longer have a dev setup right now to verify the behavior.

@nvborisenko... I guess we should add a UserPromptHandler property on Options that can be set with a dictionary? I'm not sure the best way to do this in .NET to ensure the right things happen in BiDi & Classic, but you are right, we need to support this change for BiDi sessions.

titusfortner avatar Nov 05 '25 17:11 titusfortner

I know how to be backward-compatible (partially, but will work for 99.9% of users), and how to implement new UnhandledPromptBehavior as dictionary.

class Options
{
  UnhandledPromptBehavior UnhandledPromptBehavior { get; set; } // before,  it is Enum
  UnhandledPromptBehaviorOption? UnhandledPromptBehavior { get; set; } // after
}

abstract record UnhandledPromptBehaviorOption
{
  public static implicit operator from existing Enum
}

record UnhandledPromptBehaviorSingleOption(Enum); // represents existing Enum

record UnhandledPromptBehaviorMultiOption // represents Dictionary
{
  Enum BeforeUnload { get; set; }
  Enum Confirm { get; set; }
  // ...
}

This is the best compromise to handle multiple types of the same property, even if spec will be changed in future. Backward compatible at compile phase, breaking change at binary level, supports dictionary.

nvborisenko avatar Nov 06 '25 17:11 nvborisenko