Skip to content

Commit

Permalink
Merge pull request #98 from tuvistavie/add-metadata-functionality
Browse files Browse the repository at this point in the history
Add metadata functionality.
  • Loading branch information
Daniel Perez committed Apr 4, 2016
2 parents 26f34d4 + 0e91ced commit 1a6f6f4
Show file tree
Hide file tree
Showing 19 changed files with 387 additions and 37 deletions.
11 changes: 4 additions & 7 deletions lib/hound.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ defmodule Hound do
end


@doc "Alias of Hound.Helpers.Session.start_session"
defdelegate start_session, to: Hound.Helpers.Session
defdelegate start_session(additional_capabilities), to: Hound.Helpers.Session
@doc "See `Hound.Helpers.Session.start_session/1`"
defdelegate start_session, to: Hound.Helpers.Session
defdelegate start_session(opts), to: Hound.Helpers.Session

@doc "Alias of Hound.Helpers.Session.end_session"
@doc "See `Hound.Helpers.Session.end_session/1`"
defdelegate end_session, to: Hound.Helpers.Session

@doc "Alias of Hound.Helpers.Session.end_session/1"
defdelegate end_session(pid), to: Hound.Helpers.Session

@doc false
defdelegate current_session_id, to: Hound.Helpers.Session

end
61 changes: 61 additions & 0 deletions lib/hound/browser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule Hound.Browser do
@moduledoc "Low level functions to customize browser behavior"

@type t :: Hound.BrowserLike.t

@callback default_user_agent :: String.t | atom

@callback user_agent_capabilities(String.t) :: map

@doc "Creates capabilities for the browser and options, to be sent to the webdriver"
@spec make_capabilities(t, map | Keyword.t) :: map
def make_capabilities(browser_name, opts \\ []) do
browser = browser(browser_name)

user_agent =
user_agent(opts[:user_agent] || browser.default_user_agent)
|> Hound.Metadata.append(opts[:metadata])

capabilities = %{browserName: browser_name}
ua_capabilities = browser.user_agent_capabilities(user_agent)

Map.merge(capabilities, ua_capabilities)
end

@doc "Returns a user agent string"
@spec user_agent(String.t | atom) :: String.t
def user_agent(ua) when is_binary(ua), do: ua

# bundle a few common user agents
def user_agent(:firefox_desktop) do
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"
end
def user_agent(:phantomjs) do
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"
end
def user_agent(:chrome_desktop) do
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
end
def user_agent(:chrome_android_sp) do
"Mozilla/5.0 (Linux; U; Android-4.0.3; en-us; Galaxy Nexus Build/IML74K) AppleWebKit/535.7 (KHTML, like Gecko) CrMo/16.0.912.75 Mobile Safari/535.7"
end
def user_agent(:chrome_iphone) do
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3"
end
def user_agent(:safari_iphone) do
"Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25"
end

# add some simpler aliases
def user_agent(:chrome), do: user_agent(:chrome_desktop)
def user_agent(:firefox), do: user_agent(:firefox_desktop)
def user_agent(:android), do: user_agent(:chrome_android_sp)
def user_agent(:iphone), do: user_agent(:safari_iphone)

defp browser(browser) when is_atom(browser) do
browser |> Atom.to_string |> browser()
end
defp browser("firefox"), do: Hound.Browser.Firefox
defp browser("chrome"), do: Hound.Browser.Chrome
defp browser("phantomjs"), do: Hound.Browser.PhantomJS
end
11 changes: 11 additions & 0 deletions lib/hound/browsers/chrome.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Hound.Browser.Chrome do
@moduledoc false

@behaviour Hound.Browser

def default_user_agent, do: :chrome

def user_agent_capabilities(ua) do
%{chromeOptions: %{"args" => ["--user-agent=#{ua}"]}}
end
end
14 changes: 14 additions & 0 deletions lib/hound/browsers/firefox.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule Hound.Browser.Firefox do
@moduledoc false

@behaviour Hound.Browser

alias Hound.Browser.Firefox.Profile

def default_user_agent, do: :firefox

def user_agent_capabilities(ua) do
{:ok, profile} = Profile.new |> Profile.set_user_agent(ua) |> Profile.dump
%{firefox_profile: profile}
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
defmodule Hound.Firefox.Profile do
defmodule Hound.Browser.Firefox.Profile do
@moduledoc false

@default_prefs %{
}

Expand Down
11 changes: 11 additions & 0 deletions lib/hound/browsers/phantomjs.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Hound.Browser.PhantomJS do
@moduledoc false

@behaviour Hound.Browser

def default_user_agent, do: :phantomjs

def user_agent_capabilities(ua) do
%{"phantomjs.page.settings.userAgent" => ua}
end
end
8 changes: 8 additions & 0 deletions lib/hound/exceptions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ defmodule Hound.NotSupportedError do
end
end
end

defmodule Hound.InvalidMetadataError do
defexception [:value]

def message(err) do
"could not parse metadata for value #{err.value}"
end
end
5 changes: 2 additions & 3 deletions lib/hound/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ defmodule Hound.Helpers do
end


defmacro hound_session do
defmacro hound_session(opts \\ []) do
quote do
setup do
Hound.start_session
on_exit fn-> Hound.end_session end
Hound.start_session(unquote(opts))

:ok
end
Expand Down
25 changes: 20 additions & 5 deletions lib/hound/helpers/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ defmodule Hound.Helpers.Session do
The name can be an atom or a string. The default session created is called `:default`.
"""
def change_session_to(session_name, additional_capabilities \\ %{}) do
Hound.SessionServer.change_current_session_for_pid(self, session_name, additional_capabilities)
def change_session_to(session_name, opts \\ []) do
Hound.SessionServer.change_current_session_for_pid(self, session_name, opts)
end


Expand Down Expand Up @@ -74,9 +74,25 @@ defmodule Hound.Helpers.Session do
end
end
## Options
The following options can be passed to `start_session`:
* `:browser` - The browser to be used (`"chrome"` | `"phantomjs"` | `"firefox"`)
* `:user_agent` - The user agent string that will be used for the requests.
The following atoms can also be passed
* `:firefox_desktop` (aliased to `:firefox`)
* `:chrome_desktop` (aliased to `:chrome`)
* `:phantomjs`
* `:chrome_android_sp` (aliased to `:android`)
* `:safari_iphone` (aliased to `:iphone`)
* `:metadata` - The metadata to be included in the requests.
See `Hound.Metadata` for more information
* `:driver` - The additional capabilities to be passed directly to the webdriver.
"""
def start_session(additional_capabilities \\ %{}) do
Hound.SessionServer.session_for_pid(self, additional_capabilities)
def start_session(opts \\ []) do
Hound.SessionServer.session_for_pid(self, opts)
end


Expand All @@ -95,5 +111,4 @@ defmodule Hound.Helpers.Session do
Hound.SessionServer.current_session_id(self) ||
raise "could not find a session for process #{inspect self}"
end

end
73 changes: 73 additions & 0 deletions lib/hound/metadata.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Hound.Metadata do
@moduledoc """
Metadata allows to pass and extract custom data through.
This can be useful if you need to identify sessions.
The keys and values must be serializable using `:erlang.term_to_binary/1`.
## Examples
You can start a session using metadata by doing the following:
Hound.start_session(metadata: %{pid: self()})
If you need to retrieve the metadata, you simply need to use
`Hound.Metadata.extract/1` on the user agent string, so supposing you are using plug,
user_agent = conn |> get_resp_header("user-agent") |> List.first
metadata = Hound.Metadata.extract(user_agent)
assert %{pid: pid} = metadata
# you can use your pid here
"""

@metadata_prefix "BeamMetadata"
@extract_regexp ~r{#{@metadata_prefix} \((.*?)\)}

@doc """
Appends the metdata to the user_agent string.
"""
@spec append(String.t, nil | map | String.t) :: String.t
def append(user_agent, nil), do: user_agent
def append(user_agent, metadata) when is_map(metadata) or is_list(metadata) do
append(user_agent, format(metadata))
end
def append(user_agent, metadata) when is_binary(metadata) do
"#{user_agent}/#{metadata}"
end

@doc """
Formats a string to a valid UserAgent string to be passed to be
appended to the browser user agent.
"""
@spec format(map | Keyword.t) :: String.t
def format(metadata) do
encoded = {:v1, metadata} |> :erlang.term_to_binary |> Base.url_encode64
"#{@metadata_prefix} (#{encoded})"
end

@doc """
Extracts and parses the metadata contained in a user agent string.
If the user agent does not contain any metadata, an empty map is returned.
"""
@spec parse(String.t) :: %{String.t => String.t}
def extract(str) do
ua_last_part = str |> String.split("/") |> List.last
case Regex.run(@extract_regexp, ua_last_part) do
[_, metadata] -> parse(metadata)
_ -> %{}
end
end

defp parse(encoded_metadata) do
encoded_metadata
|> Base.url_decode64!
|> :erlang.binary_to_term
|> case do
{:v1, metadata} -> metadata
_ -> raise Hound.InvalidMetadataError, value: encoded_metadata
end
end
end
31 changes: 18 additions & 13 deletions lib/hound/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,34 @@ defmodule Hound.Session do


@doc "Creates a session associated with the current pid"
@spec create_session(String.t, map) :: {:ok, String.t}
def create_session(browser_name, additional_capabilities) do
base_capabilities = %{
@spec create_session(Hound.Browser.t, map | Keyword.t) :: {:ok, String.t}
def create_session(browser, opts) do
capabilities = make_capabilities(browser, opts)
params = %{
desiredCapabilities: capabilities
}

# No retries for this request
make_req(:post, "session", params)
end

@doc "Make capabilities for session"
@spec make_capabilities(Hound.Browser.t, map | Keyword.t) :: map
def make_capabilities(browser, opts \\ []) do
browser = opts[:browser] || browser
%{
javascriptEnabled: false,
version: "",
rotatable: false,
takesScreenshot: true,
cssSelectorsEnabled: true,
browserName: browser_name,
nativeEvents: false,
platform: "ANY"
}

params = %{
desiredCapabilities: Map.merge(base_capabilities, additional_capabilities)
}

# No retries for this request
make_req(:post, "session", params)
|> Map.merge(Hound.Browser.make_capabilities(browser, opts))
|> Map.merge(opts[:driver] || %{})
end


@doc "Get capabilities of a particular session"
@spec session_info(String.t) :: map
def session_info(session_id) do
Expand All @@ -59,5 +65,4 @@ defmodule Hound.Session do
def set_timeout(session_id, operation, time) do
make_req(:post, "session/#{session_id}/timeouts", %{type: operation, ms: time})
end

end
12 changes: 6 additions & 6 deletions lib/hound/session_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ defmodule Hound.SessionServer do
end


def session_for_pid(pid, additional_capabilities) do
def session_for_pid(pid, opts) do
current_session_id(pid) ||
change_current_session_for_pid(pid, :default, additional_capabilities)
change_current_session_for_pid(pid, :default, opts)
end


Expand All @@ -23,8 +23,8 @@ defmodule Hound.SessionServer do
end


def change_current_session_for_pid(pid, session_name, additional_capabilities) do
GenServer.call(@name, {:change_session, pid, session_name, additional_capabilities}, 60000)
def change_current_session_for_pid(pid, session_name, opts) do
GenServer.call(@name, {:change_session, pid, session_name, opts}, 60000)
end


Expand All @@ -48,7 +48,7 @@ defmodule Hound.SessionServer do
end


def handle_call({:change_session, pid, session_name, additional_capabilities}, _from, state) do
def handle_call({:change_session, pid, session_name, opts}, _from, state) do
{:ok, driver_info} = Hound.driver_info

{ref, sessions} =
Expand All @@ -64,7 +64,7 @@ defmodule Hound.SessionServer do
{:ok, session_id} ->
{session_id, sessions}
:error ->
{:ok, session_id} = Hound.Session.create_session(driver_info[:browser], additional_capabilities)
{:ok, session_id} = Hound.Session.create_session(driver_info[:browser], opts)
{session_id, Map.put(sessions, session_name, session_id)}
end

Expand Down
Loading

0 comments on commit 1a6f6f4

Please sign in to comment.