diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c18d1d08..4cbb06fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - elixir-otp: [{otp: 21.x, elixir: 1.8.x}, {otp: 21.x, elixir: 1.9.x}, {otp: 21.x, elixir: 1.10.x}, {otp: 21.x, elixir: 1.11.x}, {otp: 22.x, elixir: 1.12.x}] + elixir-otp: [{otp: 23.x, elixir: 1.12.x}, {otp: 25.x, elixir: 1.13.x}] fail-fast: false env: @@ -46,7 +46,7 @@ jobs: strategy: matrix: - elixir-otp: [{otp: 21.x, elixir: 1.8.x}, {otp: 21.x, elixir: 1.9.x}, {otp: 21.x, elixir: 1.10.x}, {otp: 21.x, elixir: 1.11.x}, {otp: 22.x, elixir: 1.12.x}] + elixir-otp: [{otp: 23.x, elixir: 1.12.x}, {otp: 25.x, elixir: 1.13.x}] fail-fast: false steps: @@ -92,7 +92,7 @@ jobs: strategy: matrix: - elixir-otp: [{otp: 21.x, elixir: 1.8.x}, {otp: 21.x, elixir: 1.9.x}, {otp: 21.x, elixir: 1.10.x}, {otp: 21.x, elixir: 1.11.x}, {otp: 22.x, elixir: 1.12.x}] + elixir-otp: [{otp: 23.x, elixir: 1.12.x}, {otp: 25.x, elixir: 1.13.x}] fail-fast: false env: @@ -128,7 +128,7 @@ jobs: strategy: matrix: - elixir-otp: [{otp: 21.x, elixir: 1.8.x}, {otp: 21.x, elixir: 1.9.x}, {otp: 21.x, elixir: 1.10.x}, {otp: 21.x, elixir: 1.11.x}, {otp: 22.x, elixir: 1.12.x}] + elixir-otp: [{otp: 23.x, elixir: 1.12.x}, {otp: 25.x, elixir: 1.13.x}] fail-fast: false env: diff --git a/.tool-versions b/.tool-versions index f3366be8..11543235 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.10.4 +elixir 1.12.3 erlang 23.3.4.4 diff --git a/LICENSE.md b/LICENSE.md index cb8d6c72..f895f303 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2016 Chris Keathley +Copyright (c) 2022 Mitchell Hanberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 00000000..9d0fcfe0 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,46 @@ +LEGAL NOTICE INFORMATION +------------------------ + +All the files in this distribution are copyright to the terms below. + +== lib/wallaby/partition_supervisor.ex + +Copyright 2012 Plataformatec +Copyright 2021 The Elixir Team + +Licensed 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 + + https://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. + +== All other files + +The MIT License (MIT) + +Copyright (c) 2016 Chris Keathley +Copyright (c) 2022 Mitchell Hanberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/integration_test/chrome/test_helper.exs b/integration_test/chrome/test_helper.exs index 0eb9389e..69a3d4ba 100644 --- a/integration_test/chrome/test_helper.exs +++ b/integration_test/chrome/test_helper.exs @@ -1,4 +1,4 @@ -ExUnit.configure(max_cases: 4, exclude: [pending: true]) +ExUnit.configure(exclude: [pending: true]) ExUnit.start() # Load support files diff --git a/lib/wallaby.ex b/lib/wallaby.ex index fc5f6801..86fe4978 100644 --- a/lib/wallaby.ex +++ b/lib/wallaby.ex @@ -35,7 +35,10 @@ defmodule Wallaby do children = [ {driver(), [name: Wallaby.Driver.Supervisor]}, - :hackney_pool.child_spec(:wallaby_pool, timeout: 15_000, max_connections: 4), + :hackney_pool.child_spec(:wallaby_pool, + timeout: 15_000, + max_connections: System.schedulers_online() + ), {Wallaby.SessionStore, [name: Wallaby.SessionStore]} ] diff --git a/lib/wallaby/browser.ex b/lib/wallaby/browser.ex index d86cba9b..f54e2283 100644 --- a/lib/wallaby/browser.ex +++ b/lib/wallaby/browser.ex @@ -1181,9 +1181,7 @@ defmodule Wallaby.Browser do _ -> raise Wallaby.ExpectationNotMetError, - "Wallaby has encountered an internal error: #{inspect(error)} with session: #{ - inspect(parent) - }" + "Wallaby has encountered an internal error: #{inspect(error)} with session: #{inspect(parent)}" end end end diff --git a/lib/wallaby/chrome.ex b/lib/wallaby/chrome.ex index 15eb3c2d..faacb49f 100644 --- a/lib/wallaby/chrome.ex +++ b/lib/wallaby/chrome.ex @@ -106,7 +106,7 @@ defmodule Wallaby.Chrome do @behaviour Wallaby.Driver - @default_readiness_timeout 5_000 + @default_readiness_timeout 10_000 alias Wallaby.Chrome.Chromedriver alias Wallaby.WebdriverClient @@ -144,7 +144,10 @@ defmodule Wallaby.Chrome do def init(_) do children = [ Wallaby.Driver.LogStore, - Wallaby.Chrome.Chromedriver + {PartitionSupervisor, + child_spec: Wallaby.Chrome.Chromedriver, + name: Wallaby.Chromedrivers, + partitions: System.schedulers_online()} ] Supervisor.init(children, strategy: :one_for_one) diff --git a/lib/wallaby/chrome/chromedriver.ex b/lib/wallaby/chrome/chromedriver.ex index c125d43b..990eb0c7 100644 --- a/lib/wallaby/chrome/chromedriver.ex +++ b/lib/wallaby/chrome/chromedriver.ex @@ -4,20 +4,20 @@ defmodule Wallaby.Chrome.Chromedriver do alias Wallaby.Chrome alias Wallaby.Chrome.Chromedriver.Server - @instance __MODULE__ - def child_spec(_arg) do {:ok, chromedriver_path} = Chrome.find_chromedriver_executable() - Server.child_spec([chromedriver_path, [name: @instance]]) + Server.child_spec([chromedriver_path, []]) end @spec wait_until_ready(timeout()) :: :ok | {:error, :timeout} def wait_until_ready(timeout) do - Server.wait_until_ready(@instance, timeout) + process_name = {:via, PartitionSupervisor, {Wallaby.Chromedrivers, self()}} + Server.wait_until_ready(process_name, timeout) end @spec base_url :: String.t() def base_url do - Server.get_base_url(@instance) + process_name = {:via, PartitionSupervisor, {Wallaby.Chromedrivers, self()}} + Server.get_base_url(process_name) end end diff --git a/lib/wallaby/partition_supervisor.ex b/lib/wallaby/partition_supervisor.ex new file mode 100644 index 00000000..0df4cf55 --- /dev/null +++ b/lib/wallaby/partition_supervisor.ex @@ -0,0 +1,389 @@ +unless Code.ensure_loaded?(PartitionSupervisor) do + defmodule PartitionSupervisor do + @moduledoc """ + A supervisor that starts multiple partitions of the same child. + + Certain processes may become bottlenecks in large systems. + If those processes can have their state trivially partitioned, + in a way there is no dependency between them, then they can use + the `PartitionSupervisor` to create multiple isolated and + independent partitions. + + Once the `PartitionSupervisor` starts, you can dispatch to its + children using `{:via, PartitionSupervisor, {name, key}}`, where + `name` is the name of the `PartitionSupervisor` and key is used + for routing. + + ## Example + + The `DynamicSupervisor` is a single process responsible for starting + other processes. In some applications, the `DynamicSupervisor` may + become a bottleneck. To address this, you can start multiple instances + of the `DynamicSupervisor` through a `PartitionSupervisor`, and then + pick a "random" instance to start the child on. + + Instead of starting a single `DynamicSupervisor`: + + children = [ + {DynamicSupervisor, name: MyApp.DynamicSupervisor} + ] + + Supervisor.start_link(children, strategy: :one_for_one) + + and starting children on that dynamic supervisor directly: + + DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end}) + + You can do start the dynamic supervisors under a `PartitionSupervisor`: + + children = [ + {PartitionSupervisor, + child_spec: DynamicSupervisor, + name: MyApp.DynamicSupervisors} + ] + + Supervisor.start_link(children, strategy: :one_for_one) + + and then: + + DynamicSupervisor.start_child( + {:via, PartitionSupervisor, {MyApp.DynamicSupervisors, self()}}, + {Agent, fn -> %{} end} + ) + + In the code above, we start a partition supervisor that will by default + start a dynamic supervisor for each core in your machine. Then, instead + of calling the `DynamicSupervisor` by name, you call it through the + partition supervisor using the `{:via, PartitionSupervisor, {name, key}}` + format. We picked `self()` as the routing key, which means each process + will be assigned one of the existing dynamic supervisors. See `start_link/1` + to see all options supported by the `PartitionSupervisor`. + + ## Implementation notes + + The `PartitionSupervisor` uses either an ETS table or a `Registry` to + manage all of the partitions. Under the hood, the `PartitionSupervisor` + generates a child spec for each partition and then acts as a regular + supervisor. The ID of each child spec is the partition number. + + For routing, two strategies are used. If `key` is an integer, it is routed + using `rem(abs(key), partitions)` where `partitions` is the number of + partitions. Otherwise it uses `:erlang.phash2(key, partitions)`. + The particular routing may change in the future, and therefore must not + be relied on. If you want to retrieve a particular PID for a certain key, + you can use `GenServer.whereis({:via, PartitionSupervisor, {name, key}})`. + """ + + @behaviour Supervisor + + @registry PartitionSupervisor.Registry + + @typedoc """ + The name of the `PartitionSupervisor`. + """ + @type name :: atom() | {:via, module(), term()} + + @doc false + def child_spec(opts) when is_list(opts) do + id = + case Keyword.get(opts, :name, DynamicSupervisor) do + name when is_atom(name) -> name + {:via, _module, name} -> name + end + + %{ + id: id, + start: {PartitionSupervisor, :start_link, [opts]}, + type: :supervisor + } + end + + @doc """ + Starts a partition supervisor with the given options. + + This function is typically not invoked directly, instead it is invoked + when using a `PartitionSupervisor` as a child of another supervisor: + + children = [ + {PartitionSupervisor, child_spec: SomeChild, name: MyPartitionSupervisor} + ] + + If the supervisor is successfully spawned, this function returns + `{:ok, pid}`, where `pid` is the PID of the supervisor. If the given name + for the partition supervisor is already assigned to a process, + the function returns `{:error, {:already_started, pid}}`, where `pid` + is the PID of that process. + + Note that a supervisor started with this function is linked to the parent + process and exits not only on crashes but also if the parent process exits + with `:normal` reason. + + ## Options + + * `:name` - an atom or via tuple representing the name of the partition + supervisor (see `t:name/0`). + + * `:partitions` - a positive integer with the number of partitions. + Defaults to `System.schedulers_online()` (typically the number of cores). + + * `:strategy` - the restart strategy option, defaults to `:one_for_one`. + You can learn more about strategies in the `Supervisor` module docs. + + * `:max_restarts` - the maximum number of restarts allowed in + a time frame. Defaults to `3`. + + * `:max_seconds` - the time frame in which `:max_restarts` applies. + Defaults to `5`. + + * `:with_arguments` - a two-argument anonymous function that allows + the partition to be given to the child starting function. See the + `:with_arguments` section below. + + ## `:with_arguments` + + Sometimes you want each partition to know their partition assigned number. + This can be done with the `:with_arguments` option. This function receives + the list of arguments of the child specification and the partition. It + must return a new list of arguments that will be passed to the child specification + of children. + + For example, most processes are started by calling `start_link(opts)`, + where `opts` is a keyword list. You could inject the partition into the + options given to the child: + + with_arguments: fn [opts], partition -> + [Keyword.put(opts, :partition, partition)] + end + + """ + @doc since: "1.14.0" + @spec start_link(keyword) :: Supervisor.on_start() + def start_link(opts) when is_list(opts) do + name = opts[:name] + + unless name do + raise ArgumentError, "the :name option must be given to PartitionSupervisor" + end + + {child_spec, opts} = Keyword.pop(opts, :child_spec) + + unless child_spec do + raise ArgumentError, "the :child_spec option must be given to PartitionSupervisor" + end + + {partitions, opts} = Keyword.pop(opts, :partitions, System.schedulers_online()) + + unless is_integer(partitions) and partitions >= 1 do + raise ArgumentError, + "the :partitions option must be a positive integer, got: #{inspect(partitions)}" + end + + {with_arguments, opts} = Keyword.pop(opts, :with_arguments, fn args, _partition -> args end) + + unless is_function(with_arguments, 2) do + raise ArgumentError, + "the :with_arguments option must be a function that receives two arguments, " <> + "the current call arguments and the partition, got: #{inspect(with_arguments)}" + end + + %{start: {mod, fun, args}} = map = Supervisor.child_spec(child_spec, []) + modules = map[:modules] || [mod] + + children = + for partition <- 0..(partitions - 1) do + args = with_arguments.(args, partition) + + unless is_list(args) do + raise "the call to the function in :with_arguments must return a list, got: #{inspect(args)}" + end + + start = {__MODULE__, :start_child, [mod, fun, args, name, partition]} + Map.merge(map, %{id: partition, start: start, modules: modules}) + end + + {init_opts, start_opts} = Keyword.split(opts, [:strategy, :max_seconds, :max_restarts]) + Supervisor.start_link(__MODULE__, {name, partitions, children, init_opts}, start_opts) + end + + @doc false + def start_child(mod, fun, args, name, partition) do + case apply(mod, fun, args) do + {:ok, pid} -> + register_child(name, partition, pid) + {:ok, pid} + + {:ok, pid, info} -> + register_child(name, partition, pid) + {:ok, pid, info} + + other -> + other + end + end + + defp register_child(name, partition, pid) when is_atom(name) do + :ets.insert(name, {partition, pid}) + end + + defp register_child({:via, _, _}, partition, pid) do + Registry.register(@registry, {self(), partition}, pid) + end + + @impl true + def init({name, partitions, children, init_opts}) do + init_partitions(name, partitions) + Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one)) + end + + defp init_partitions(name, partitions) when is_atom(name) do + :ets.new(name, [:set, :named_table, :protected, read_concurrency: true]) + :ets.insert(name, {:partitions, partitions}) + end + + defp init_partitions({:via, _, _}, partitions) do + child_spec = {Registry, keys: :unique, name: @registry} + + unless Process.whereis(@registry) do + Supervisor.start_child(:elixir_sup, child_spec) + end + + Registry.register(@registry, self(), partitions) + end + + @doc """ + Returns the number of partitions for the partition supervisor. + """ + @doc since: "1.14.0" + @spec partitions(name()) :: pos_integer() + def partitions(name) do + {_name, partitions} = name_partitions(name) + partitions + end + + # For whereis_name, we want to lookup on GenServer.whereis/1 + # just once, so we lookup the name and partitions together. + defp name_partitions(name) when is_atom(name) do + # credo:disable-for-next-line + try do + {name, :ets.lookup_element(name, :partitions, 2)} + rescue + _ -> exit({:noproc, {__MODULE__, :partitions, [name]}}) + end + end + + defp name_partitions(name) when is_tuple(name) do + with pid when is_pid(pid) <- GenServer.whereis(name), + [name_partitions] <- Registry.lookup(@registry, pid) do + name_partitions + else + _ -> exit({:noproc, {__MODULE__, :partitions, [name]}}) + end + end + + @doc """ + Returns a list with information about all children. + + This function returns a list of tuples containing: + + * `id` - the partition number + + * `child` - the PID of the corresponding child process or the + atom `:restarting` if the process is about to be restarted + + * `type` - `:worker` or `:supervisor` as defined in the child + specification + + * `modules` - as defined in the child specification + + """ + @doc since: "1.14.0" + @spec which_children(name()) :: [ + # Inlining [module()] | :dynamic here because :supervisor.modules() is not exported + {:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic} + ] + def which_children(name) when is_atom(name) or elem(name, 0) == :via do + Supervisor.which_children(name) + end + + @doc """ + Returns a map containing count values for the supervisor. + + The map contains the following keys: + + * `:specs` - the number of partitions (children processes) + + * `:active` - the count of all actively running child processes managed by + this supervisor + + * `:supervisors` - the count of all supervisors whether or not the child + process is still alive + + * `:workers` - the count of all workers, whether or not the child process + is still alive + + """ + @doc since: "1.14.0" + @spec count_children(name()) :: %{ + specs: non_neg_integer, + active: non_neg_integer, + supervisors: non_neg_integer, + workers: non_neg_integer + } + def count_children(supervisor) when is_atom(supervisor) do + Supervisor.count_children(supervisor) + end + + @doc """ + Synchronously stops the given partition supervisor with the given `reason`. + + It returns `:ok` if the supervisor terminates with the given + reason. If it terminates with another reason, the call exits. + + This function keeps OTP semantics regarding error reporting. + If the reason is any other than `:normal`, `:shutdown` or + `{:shutdown, _}`, an error report is logged. + """ + @doc since: "1.14.0" + @spec stop(name(), reason :: term, timeout) :: :ok + def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do + Supervisor.stop(supervisor, reason, timeout) + end + + ## Via callbacks + + @doc false + def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do + {name, partitions} = name_partitions(name) + + partition = + if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions) + + whereis_name(name, partition) + end + + defp whereis_name(name, partition) when is_atom(name) do + :ets.lookup_element(name, partition, 2) + end + + defp whereis_name(name, partition) when is_pid(name) do + @registry + |> Registry.values({name, partition}, name) + |> List.first(:undefined) + end + + @doc false + def send(name_key, msg) do + Kernel.send(whereis_name(name_key), msg) + end + + @doc false + def register_name(_, _) do + raise "{:via, PartitionSupervisor, _} cannot be given on registration" + end + + @doc false + def unregister_name(_, _) do + raise "{:via, PartitionSupervisor, _} cannot be given on unregistration" + end + end +end diff --git a/lib/wallaby/query/error_message.ex b/lib/wallaby/query/error_message.ex index c567d380..e52abf08 100644 --- a/lib/wallaby/query/error_message.ex +++ b/lib/wallaby/query/error_message.ex @@ -105,13 +105,7 @@ defmodule Wallaby.Query.ErrorMessage do defp found_error_message(query) do """ - #{expected_count(query)} #{visibility_and_selection(query)} #{method(query)} #{ - selector(query) - }#{with_index(Query.at_number(query))}, but #{result_adverb(query)}#{ - result_count(query.result) - } #{visibility_and_selection(query)} #{short_method(query.method, Enum.count(query.result))} #{ - result_expectation(query.result) - }. + #{expected_count(query)} #{visibility_and_selection(query)} #{method(query)} #{selector(query)}#{with_index(Query.at_number(query))}, but #{result_adverb(query)}#{result_count(query.result)} #{visibility_and_selection(query)} #{short_method(query.method, Enum.count(query.result))} #{result_expectation(query.result)}. """ end diff --git a/lib/wallaby/query/xpath.ex b/lib/wallaby/query/xpath.ex index a80567cb..ab9e044e 100644 --- a/lib/wallaby/query/xpath.ex +++ b/lib/wallaby/query/xpath.ex @@ -15,9 +15,7 @@ defmodule Wallaby.Query.XPath do https://github.com/jnicklas/xpath/blob/master/lib/xpath/html.rb """ def link(lnk) do - ~s{.//a[./@href][(((./@id = "#{lnk}" or contains(normalize-space(string(.)), "#{lnk}")) or contains(./@title, "#{ - lnk - }")) or .//img[contains(./@alt, "#{lnk}")])]} + ~s{.//a[./@href][(((./@id = "#{lnk}" or contains(normalize-space(string(.)), "#{lnk}")) or contains(./@title, "#{lnk}")) or .//img[contains(./@alt, "#{lnk}")])]} end @doc """ @@ -27,9 +25,7 @@ defmodule Wallaby.Query.XPath do types = "./@type = 'submit' or ./@type = 'reset' or ./@type = 'button' or ./@type = 'image'" locator = - ~s{(((./@id = "#{query}" or ./@name = "#{query}" or ./@value = "#{query}" or ./@alt = "#{ - query - }" or ./@title = "#{query}" or contains(normalize-space(string(.)), "#{query}"))))} + ~s{(((./@id = "#{query}" or ./@name = "#{query}" or ./@value = "#{query}" or ./@alt = "#{query}" or ./@title = "#{query}" or contains(normalize-space(string(.)), "#{query}"))))} ~s{.//input[#{types}][#{locator}] | .//button[(not(./@type) or #{types})][#{locator}]} end @@ -38,11 +34,7 @@ defmodule Wallaby.Query.XPath do Match any radio buttons """ def radio_button(query) do - ~s{.//input[./@type = 'radio'][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{ - query - }") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{ - query - }")]//.//input[./@type = "radio"]} + ~s{.//input[./@type = 'radio'][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = "radio"]} end @doc """ @@ -51,33 +43,21 @@ defmodule Wallaby.Query.XPath do `hidden`, or `file`. """ def fillable_field(query) when is_binary(query) do - ~s{.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = "#{ - query - }" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{ - query - }")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')]} + ~s{.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')]} end @doc """ Match any checkboxes """ def checkbox(query) do - ~s{.//input[./@type = 'checkbox'][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{ - query - }") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{ - query - }")]//.//input[./@type = "checkbox"]} + ~s{.//input[./@type = 'checkbox'][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = "checkbox"]} end @doc """ Match any `select` by name, id, or label. """ def select(query) do - ~s{.//select[(((./@id = "#{query}" or ./@name = "#{query}")) or ./@name = //label[contains(normalize-space(string(.)), "#{ - query - }")]/@for or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{ - query - }")]//.//select} + ~s{.//select[(((./@id = "#{query}" or ./@name = "#{query}")) or ./@name = //label[contains(normalize-space(string(.)), "#{query}")]/@for or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//select} end @doc """ @@ -91,9 +71,7 @@ defmodule Wallaby.Query.XPath do Matches any file field by name, id, or label """ def file_field(query) do - ~s{.//input[./@type = 'file'][(((./@id = "#{query}" or ./@name = "#{query}")) or ./@id = //label[contains(normalize-space(string(.)), "#{ - query - }")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = 'file']} + ~s{.//input[./@type = 'file'][(((./@id = "#{query}" or ./@name = "#{query}")) or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = 'file']} end @doc """ diff --git a/lib/wallaby/selenium.ex b/lib/wallaby/selenium.ex index 08396d8b..e5184c2f 100644 --- a/lib/wallaby/selenium.ex +++ b/lib/wallaby/selenium.ex @@ -28,7 +28,7 @@ defmodule Wallaby.Selenium do ### Selenium Remote URL It is possible to globally set Selenium's "Remote URL" by setting the following option. - + By default it is http://localhost:4444/wd/hub/ ```elixir @@ -382,11 +382,13 @@ defmodule Wallaby.Selenium do zipfile end + @binread_arg if Version.parse!(System.version()).minor >= 13, do: :eof, else: :all + # Base64 encode the zipfile for transfer to remote Selenium defp encode_zipfile(zipfile) do File.open!(zipfile, [:read, :raw], fn f -> f - |> IO.binread(:all) + |> IO.binread(@binread_arg) |> Base.encode64() end) end diff --git a/mix.exs b/mix.exs index f705ab97..751440b3 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,7 @@ defmodule Wallaby.Mixfile do [ app: :wallaby, version: @version, - elixir: "~> 1.8", + elixir: "~> 1.12", elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, @@ -48,7 +48,7 @@ defmodule Wallaby.Mixfile do {:dialyxir, "~> 1.0", only: :dev, runtime: false}, {:benchee, "~> 0.9", only: :dev}, {:benchee_html, "~> 0.3", only: :dev}, - {:credo, "~> 1.6.0-rc.1", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.6.4", only: [:dev, :test], runtime: false}, {:bypass, "~> 1.0.0", only: :test}, {:ex_doc, "~> 0.23", only: :dev}, {:ecto_sql, ">= 3.0.0", optional: true}, diff --git a/mix.lock b/mix.lock index 4fa5c709..83326061 100644 --- a/mix.lock +++ b/mix.lock @@ -8,7 +8,7 @@ "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, - "credo": {:hex, :credo, "1.6.0-rc.1", "6c6890e07a5c7517e32926ecebbfc288e9081a3847676bb525a0dc910a172983", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ed372185f79f0b2e1859a88bed910866cd9a0ca0763b8f8d7a28ff334c51eb86"}, + "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "deep_merge": {:hex, :deep_merge, "0.1.1", "c27866a7524a337b6a039eeb8dd4f17d458fd40fbbcb8c54661b71a22fffe846", [:mix], [], "hexpm", "7dd57812332d76067deb9611b93cc98ac74cbb3b09480eef80c7e3c5a0813ecf"}, @@ -22,7 +22,7 @@ "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d480e9b89a1da274393c0a17cd548581ce7b2c45ac9d5ae2cdda8fd66d906295"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, diff --git a/test/test_helper.exs b/test/test_helper.exs index 9f016fd0..87c79f93 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,6 +1,4 @@ ExUnit.configure(exclude: [pending: true]) EventEmitter.start_link([]) -IO.inspect(System.schedulers_online(), label: "online schedulers") - ExUnit.start()