diff --git a/common/extensions/BUILD.bazel b/common/extensions/BUILD.bazel index 9095b311e4fb3..71b72cbbdf491 100644 --- a/common/extensions/BUILD.bazel +++ b/common/extensions/BUILD.bazel @@ -7,6 +7,7 @@ filegroup( "**/*", ]), visibility = [ + "//dotnet/test/common:__pkg__", "//java/test/org/openqa/selenium/chrome:__pkg__", "//java/test/org/openqa/selenium/edge:__pkg__", "//java/test/org/openqa/selenium/environment:__pkg__", diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 9964b4e59cea8..7bb83f93137a3 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -35,6 +35,7 @@ public class BiDi : IAsyncDisposable private readonly Lazy _scriptModule; private readonly Lazy _logModule; private readonly Lazy _storageModule; + private readonly Lazy _webExtensionModule; internal BiDi(string url) { @@ -50,6 +51,7 @@ internal BiDi(string url) _scriptModule = new Lazy(() => new Script.ScriptModule(_broker)); _logModule = new Lazy(() => new Log.LogModule(_broker)); _storageModule = new Lazy(() => new Storage.StorageModule(_broker)); + _webExtensionModule = new Lazy(() => new WebExtension.WebExtensionModule(_broker)); } internal Session.SessionModule SessionModule => _sessionModule.Value; @@ -60,6 +62,7 @@ internal BiDi(string url) public Script.ScriptModule Script => _scriptModule.Value; public Log.LogModule Log => _logModule.Value; public Storage.StorageModule Storage => _storageModule.Value; + public WebExtension.WebExtensionModule WebExtension => _webExtensionModule.Value; public Task StatusAsync() { diff --git a/dotnet/src/webdriver/BiDi/Communication/Broker.cs b/dotnet/src/webdriver/BiDi/Communication/Broker.cs index db926e3b7228e..f1f4aa79579d7 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Broker.cs @@ -86,6 +86,7 @@ internal Broker(BiDi bidi, Uri url) new DateTimeOffsetConverter(), new PrintPageRangeConverter(), new InputOriginConverter(), + new WebExtensionConverter(_bidi), new SubscriptionConverter(), new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs b/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs index 5b2a0b803ff7a..e467b53c4922e 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs @@ -165,4 +165,8 @@ namespace OpenQA.Selenium.BiDi.Communication.Json; [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(WebExtension.InstallCommand))] +[JsonSerializable(typeof(WebExtension.InstallResult))] +[JsonSerializable(typeof(WebExtension.UninstallCommand))] + internal partial class BiDiJsonSerializerContext : JsonSerializerContext; diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/WebExtensionConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/WebExtensionConverter.cs new file mode 100644 index 0000000000000..f29dc90567fad --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/WebExtensionConverter.cs @@ -0,0 +1,47 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.WebExtension; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenQA.Selenium.BiDi.Communication.Json.Converters; + +internal class WebExtensionConverter : JsonConverter +{ + private readonly BiDi _bidi; + + public WebExtensionConverter(BiDi bidi) + { + _bidi = bidi; + } + + public override Extension? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var id = reader.GetString(); + + return new Extension(_bidi, id!); + } + + public override void Write(Utf8JsonWriter writer, Extension value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Id); + } +} diff --git a/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs b/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs new file mode 100644 index 0000000000000..c19beb1579e70 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs @@ -0,0 +1,40 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.Threading.Tasks; + +namespace OpenQA.Selenium.BiDi.WebExtension; + +public sealed class Extension +{ + private readonly BiDi _bidi; + + public Extension(BiDi bidi, string id) + { + _bidi = bidi; + Id = id; + } + + internal string Id { get; } + + public Task UninstallAsync(UninstallOptions? options = null) + { + return _bidi.WebExtension.UninstallAsync(this, options); + } +} diff --git a/dotnet/src/webdriver/BiDi/WebExtension/InstallCommand.cs b/dotnet/src/webdriver/BiDi/WebExtension/InstallCommand.cs new file mode 100644 index 0000000000000..b0f121420656d --- /dev/null +++ b/dotnet/src/webdriver/BiDi/WebExtension/InstallCommand.cs @@ -0,0 +1,44 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Communication; +using System.Text.Json.Serialization; + +namespace OpenQA.Selenium.BiDi.WebExtension; + +internal sealed class InstallCommand(InstallCommandParameters @params) + : Command(@params, "webExtension.install"); + +internal sealed record InstallCommandParameters(ExtensionData ExtensionData) : CommandParameters; + +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(ExtensionArchivePath), "archivePath")] +[JsonDerivedType(typeof(ExtensionBase64Encoded), "base64")] +[JsonDerivedType(typeof(ExtensionPath), "path")] +public abstract record ExtensionData; + +public sealed record ExtensionArchivePath(string Path) : ExtensionData; + +public sealed record ExtensionBase64Encoded(string Value) : ExtensionData; + +public sealed record ExtensionPath(string Path) : ExtensionData; + +public sealed record InstallOptions : CommandOptions; + +public sealed record InstallResult(Extension Extension) : EmptyResult; diff --git a/dotnet/src/webdriver/BiDi/WebExtension/UninstallCommand.cs b/dotnet/src/webdriver/BiDi/WebExtension/UninstallCommand.cs new file mode 100644 index 0000000000000..7bede2fa2c384 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/WebExtension/UninstallCommand.cs @@ -0,0 +1,29 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Communication; + +namespace OpenQA.Selenium.BiDi.WebExtension; + +internal sealed class UninstallCommand(UninstallCommandParameters @params) + : Command(@params, "webExtension.uninstall"); + +internal sealed record UninstallCommandParameters(Extension Extension) : CommandParameters; + +public sealed record UninstallOptions : CommandOptions; diff --git a/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs b/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs new file mode 100644 index 0000000000000..5ec32d4db2342 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs @@ -0,0 +1,40 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Communication; +using System.Threading.Tasks; + +namespace OpenQA.Selenium.BiDi.WebExtension; + +public sealed class WebExtensionModule(Broker broker) : Module(broker) +{ + public async Task InstallAsync(ExtensionData extensionData, InstallOptions? options = null) + { + var @params = new InstallCommandParameters(extensionData); + + return await Broker.ExecuteCommandAsync(new InstallCommand(@params), options).ConfigureAwait(false); + } + + internal async Task UninstallAsync(Extension extension, UninstallOptions? options = null) + { + var @params = new UninstallCommandParameters(extension); + + await Broker.ExecuteCommandAsync(new UninstallCommand(@params), options).ConfigureAwait(false); + } +} diff --git a/dotnet/test/common/BUILD.bazel b/dotnet/test/common/BUILD.bazel index 23212fd57def4..7c210e2b8ea6c 100644 --- a/dotnet/test/common/BUILD.bazel +++ b/dotnet/test/common/BUILD.bazel @@ -17,6 +17,7 @@ filegroup( srcs = [], data = [ "appconfig.json", + "//common/extensions", "//common/src/web", "//dotnet/src/webdriver:manager-linux", "//dotnet/src/webdriver:manager-macos", diff --git a/dotnet/test/common/BiDi/WebExtension/WebExtensionTest.cs b/dotnet/test/common/BiDi/WebExtension/WebExtensionTest.cs new file mode 100644 index 0000000000000..7973ea497cd85 --- /dev/null +++ b/dotnet/test/common/BiDi/WebExtension/WebExtensionTest.cs @@ -0,0 +1,89 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using NUnit.Framework; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace OpenQA.Selenium.BiDi.WebExtension; + +class WebExtensionTest : BiDiTestFixture +{ + [Test] + public async Task CanInstallPathWebExtension() + { + string path = Path.GetFullPath("common/extensions/webextensions-selenium-example"); + + var result = await bidi.WebExtension.InstallAsync(new ExtensionPath(path)); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Extension, Is.Not.Null); + } + + [Test] + [IgnoreBrowser(Selenium.Browser.Chrome, "Archived and Base64 extensions are not supported?")] + [IgnoreBrowser(Selenium.Browser.Edge, "Archived and Base64 extensions are not supported?")] + public async Task CanInstallArchiveWebExtension() + { + string path = LocateRelativePath("common/extensions/webextensions-selenium-example.zip"); + + var result = await bidi.WebExtension.InstallAsync(new ExtensionArchivePath(path)); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Extension, Is.Not.Null); + } + + [Test] + [IgnoreBrowser(Selenium.Browser.Chrome, "Archived and Base64 extensions are not supported?")] + [IgnoreBrowser(Selenium.Browser.Edge, "Archived and Base64 extensions are not supported?")] + public async Task CanInstallBase64WebExtension() + { + var path = LocateRelativePath("common/extensions/webextensions-selenium-example.zip"); + + string base64 = Convert.ToBase64String(File.ReadAllBytes(path)); + + var result = await bidi.WebExtension.InstallAsync(new ExtensionBase64Encoded(base64)); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Extension, Is.Not.Null); + } + + [Test] + public async Task CanUninstallExtension() + { + string path = LocateRelativePath("common/extensions/webextensions-selenium-example"); + + var result = await bidi.WebExtension.InstallAsync(new ExtensionPath(path)); + + await result.Extension.UninstallAsync(); + } + + private static string LocateRelativePath(string path) + { + try + { + return Bazel.Runfiles.Create().Rlocation($"_main/{path}"); + } + catch (FileNotFoundException) + { + return Path.GetFullPath(path); + } + } +} diff --git a/dotnet/test/common/Environment/DriverFactory.cs b/dotnet/test/common/Environment/DriverFactory.cs index f58eea2430673..94305a1d9977f 100644 --- a/dotnet/test/common/Environment/DriverFactory.cs +++ b/dotnet/test/common/Environment/DriverFactory.cs @@ -86,7 +86,7 @@ public IWebDriver CreateDriverWithOptions(Type driverType, DriverOptions driverO options = GetDriverOptions(driverType, driverOptions); var chromeOptions = (ChromeOptions)options; - chromeOptions.AddArguments("--no-sandbox", "--disable-dev-shm-usage"); + chromeOptions.AddArguments("--no-sandbox", "--disable-dev-shm-usage", "--remote-debugging-pipe", "--enable-unsafe-extension-debugging"); service = CreateService(); if (!string.IsNullOrEmpty(this.browserBinaryLocation)) @@ -104,7 +104,7 @@ public IWebDriver CreateDriverWithOptions(Type driverType, DriverOptions driverO options = GetDriverOptions(driverType, driverOptions); var edgeOptions = (EdgeOptions)options; - edgeOptions.AddArguments("--no-sandbox", "--disable-dev-shm-usage"); + edgeOptions.AddArguments("--no-sandbox", "--disable-dev-shm-usage", "--remote-debugging-pipe", "--enable-unsafe-extension-debugging"); service = CreateService(); if (!string.IsNullOrEmpty(this.browserBinaryLocation)) diff --git a/dotnet/test/common/Selenium.WebDriver.Common.Tests.csproj b/dotnet/test/common/Selenium.WebDriver.Common.Tests.csproj index 2451c0556f8af..9518def5a986b 100644 --- a/dotnet/test/common/Selenium.WebDriver.Common.Tests.csproj +++ b/dotnet/test/common/Selenium.WebDriver.Common.Tests.csproj @@ -27,6 +27,10 @@ Always + + common\extensions\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest +