diff --git a/dotnet/defs.bzl b/dotnet/defs.bzl index 827de3f8d904a..14305f2b280a5 100644 --- a/dotnet/defs.bzl +++ b/dotnet/defs.bzl @@ -3,6 +3,7 @@ load("//dotnet:selenium-dotnet-version.bzl", "SUPPORTED_DEVTOOLS_VERSIONS") load("//dotnet/private:dotnet_nunit_test_suite.bzl", _dotnet_nunit_test_suite = "dotnet_nunit_test_suite") load("//dotnet/private:framework.bzl", _framework = "framework") load("//dotnet/private:generate_devtools.bzl", _generate_devtools = "generate_devtools") +load("//dotnet/private:generate_resources.bzl", _generated_resource_utilities = "generated_resource_utilities") load("//dotnet/private:generated_assembly_info.bzl", _generated_assembly_info = "generated_assembly_info") load("//dotnet/private:nuget_pack.bzl", _nuget_pack = "nuget_pack") load("//dotnet/private:nunit_test.bzl", _nunit_test = "nunit_test") @@ -19,6 +20,7 @@ csharp_test = _csharp_test dotnet_nunit_test_suite = _dotnet_nunit_test_suite framework = _framework generate_devtools = _generate_devtools +generated_resource_utilities = _generated_resource_utilities generated_assembly_info = _generated_assembly_info nuget_pack = _nuget_pack nunit_test = _nunit_test diff --git a/dotnet/private/BUILD.bazel b/dotnet/private/BUILD.bazel index e69de29bb2d1d..03a5687d9cf35 100644 --- a/dotnet/private/BUILD.bazel +++ b/dotnet/private/BUILD.bazel @@ -0,0 +1,4 @@ +py_binary( + name = "generate_resources_tool", + srcs = ["generate_resources_tool.py"], +) diff --git a/dotnet/private/generate_resources.bzl b/dotnet/private/generate_resources.bzl new file mode 100644 index 0000000000000..987de7670c70c --- /dev/null +++ b/dotnet/private/generate_resources.bzl @@ -0,0 +1,43 @@ +"""Generate C# partial class with embedded JS resources via a Python tool.""" + +def _generate_resource_utilities_impl(ctx): + """Invoke a Python script to generate ResourceUtilities.cs from input files. + + The mapping from C# property name to JS file is provided explicitly via the + 'resources' attribute as a dict: { "PropertyName": label }. + """ + + args = ctx.actions.args() + args.add("--output", ctx.outputs.out) + + inputs = [] + for target, name in ctx.attr.resources.items(): + files = target.files.to_list() + if len(files) != 1: + fail("Each resource label must produce exactly one file, got {} for {}".format(len(files), name)) + src = files[0] + inputs.append(src) + args.add("--input") + args.add("%s=%s" % (name, src.path)) + + ctx.actions.run( + inputs = inputs, + outputs = [ctx.outputs.out], + executable = ctx.executable._tool, + arguments = [args], + mnemonic = "GenerateResourceUtilities", + progress_message = "Generating C# ResourceUtilities partial class", + ) + +generated_resource_utilities = rule( + implementation = _generate_resource_utilities_impl, + attrs = { + "resources": attr.label_keyed_string_dict(allow_files = True), + "out": attr.output(mandatory = True), + "_tool": attr.label( + default = Label("//dotnet/private:generate_resources_tool"), + executable = True, + cfg = "exec", + ), + }, +) diff --git a/dotnet/private/generate_resources_tool.py b/dotnet/private/generate_resources_tool.py new file mode 100644 index 0000000000000..b60ee6f02db92 --- /dev/null +++ b/dotnet/private/generate_resources_tool.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Generate C# ResourceUtilities partial class with embedded JS resources. + +Usage: + generate_resources_tool.py --output path/to/ResourceUtilities.g.cs \ + --input Ident1=path/to/file1.js \ + --input Ident2=path/to/file2.js ... + +Each identifier becomes a const string in ResourceUtilities class. +The content is emitted as a C# raw string literal using 5-quotes. + +TODO: +It would be nice to convert this small single-file utility to .NET10/C#, +so it would work like `dotnet run generate_resources.cs -- `. +Meaning .NET developers can easily support it. +""" + +import argparse +import os +import sys +from typing import List, Tuple + + +def parse_args(argv: List[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--output", required=True) + parser.add_argument("--input", action="append", default=[], help="IDENT=path") + return parser.parse_args(argv) + + +def parse_input_spec(spec: str) -> Tuple[str, str]: + if "=" not in spec: + raise ValueError(f"Invalid --input value, expected IDENT=path, got: {spec}") + ident, path = spec.split("=", 1) + ident = ident.strip() + path = path.strip() + if not ident: + raise ValueError(f"Empty identifier in --input value: {spec}") + if not path: + raise ValueError(f"Empty path in --input value: {spec}") + return ident, path + + +def generate(output: str, inputs: List[Tuple[str, str]]) -> None: + props: List[str] = [] + for prop_name, path in inputs: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + # Use a C# raw string literal with five quotes. For a valid raw + # literal, the content must start on a new line and the closing + # quotes must be on their own line as well. We assume the content + # does not contain a sequence of five consecutive double quotes. + # + # Resulting C# will look like: + # """"" + # + # """"" + literal = '"""""\n' + content + '\n"""""' + props.append(f" internal const string {prop_name} = {literal};") + + lines: List[str] = [] + lines.append("// ") + lines.append("namespace OpenQA.Selenium.Internal;") + lines.append("") + lines.append("internal static partial class ResourceUtilities") + lines.append("{") + for p in props: + lines.append(p) + lines.append("}") + lines.append("") + + os.makedirs(os.path.dirname(output), exist_ok=True) + with open(output, "w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(lines)) + + +def main(argv: List[str]) -> int: + args = parse_args(argv) + inputs: List[Tuple[str, str]] = [] + for spec in args.input: + ident, path = parse_input_spec(spec) + inputs.append((ident, path)) + generate(args.output, inputs) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/dotnet/src/webdriver/BUILD.bazel b/dotnet/src/webdriver/BUILD.bazel index 7eb83adc81ec4..0d57751f521e1 100644 --- a/dotnet/src/webdriver/BUILD.bazel +++ b/dotnet/src/webdriver/BUILD.bazel @@ -1,5 +1,5 @@ load("//common:defs.bzl", "copy_file") -load("//dotnet:defs.bzl", "csharp_library", "devtools_version_targets", "framework", "generated_assembly_info", "nuget_pack") +load("//dotnet:defs.bzl", "csharp_library", "devtools_version_targets", "framework", "generated_assembly_info", "generated_resource_utilities", "nuget_pack") load( "//dotnet:selenium-dotnet-version.bzl", "ASSEMBLY_COMPANY", @@ -25,10 +25,23 @@ generated_assembly_info( version = ASSEMBLY_VERSION, ) +generated_resource_utilities( + name = "resource-utilities", + out = "ResourceUtilities.g.cs", + resources = { + "//javascript/atoms/fragments:find-elements.js": "FindElementsAtom", + "//javascript/atoms/fragments:is-displayed.js": "IsDisplayedAtom", + "//javascript/cdp-support:mutation-listener.js": "MutationListenerAtom", + "//javascript/webdriver/atoms:get-attribute.js": "GetAttributeAtom", + "//third_party/js/selenium:webdriver_json": "WebDriverPrefsJson", + }, +) + csharp_library( name = "webdriver-netstandard2.0", srcs = [ ":assembly-info", + ":resource-utilities", ] + glob([ "**/*.cs", ]) + devtools_version_targets(), @@ -38,13 +51,7 @@ csharp_library( ], langversion = "12.0", nullable = "enable", - resources = [ - "//javascript/atoms/fragments:find-elements.js", - "//javascript/atoms/fragments:is-displayed.js", - "//javascript/cdp-support:mutation-listener.js", - "//javascript/webdriver/atoms:get-attribute.js", - "//third_party/js/selenium:webdriver_json", - ], + resources = [], target_frameworks = [ "netstandard2.0", ], @@ -66,6 +73,7 @@ csharp_library( name = "webdriver-net8.0", srcs = [ ":assembly-info", + ":resource-utilities", ] + glob([ "**/*.cs", ]) + devtools_version_targets(), @@ -78,13 +86,7 @@ csharp_library( ], langversion = "12.0", nullable = "enable", - resources = [ - "//javascript/atoms/fragments:find-elements.js", - "//javascript/atoms/fragments:is-displayed.js", - "//javascript/cdp-support:mutation-listener.js", - "//javascript/webdriver/atoms:get-attribute.js", - "//third_party/js/selenium:webdriver_json", - ], + resources = [], target_frameworks = [ "net8.0", ], @@ -99,6 +101,7 @@ csharp_library( name = "webdriver-netstandard2.0-strongnamed", srcs = [ ":assembly-info", + ":resource-utilities", ] + glob([ "**/*.cs", ]) + devtools_version_targets(), @@ -106,13 +109,7 @@ csharp_library( keyfile = "//dotnet:Selenium.snk", langversion = "12.0", nullable = "enable", - resources = [ - "//javascript/atoms/fragments:find-elements.js", - "//javascript/atoms/fragments:is-displayed.js", - "//javascript/cdp-support:mutation-listener.js", - "//javascript/webdriver/atoms:get-attribute.js", - "//third_party/js/selenium:webdriver_json", - ], + resources = [], target_frameworks = [ "netstandard2.0", ], @@ -134,6 +131,7 @@ csharp_library( name = "webdriver-net8.0-strongnamed", srcs = [ ":assembly-info", + ":resource-utilities", ] + glob([ "**/*.cs", ]) + devtools_version_targets(), @@ -144,13 +142,7 @@ csharp_library( keyfile = "//dotnet:Selenium.snk", langversion = "12.0", nullable = "enable", - resources = [ - "//javascript/atoms/fragments:find-elements.js", - "//javascript/atoms/fragments:is-displayed.js", - "//javascript/cdp-support:mutation-listener.js", - "//javascript/webdriver/atoms:get-attribute.js", - "//third_party/js/selenium:webdriver_json", - ], + resources = [], target_frameworks = [ "net8.0", ], diff --git a/dotnet/src/webdriver/Firefox/FirefoxExtension.cs b/dotnet/src/webdriver/Firefox/FirefoxExtension.cs index 11a97c8c01e83..ddfe9f183e744 100644 --- a/dotnet/src/webdriver/Firefox/FirefoxExtension.cs +++ b/dotnet/src/webdriver/Firefox/FirefoxExtension.cs @@ -22,7 +22,7 @@ using System.Globalization; using System.IO; using System.IO.Compression; -using System.Reflection; +using System.Text; using System.Text.Json.Nodes; using System.Xml; @@ -38,7 +38,6 @@ public class FirefoxExtension private const string JsonManifestFileName = "manifest.json"; private readonly string extensionFileName; - private readonly string extensionResourceId; /// /// Initializes a new instance of the class. @@ -49,27 +48,8 @@ public class FirefoxExtension /// then using the full path to the file, if a full path is provided. /// If is . public FirefoxExtension(string fileName) - : this(fileName, string.Empty) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the file containing the Firefox extension. - /// The ID of the resource within the assembly containing the extension - /// if the file is not present in the file system. - /// WebDriver attempts to resolve the parameter - /// by looking first for the specified file in the directory of the calling assembly, - /// then using the full path to the file, if a full path is provided. If the file is - /// not found in the file system, WebDriver attempts to locate a resource in the - /// executing assembly with the name specified by the - /// parameter. - /// If or are . - internal FirefoxExtension(string fileName, string resourceId) { this.extensionFileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - this.extensionResourceId = resourceId ?? throw new ArgumentNullException(nameof(resourceId)); } /// @@ -89,7 +69,7 @@ public void Install(string profileDirectory) // First, expand the .xpi archive into a temporary location. Directory.CreateDirectory(tempFileName); - Stream zipFileStream = ResourceUtilities.GetResourceStream(this.extensionFileName, $"{Assembly.GetExecutingAssembly().GetName().Name}.{this.extensionResourceId}"); + using Stream zipFileStream = new MemoryStream(Encoding.UTF8.GetBytes(ResourceUtilities.WebDriverPrefsJson)); using (ZipArchive extensionZipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read)) { extensionZipArchive.ExtractToDirectory(tempFileName); diff --git a/dotnet/src/webdriver/Firefox/FirefoxProfile.cs b/dotnet/src/webdriver/Firefox/FirefoxProfile.cs index 0af71b5d9c014..7284973d066d2 100644 --- a/dotnet/src/webdriver/Firefox/FirefoxProfile.cs +++ b/dotnet/src/webdriver/Firefox/FirefoxProfile.cs @@ -23,7 +23,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; -using System.Reflection; using System.Text.Json; namespace OpenQA.Selenium.Firefox; @@ -298,15 +297,12 @@ private void UpdateUserPreferences(string profileDirectory) private Preferences ReadDefaultPreferences() { - using (Stream defaultPrefsStream = ResourceUtilities.GetResourceStream("webdriver_prefs.json", $"{Assembly.GetExecutingAssembly().GetName().Name}.webdriver_prefs.json")) - { - using JsonDocument defaultPreferences = JsonDocument.Parse(defaultPrefsStream); + using JsonDocument defaultPreferences = JsonDocument.Parse(ResourceUtilities.WebDriverPrefsJson); - JsonElement immutableDefaultPreferences = defaultPreferences.RootElement.GetProperty("frozen"); - JsonElement editableDefaultPreferences = defaultPreferences.RootElement.GetProperty("mutable"); + JsonElement immutableDefaultPreferences = defaultPreferences.RootElement.GetProperty("frozen"); + JsonElement editableDefaultPreferences = defaultPreferences.RootElement.GetProperty("mutable"); - return new Preferences(immutableDefaultPreferences, editableDefaultPreferences); - } + return new Preferences(immutableDefaultPreferences, editableDefaultPreferences); } /// diff --git a/dotnet/src/webdriver/Internal/ResourceUtilities.cs b/dotnet/src/webdriver/Internal/ResourceUtilities.cs index 06c04389e5e92..555d9244586aa 100644 --- a/dotnet/src/webdriver/Internal/ResourceUtilities.cs +++ b/dotnet/src/webdriver/Internal/ResourceUtilities.cs @@ -27,7 +27,7 @@ namespace OpenQA.Selenium.Internal; /// /// Encapsulates methods for finding and extracting WebDriver resources. /// -internal static class ResourceUtilities +internal static partial class ResourceUtilities { private static string? productVersion; private static string? platformFamily; diff --git a/dotnet/src/webdriver/JavaScriptEngine.cs b/dotnet/src/webdriver/JavaScriptEngine.cs index 3ed08e9fa944c..d44f786a06943 100644 --- a/dotnet/src/webdriver/JavaScriptEngine.cs +++ b/dotnet/src/webdriver/JavaScriptEngine.cs @@ -23,9 +23,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Linq; -using System.Reflection; using System.Text.Json; using System.Threading.Tasks; @@ -142,7 +140,7 @@ public void StopEventMonitoring() public async Task EnableDomMutationMonitoring() { // Execute the script to have it enabled on the currently loaded page. - string script = GetMutationListenerScript(); + string script = ResourceUtilities.MutationListenerAtom; await this.session.Value.Domains.JavaScript.Evaluate(script).ConfigureAwait(false); await this.AddScriptCallbackBinding(MonitorBindingName).ConfigureAwait(false); @@ -409,20 +407,6 @@ private async Task EnableDomains() } } - private static string GetMutationListenerScript() - { - string listenerScript = string.Empty; - using (Stream resourceStream = ResourceUtilities.GetResourceStream("mutation-listener.js", $"{Assembly.GetExecutingAssembly().GetName().Name}.mutation-listener.js")) - { - using (StreamReader resourceReader = new StreamReader(resourceStream)) - { - listenerScript = resourceReader.ReadToEnd(); - } - } - - return listenerScript; - } - private void OnScriptBindingCalled(object? sender, BindingCalledEventArgs e) { if (e.Name == MonitorBindingName) diff --git a/dotnet/src/webdriver/RelativeBy.cs b/dotnet/src/webdriver/RelativeBy.cs index dd6335d76b32c..fa4d8878da1b9 100644 --- a/dotnet/src/webdriver/RelativeBy.cs +++ b/dotnet/src/webdriver/RelativeBy.cs @@ -22,8 +22,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; -using System.IO; -using System.Reflection; namespace OpenQA.Selenium; @@ -38,14 +36,7 @@ public sealed class RelativeBy : By private static string GetWrappedAtom() { - string atom; - using (Stream atomStream = ResourceUtilities.GetResourceStream("find-elements.js", $"{Assembly.GetExecutingAssembly().GetName().Name}.find-elements.js")) - { - using (StreamReader atomReader = new StreamReader(atomStream)) - { - atom = atomReader.ReadToEnd(); - } - } + string atom = ResourceUtilities.FindElementsAtom; return string.Format(CultureInfo.InvariantCulture, "/* findElements */return ({0}).apply(null, arguments);", atom); } diff --git a/dotnet/src/webdriver/Selenium.WebDriver.csproj b/dotnet/src/webdriver/Selenium.WebDriver.csproj index 6cee5f837ea24..5d549c6342759 100644 --- a/dotnet/src/webdriver/Selenium.WebDriver.csproj +++ b/dotnet/src/webdriver/Selenium.WebDriver.csproj @@ -79,30 +79,11 @@ - - + + - - False - $(AssemblyName).webdriver_prefs.json - - - False - $(AssemblyName).get-attribute.js - - - False - $(AssemblyName).is-displayed.js - - - False - $(AssemblyName).find-elements.js - - - False - $(AssemblyName).mutation-listener.js - + @@ -114,4 +95,4 @@ - \ No newline at end of file + diff --git a/dotnet/src/webdriver/WebElement.cs b/dotnet/src/webdriver/WebElement.cs index 8a5c66bdad286..f54f510c701a1 100644 --- a/dotnet/src/webdriver/WebElement.cs +++ b/dotnet/src/webdriver/WebElement.cs @@ -27,7 +27,6 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Reflection; namespace OpenQA.Selenium; @@ -201,7 +200,7 @@ public virtual bool Displayed get { Dictionary parameters = new Dictionary(); - string atom = GetAtom("is-displayed.js"); + string atom = GetWrappedAtom("is_displayed", ResourceUtilities.IsDisplayedAtom); parameters.Add("script", atom); parameters.Add("args", new object[] { ((IWebDriverObjectReference)this).ToDictionary() }); @@ -436,7 +435,7 @@ public virtual ReadOnlyCollection FindElements(string mechanism, st public virtual string? GetAttribute(string attributeName) { Dictionary parameters = new Dictionary(); - string atom = GetAtom("get-attribute.js"); + string atom = GetWrappedAtom("get_attribute", ResourceUtilities.GetAttributeAtom); parameters.Add("script", atom); parameters.Add("args", new object[] { ((IWebDriverObjectReference)this).ToDictionary(), attributeName }); @@ -707,20 +706,9 @@ protected virtual Response Execute(string commandToExecute, Dictionary