# `Expresso.Check.Browser`
[🔗](https://github.com/rellen/expresso/blob/main/lib/expresso/check/browser.ex#L1)

Runs the layout check and the accessibility check of a document in a headless browser

`--check=layout` and `--check=a11y` need Chrome or Chromium. The function
`find/1` finds the browser, and `find_axe/2` finds axe-core for
`--check=a11y`. `run/3` writes a copy of the document to a temporary
directory, with axe-core in it for `--check=a11y`. The browser opens the copy
with `?check` or `?check=a11y`, and it writes the document to its standard
output after the check. The script of the page writes the result into an
element `check-result`. `assets/src/check.ts` gives the rules.

The page has the size 1920 × 1080. A browser can keep a part of the window
for itself, so `run/3` first opens a small page that writes its size, and it
makes the window larger by the difference. A Chrome window is not narrower
than 500 px, so `run/3` returns an error when the page does not get the
requested size.

# `result`

```elixir
@type result() :: %{
  width: pos_integer(),
  height: pos_integer(),
  layout: [String.t()],
  accessibility: [map()] | nil
}
```

The result of the page: the size, the problems of the layout and the violations of axe-core

# `decode`

```elixir
@spec decode(String.t()) :: {:ok, result()} | {:error, String.t()}
```

Read the JSON of the element `check-result`

    iex> Expresso.Check.Browser.decode(~s({"width":1920,"height":1080,"layout":["x"],"accessibility":[]}))
    {:ok, %{width: 1920, height: 1080, layout: ["x"], accessibility: []}}

# `find`

```elixir
@spec find(%{required(String.t()) =&gt; String.t()}) ::
  {:ok, Path.t()} | {:error, String.t()}
```

Find Chrome or Chromium

The variable `EXPRESSO_CHROMIUM` in `env` gives the path of the browser.
Without it, the function looks in this sequence:

1. The browsers of Playwright, as `playwright/1` finds them. The headless
   shell starts fast, and it gives the page its size exactly.
2. Each usual name of the browser on the `PATH`. Chrome comes before
   Chromium. The function skips a command that installs a snap, such as
   `chromium` on Ubuntu, because that command does not end.
3. The usual places of Chrome and Chromium on macOS.

# `find_axe`

```elixir
@spec find_axe([Path.t()], %{required(String.t()) =&gt; String.t()}) ::
  {:ok, Path.t()} | {:error, String.t()}
```

Find the file `axe.min.js` of axe-core

The variable `EXPRESSO_AXE` in `env` gives the path of the file. Without it,
the function looks for `node_modules/axe-core/axe.min.js` in each directory
of `directories`, in each parent of them, and in the directory of
`Expresso.Check.Tools`.

# `places`

```elixir
@spec places([Path.t()], %{required(String.t()) =&gt; String.t()}) :: [Path.t()]
```

Return the directories where the check looks for `node_modules`

The list holds each directory of `directories`, then each parent of them,
and then the directory of `Expresso.Check.Tools.directory/1`. Thus a
directory of `directories` comes before the parent of a different one.

    iex> Expresso.Check.Browser.places(["/a/b", "/c"], %{"EXPRESSO_TOOLS" => "/t"})
    ["/a/b", "/c", "/a", "/", "/t"]

# `playwright`

```elixir
@spec playwright(%{required(String.t()) =&gt; String.t()}) :: Path.t() | nil
```

Return the path of a browser of Playwright, or `nil`

The variable `PLAYWRIGHT_BROWSERS_PATH` in `env` gives the directory of the
browsers, as for Playwright. Without it, the function looks in the cache of
Playwright on Linux and on macOS. It prefers the headless shell, and the
latest version of each browser, by the integer of its revision.

# `result`

```elixir
@spec result(String.t()) :: {:ok, result()} | {:error, String.t()}
```

Read the result of the page from the document that the browser writes

    iex> dom = ~s(<body><script type="application/json" id="check-result">) <>
    ...>   ~s({"width":1920,"height":1080,"layout":[],"accessibility":null}</script></body>)
    iex> Expresso.Check.Browser.result(dom)
    {:ok, %{width: 1920, height: 1080, layout: [], accessibility: nil}}

# `run`

```elixir
@spec run(String.t(), Path.t(), Path.t() | nil, keyword()) ::
  {:ok, result()} | {:error, String.t()}
```

Run the check of the page on a document in the browser, and return the result

With `axe`, the path of `axe.min.js`, the page also runs axe-core. The
function returns an error when the browser does not start, or when the page
writes no result. The option `:probe_timeout` gives the time in
milliseconds for the small page of the size, 30 000 by default, and
`:timeout` gives the time for the check, 600 000 by default. The option
`:scheme`, `:light` or `:dark`, shows that variant of a theme with two
variants, with `?scheme=` in the address. The option `:size`, such as
`{1280, 720}`, gives the size of the page, and it is `{1920, 1080}` by
default.

# `same_size`

```elixir
@spec same_size(result(), {pos_integer(), pos_integer()}, Path.t()) ::
  {:ok, result()} | {:error, String.t()}
```

Return the result when the page has the requested size, or an error

A Chrome window is not narrower than 500 px, so Chrome can give a page that
is larger than the size of `--size`. The values of such a check come from
the wrong size, so the function returns an error. The headless shell gives
each size.

    iex> page = %{width: 1920, height: 1080, layout: [], accessibility: nil}
    iex> Expresso.Check.Browser.same_size(page, {1920, 1080}, "chrome")
    {:ok, %{width: 1920, height: 1080, layout: [], accessibility: nil}}
    iex> page = %{width: 500, height: 667, layout: [], accessibility: nil}
    iex> {:error, message} = Expresso.Check.Browser.same_size(page, {375, 667}, "chrome")
    iex> message
    "The browser chrome made a page of 500 × 667, and not of 375 × 667. Chrome cannot " <>
      "make a page narrower than 500 px. Use the headless shell of Playwright in " <>
      "EXPRESSO_CHROMIUM, or a larger --size."

# `scheme`

```elixir
@spec scheme(String.t(), :light | :dark | nil) :: String.t()
```

Add the variant of a theme to the address of a page

    iex> Expresso.Check.Browser.scheme("file:///d.html?check", :dark)
    "file:///d.html?check&scheme=dark"
    iex> Expresso.Check.Browser.scheme("file:///d.html?check", nil)
    "file:///d.html?check"

# `snap?`

```elixir
@spec snap?(Path.t()) :: boolean()
```

Return true for a command that runs or installs a snap

A command of a snap is a link to `/usr/bin/snap`, or a short script that
names `snap`, as `chromium-browser` on Ubuntu is.

# `with_axe`

```elixir
@spec with_axe(String.t(), Path.t() | nil) :: String.t()
```

Put axe-core into a document before the end of its `body`

`axe` is the path of `axe.min.js`, or `nil` for no change. A `</script` in
the source of axe-core would close the element, so it gets a backslash,
which JavaScript reads as the same text. `Expresso.Check.Session` also
calls this function.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
