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 08c2822697dfb..a3b8e898a0861 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -36,6 +36,7 @@ public sealed class BiDi : IAsyncDisposable private Script.ScriptModule? _scriptModule; private Log.LogModule? _logModule; private Storage.StorageModule? _storageModule; + private WebExtension.WebExtensionModule? _webExtensionModule; private readonly object _moduleLock = new(); @@ -150,6 +151,19 @@ public Storage.StorageModule Storage } } + public WebExtension.WebExtensionModule WebExtension + { + get + { + if (_webExtensionModule is not null) return _webExtensionModule; + lock (_moduleLock) + { + _webExtensionModule ??= new WebExtension.WebExtensionModule(_broker); + } + return _webExtensionModule; + } + } + public Task StatusAsync() { return SessionModule.StatusAsync(); diff --git a/dotnet/src/webdriver/BiDi/Communication/Broker.cs b/dotnet/src/webdriver/BiDi/Communication/Broker.cs index 92595f4500def..db2058224749b 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..4092203c47bac --- /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(InstallParameters @params) + : Command(@params, "webExtension.install"); + +internal sealed record InstallParameters(ExtensionData ExtensionData) : Parameters; + +[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 class 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..4d2635293e75c --- /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(UninstallParameters @params) + : Command(@params, "webExtension.uninstall"); + +internal sealed record UninstallParameters(Extension Extension) : Parameters; + +public sealed class 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..009e01f1e7012 --- /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 InstallParameters(extensionData); + + return await Broker.ExecuteCommandAsync(new InstallCommand(@params), options).ConfigureAwait(false); + } + + public async Task UninstallAsync(Extension extension, UninstallOptions? options = null) + { + var @params = new UninstallParameters(extension); + + return 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..4be01281dc79c --- /dev/null +++ b/dotnet/test/common/BiDi/WebExtension/WebExtensionTest.cs @@ -0,0 +1,96 @@ +// +// 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; + +[Ignore(""" + The following test suite wants to set driver arguments via Options, but it breaks CDP/DevTools tests. + The desired arguments (for Chromium only?): + --enable-unsafe-extension-debugging + --remote-debugging-pipe + Ignoring these tests for now. Hopefully https://github.com/SeleniumHQ/selenium/issues/15536 will be resolved soon. + """)] +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/Selenium.WebDriver.Common.Tests.csproj b/dotnet/test/common/Selenium.WebDriver.Common.Tests.csproj index 65a28950931e9..042384fd1fba0 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 +