# `LiveLoad.Scenario.Context`
[🔗](https://github.com/probably-not/live-load/blob/v0.1.1/lib/live_load/scenario/context.ex#L1)

A context struct that is given to every `c:LiveLoad.Scenario.run/3` callback while running a `LiveLoad.Scenario`.
It wraps the `LiveLoad.Browser.Context` details for the runner and provides a simple API for interacting with the browser.

This module's API is based on Plug.Conn, using the following patterns:
- A scenario that is `:halted?` at any point in the scenario will short-circuit and not run any other functions.
- A scenario that has an `:error` at any point in the scenario will short-circuit and not run any other functions.
- Values can be extracted from the page through the available functions and `assign`ed onto the context for future use in the pipeline.

## Scenario Context Lifecycle

A `LiveLoad.Scenario.Context` is created once per user process at the start of the load test, before
the scenario's `c:LiveLoad.Scenario.run/3` callback is called for the first time. It wraps the
`LiveLoad.Browser.Context` that was created for that user, and is then passed into `c:LiveLoad.Scenario.run/3`
as the first argument on every iteration of the scenario.

The same context is reused across all iterations of a scenario for a given user. This means that
any `assigns` set during one iteration will still be present on the context at the start of the next
iteration. If you want to start each iteration with a clean set of assigns, you can call `reset_assigns/1`
at the start of your `c:LiveLoad.Scenario.run/3` callback.

## Halting and Errors

Halting and errors are mutually exclusive:
- A scenario can be halted by calling the `halt/1` function in order to mark the scenario as halted.
- A scenario will be marked with an error whenever any error, exception, or exit occurs while an operation is running.

The reason for this mutual exclusivity is to preserve the reason for a scenario not completing. Errors are unexpected
occurrences that may happen for any number of reasons and are marked as such to ensure that any consumers of LiveLoad
are able to understand what errors are happening in their application while load testing their sites. However, halting
is manually marked and is a decision by the writer of the scenario. Halting can be used to simply short-circuit a scenario
should the writer decide that it can be short-circuited at any point during the scenario's run.

As such, when calling `halt/1`, a scenario will only be marked as halted if no error has occurred in the pipeline in order
to preserve the reason for the scenario not completing.

Two helper functions, `halted?/1` and `failed?/1` are provided in order to determine if a scenario failed or was halted manually.

A `LiveLoad.Scenario.Context` which is `halted?` or `failed?` will stop the `LiveLoad.Scenario` from iterating any further, and no more
iterations will occur for this user's process.

## Assigns

All functions exposed on the API accept both a raw value and a 1-arity function so that you can get values from the context and its assigns.

For example, if you simple need to click on a button with a specific hard-coded ID, you might write something like this:

```
click(context, "#submit")
```

However, if the button may have a dynamic ID, you can pass in a 1-arity function and build the ID based on the assigns:

```
click(context, &"##{&1.assigns.button_id}")
```

# `assigned_as`

```elixir
@type assigned_as() ::
  atom() | (term() -&gt; atom()) | (term() -&gt; %{required(atom()) =&gt; term()})
```

A key passed in to the `:as` option when using functions that extract values from the context.

Values that are extracted can be specified to be assigned to the context's assigns via the `:as`
option, which can receive an `t:atom/0`, or a 1-arity function which may return either an `t:atom/0`
or a `t:map/0` of `t:atom/0` keys to values which will be merged onto the context's assigns.

# `phoenix_loading_type`

```elixir
@type phoenix_loading_type() ::
  :click | :submit | :change | :focus | :blur | :keydown | :keyup
```

One of the loading class types that Phoenix adds to elements when events are in-flight on the `Phoenix.LiveView.Socket`.

# `resolvable`

```elixir
@type resolvable(value) :: value | (t() -&gt; value)
```

A resolvable variable given to operations on the `LiveLoad.Scenario.Context`.

Can either be a value, or a 1-arity function that receives a `LiveLoad.Scenario.Context`
and returns a value.

# `t`

```elixir
@type t() :: %LiveLoad.Scenario.Context{
  assigns: %{optional(atom()) =&gt; term()},
  browser_context: LiveLoad.Browser.Context.t(),
  error: LiveLoad.Scenario.Error.t() | nil,
  halted?: boolean(),
  step: non_neg_integer(),
  throttle_names: MapSet.t(atom())
}
```

# `assign`

```elixir
@spec assign(context :: t(), key :: atom(), value :: term()) :: t()
```

Assigns a value to a key in the context.

The `assigns` storage is meant to be used to store values in the connection
so that other operations in your scenario's pipeline can access them.
The `assigns` storage is a map.

# `blur`

```elixir
@spec blur(context :: t(), selector :: resolvable(String.t())) :: t()
```

Blurs an element that matches the given selector on the current page.

# `check`

```elixir
@spec check(context :: t(), selector :: resolvable(String.t())) :: t()
```

Checks a checkbox or radio button element that matches the given selector on the current page.

# `checked?`

```elixir
@spec checked?(context :: t(), selector :: String.t(), opts :: [{:as, assigned_as()}]) ::
  t()
```

Extracts whether or not a checkbox or radio button element matching the given selector is checked and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `clear`

```elixir
@spec clear(context :: t(), selector :: resolvable(String.t())) :: t()
```

Clears an input element that matches the given selector on the current page.

# `clear_assign`

```elixir
@spec clear_assign(context :: t(), key :: atom()) :: t()
```

Clears a specific assign on the context.

After clearing this key will no longer be available and assertive access will cause an exception.

See `assign/3` for information about the `assigns` storage.

# `click`

```elixir
@spec click(context :: t(), selector :: resolvable(String.t())) :: t()
```

Clicks an element that matches the given selector on the current page.

# `context_storage_snapshot`

```elixir
@spec context_storage_snapshot(context :: t(), opts :: [{:as, assigned_as()}]) :: t()
```

Gets a snapshot of the current storage state of the browser context.

This is a serializable storage snapshot that contains the current browser context's cookies, local storage, and session storage.

This type is intentionally left as an ambiguous term, as it is meant to be usable as a storage and restoration mechanism,
however it is not guaranteed to be uniform across different browser connections.

## Options

* `:as` - an atom key to place the storage snapshot under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `drag_and_drop`

```elixir
@spec drag_and_drop(
  context :: t(),
  source :: resolvable(String.t()),
  target :: resolvable(String.t())
) ::
  t()
```

Drags from the source element matching the given selector to the target element matching the given selector.

# `ensure_liveview`

```elixir
@spec ensure_liveview(context :: t()) :: t()
```

Detects whether or not the current page is a LiveView.

# `fail`

```elixir
@spec fail(context :: t(), reason :: term()) :: t()
```

Marks a `LiveLoad.Scenario` as failed and prevents the scenario from doing any further operations on the context.

If the scenario has already had an error, this results in a No-Op in order to preserve the error.

# `failed?`
*macro* 

Checks to see if the `LiveLoad.Scenario.Context` failed to complete.

Failure occurs when an error occurs while the `LiveLoad.Scenario` is running.

Allowed in guard tests.

# `fill`

```elixir
@spec fill(
  context :: t(),
  selector :: resolvable(String.t()),
  value :: resolvable(String.t())
) :: t()
```

Fills an input element that matches the given selector on the current page with the given value.

Passing an empty string as the value will clear the input's value.

# `focus`

```elixir
@spec focus(context :: t(), selector :: resolvable(String.t())) :: t()
```

Focuses an element that matches the given selector on the current page.

# `get_attribute`

```elixir
@spec get_attribute(
  context :: t(),
  selector :: String.t(),
  name :: String.t(),
  opts :: [{:as, assigned_as()}]
) :: t()
```

Extracts the element attribute value for an element matching the given selector and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `halt`

```elixir
@spec halt(context :: t()) :: t()
```

Halts a `LiveLoad.Scenario` by preventing any further operations to take place on the context.

If the scenario has already had an error, this results in a No-Op and the context will not be marked
as halted in order to preserve the error.

# `halted?`
*macro* 

Checks to see if the `LiveLoad.Scenario.Context` was halted.

Halting occurs when the `halt/1` function is called on the context directly.

Allowed in guard tests.

# `hover`

```elixir
@spec hover(context :: t(), selector :: resolvable(String.t())) :: t()
```

Hovers over an element that matches the given selector on the current page.

# `inner_html`

```elixir
@spec inner_html(
  context :: t(),
  selector :: String.t(),
  opts :: [{:as, assigned_as()}]
) :: t()
```

Extracts the innerHTML value of an element matching the given selector and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `inner_text`

```elixir
@spec inner_text(
  context :: t(),
  selector :: String.t(),
  opts :: [{:as, assigned_as()}]
) :: t()
```

Extracts the innerText value of an element matching the given selector and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `input_value`

```elixir
@spec input_value(
  context :: t(),
  selector :: String.t(),
  opts :: [{:as, assigned_as()}]
) :: t()
```

Extracts the value of an input, textarea, or select element matching the given selector and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `navigate`

```elixir
@spec navigate(context :: t(), url :: resolvable(String.t())) :: t()
```

Navigates to the given URL.

# `page_content`

```elixir
@spec page_content(context :: t(), opts :: [{:as, assigned_as()}]) :: t()
```

Extracts the current page's content and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `press`

```elixir
@spec press(
  context :: t(),
  selector :: resolvable(String.t()),
  key :: resolvable(String.t())
) :: t()
```

Focuses an element that matches the given selector on the current page and activates the given key.

# `reload`

```elixir
@spec reload(context :: t()) :: t()
```

Reloads the current page.

# `reset_assigns`

```elixir
@spec reset_assigns(context :: t()) :: t()
```

Resets all of the assigns on the context.

During a `LiveLoad.Scenario`, the scenario will loop and run many iterations per user process.
The `LiveLoad.Scenario.Context` is maintained across the loops, so that you can use assigns
from a previous loop in order to take new actions on the previous data.

See `assign/3` for information about the `assigns` storage.

# `reset_context_storage`

```elixir
@spec reset_context_storage(context :: t()) :: t()
```

Resets the storage state of the current browser context to an empty state.

This will clear all cookies, local storage and session storage of the browser context.

# `restore_context_storage`

```elixir
@spec restore_context_storage(context :: t(), snapshot :: resolvable(term())) :: t()
```

Restores the storage state of the browser context from a previously stored snapshot.

# `select_multiple_options`

```elixir
@spec select_multiple_options(
  context :: t(),
  selector :: resolvable(String.t()),
  values :: resolvable([String.t()])
) :: t()
```

Selects multiple options on a select element that matches the given selector on the current page.

# `select_option`

```elixir
@spec select_option(
  context :: t(),
  selector :: resolvable(String.t()),
  value :: resolvable(String.t())
) ::
  t()
```

Selects an option on a select element that matches the given selector on the current page.

# `submit_form`

```elixir
@spec submit_form(context :: t(), form_selector :: resolvable(String.t())) :: t()
```

Submits a LiveView form by clicking its submit button.
Waits for the form to no longer have the `phx-submit-loading` class applied.

# `text_content`

```elixir
@spec text_content(
  context :: t(),
  selector :: String.t(),
  opts :: [{:as, assigned_as()}]
) :: t()
```

Extracts the textContent value of an element matching the given selector and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `throttle`

```elixir
@spec throttle(context :: t(), name :: atom()) :: t()
```

Wait for a named throttle to allow the next step in the pipeline.

# `uncheck`

```elixir
@spec uncheck(context :: t(), selector :: resolvable(String.t())) :: t()
```

Unchecks a checkbox or radio button element that matches the given selector on the current page.

# `update_assign!`

```elixir
@spec update_assign!(context :: t(), key :: atom(), (term() -&gt; term())) :: t()
```

Updates a value under a specified key in the context.

Raises a `KeyError` exception if the specified key does not exist on the assigns.

See `assign/3` for information about the `assigns` storage.

# `visible?`

```elixir
@spec visible?(context :: t(), selector :: String.t(), opts :: [{:as, assigned_as()}]) ::
  t()
```

Extracts whether or not an element matching the given selector is visible and assigns it to the `:as` option on the context's assigns.

## Options

* `:as` - an atom key to place the page content under on the context's assigns.
Alternatively, you can pass a 1-arity function which will be run with the returned value.
The function must return either an atom, which will be used as the key, or a map of new assigns
values that will be merged into the current assigns on the context.

# `wait_for_liveview`

```elixir
@spec wait_for_liveview(context :: t()) :: t()
```

Waits for a LiveView to be fully connected.

# `wait_for_phx_loading_completion`

```elixir
@spec wait_for_phx_loading_completion(
  context :: t(),
  type :: phoenix_loading_type(),
  selector :: resolvable(String.t())
) :: t()
```

Waits for a phx-*-loading attribute to be removed from an element.
Useful for waiting for LiveView event handling to complete.

# `wait_for_selector`

```elixir
@spec wait_for_selector(context :: t(), selector :: resolvable(String.t())) :: t()
```

Waits for an element that matches the given selector to appear on the current page.

