-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create ViewUtils and let the mock view be passed in. (#42)
* Extract mount to ViewUtils * Fix compilation * Add simple API for creating mock views * Move dialyzer out of Elixir CI * Document LiveIsolatedComponent.View` * Improve docs
- Loading branch information
Showing
7 changed files
with
214 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
name: Dialyzer | ||
|
||
on: | ||
push: | ||
branches: [main, staging] | ||
pull_request: | ||
branches: [main, staging] | ||
env: | ||
MIX_ENV: test | ||
phoenix-version: 1.7.0 | ||
phoenix-live-view-version: 1.0.0 | ||
elixir: 1.18.0 | ||
otp: 27.0 | ||
|
||
jobs: | ||
test: | ||
name: Build and test | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Set up Elixir | ||
uses: ./.github/actions/setup-elixir | ||
with: | ||
elixir-version: ${{ env.elixir }} | ||
otp-version: ${{ env.otp }} | ||
phoenix-live-view-version: ${{ env.phoenix-live-view-version }} | ||
phoenix-version: ${{ env.phoenix-version }} | ||
- name: Retrieve PLT Cache | ||
uses: actions/cache@v3 | ||
id: plt-cache | ||
with: | ||
path: priv/plts | ||
key: plts-v.2-${{ runner.os }}-${{ env.otp }}-${{ env.elixir }}-${{ env.phoenix-version }}-${{ env.phoenix-live-view-version }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} | ||
- name: Create PLTs | ||
if: steps.plt-cache.outputs.cache-hit != 'true' | ||
run: | | ||
mkdir -p priv/plts | ||
mix dialyzer --plt | ||
- run: mix dialyzer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 +1,57 @@ | ||
defmodule LiveIsolatedComponent.View do | ||
@moduledoc false | ||
use Phoenix.LiveView | ||
|
||
alias LiveIsolatedComponent.Hooks | ||
alias LiveIsolatedComponent.StoreAgent | ||
alias LiveIsolatedComponent.Utils | ||
alias Phoenix.LiveView.TagEngine | ||
|
||
def mount(params, session, socket) do | ||
socket = | ||
socket | ||
|> assign(:store_agent, session[LiveIsolatedComponent.MessageNames.store_agent_key()]) | ||
|> run_on_mount(params, session) | ||
|> Utils.update_socket_from_store_agent() | ||
|
||
{:ok, socket} | ||
end | ||
|
||
def render(%{component: component, store_agent: agent, assigns: component_assigns} = _assigns) | ||
when is_function(component) do | ||
TagEngine.component( | ||
component, | ||
Map.merge( | ||
component_assigns, | ||
StoreAgent.get_slots(agent, component_assigns) | ||
), | ||
{__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line} | ||
) | ||
end | ||
|
||
def render(assigns) do | ||
new_inner_assigns = Map.put_new(assigns.assigns, :id, "some-unique-id") | ||
@moduledoc """ | ||
This module serves as a starting point to creating your own | ||
mock views for `LiveIsolatedComponent`. | ||
assigns = Map.put(assigns, :assigns, new_inner_assigns) | ||
You might want to use custom mock views for multiple reasons | ||
(using some custom UI library like `Surface`, having important | ||
logic in hooks...). In any case, think whether or not the test | ||
can work and test effectively the isolated behaviour of your | ||
component. If that is not the case, you are welcomed to use | ||
your own mock view. | ||
~H""" | ||
<.live_component | ||
id={@assigns.id} | ||
module={@component} | ||
{@assigns} | ||
{@slots} | ||
/> | ||
""" | ||
end | ||
|
||
def handle_info(event, socket) do | ||
handle_info = socket |> Utils.store_agent_pid() |> StoreAgent.get_handle_info() | ||
original_assigns = socket.assigns | ||
## Custom `c:Phoenix.LiveView.mount/3` | ||
{:noreply, socket} = handle_info.(event, Utils.normalize_socket(socket, original_assigns)) | ||
Just override the callback and make sure to call `LiveIsolatedComponent.ViewUtils.mount/3` | ||
to properly initialize the socket to work with `LiveIsolatedComponent`. Refer to the | ||
documentation of the util and to the callback for more specific usages. | ||
{:noreply, Utils.denormalize_socket(socket, original_assigns)} | ||
end | ||
## Custom `c:Phoenix.LiveView.render/1` | ||
def handle_event(event, params, socket) do | ||
handle_event = socket |> Utils.store_agent_pid() |> StoreAgent.get_handle_event() | ||
original_assigns = socket.assigns | ||
The given assigns contain the following keys you can use to create your custom render: | ||
result = handle_event.(event, params, Utils.normalize_socket(socket, original_assigns)) | ||
- `@component` contains the passed in component, be it a function or a module. | ||
- `@assigns` contains the list of given assigns for the component. | ||
- `@slots` for the given slots. If you are having problems rendering slots, use `LiveIsolatedComponent.ViewUtils.prerender_slots/1` | ||
with the full assigns to get a pre-rendered list of slots. | ||
Utils.send_to_test( | ||
socket, | ||
original_assigns, | ||
{LiveIsolatedComponent.MessageNames.handle_event_result_message(), self(), | ||
handle_event_result_as_event_param(result)} | ||
) | ||
## Custom `c:Phoenix.LiveView.handle_info/2` and `c:Phoenix.LiveView.handle_event/3` | ||
denormalize_result(result, original_assigns) | ||
end | ||
Either use an `m:Phoenix.LiveView.on_mount/1` hook or one of the options in | ||
`m:LiveIsolatedComponent.live_isolated_component/2`. There is some convoluted | ||
logic in these handles and already some work put on making them extensible with these | ||
mechanisms to make overriding them worthy. | ||
""" | ||
use Phoenix.LiveView | ||
|
||
defp handle_event_result_as_event_param({:noreply, _socket}), do: :noreply | ||
defp handle_event_result_as_event_param({:reply, map, _socket}), do: {:reply, map} | ||
defmacro __using__(_opts) do | ||
quote do | ||
use Phoenix.LiveView | ||
|
||
defp denormalize_result({:noreply, socket}, original_assigns), | ||
do: {:noreply, Utils.denormalize_socket(socket, original_assigns)} | ||
@impl Phoenix.LiveView | ||
def mount(params, session, socket), | ||
do: {:ok, LiveIsolatedComponent.ViewUtils.mount(params, session, socket)} | ||
|
||
defp denormalize_result({:reply, map, socket}, original_assigns), | ||
do: {:reply, map, Utils.denormalize_socket(socket, original_assigns)} | ||
@impl Phoenix.LiveView | ||
defdelegate render(assigns), to: LiveIsolatedComponent.ViewUtils | ||
|
||
defp run_on_mount(socket, params, session), | ||
do: run_on_mount(socket.assigns.store_agent, params, session, socket) | ||
@impl Phoenix.LiveView | ||
defdelegate handle_info(event, socket), to: LiveIsolatedComponent.ViewUtils | ||
|
||
defp run_on_mount(agent, params, session, socket) do | ||
agent | ||
|> StoreAgent.get_on_mount() | ||
|> add_lic_hooks() | ||
|> Enum.reduce(socket, &do_run_on_mount(&1, params, session, &2)) | ||
end | ||
@impl Phoenix.LiveView | ||
defdelegate handle_event(event, params, socket), to: LiveIsolatedComponent.ViewUtils | ||
|
||
defp do_run_on_mount({module, first}, params, session, socket) do | ||
{:cont, socket} = module.on_mount(first, params, session, socket) | ||
socket | ||
defoverridable mount: 3, render: 1 | ||
end | ||
end | ||
|
||
defp do_run_on_mount(module, params, session, socket), | ||
do: do_run_on_mount({module, :default}, params, session, socket) | ||
|
||
defp add_lic_hooks(list), | ||
do: [Hooks.HandleEventSpyHook, Hooks.HandleInfoSpyHook, Hooks.AssignsUpdateSpyHook | list] | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
defmodule LiveIsolatedComponent.View.LiveView do | ||
@moduledoc false | ||
use LiveIsolatedComponent.View | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
defmodule LiveIsolatedComponent.ViewUtils do | ||
@moduledoc """ | ||
Collection of utils for people that want to write their own | ||
mock LiveView to use with `m:LiveIsolatedComponent.live_isolated_component/2`. | ||
""" | ||
|
||
import Phoenix.Component, only: [live_component: 1, sigil_H: 2] | ||
|
||
alias LiveIsolatedComponent.Hooks | ||
alias LiveIsolatedComponent.MessageNames | ||
alias LiveIsolatedComponent.StoreAgent | ||
alias LiveIsolatedComponent.Utils | ||
alias Phoenix.Component | ||
alias Phoenix.LiveView.TagEngine | ||
|
||
@doc """ | ||
Run this in your mock view `c:Phoenix.LiveView.mount/3`. | ||
## Options | ||
- `:on_mount`, _boolean_, defaults to `true`. Can disable adding `on_mount` hooks. | ||
""" | ||
def mount(params, session, socket, opts \\ []) do | ||
socket | ||
|> Component.assign(:store_agent, session[MessageNames.store_agent_key()]) | ||
|> run_on_mount(params, session, opts) | ||
|> Utils.update_socket_from_store_agent() | ||
end | ||
|
||
@doc """ | ||
Use this function to get the slot list if for some reason is not working for you. | ||
""" | ||
def prerender_slots(assigns), do: StoreAgent.get_slots(assigns.store_agent, assigns.assigns) | ||
|
||
@doc """ | ||
This function renders the given component in `component` (be it a function or a module) | ||
with the given assigns and slots. | ||
""" | ||
def render(%{component: component, assigns: component_assigns} = assigns) | ||
when is_function(component) do | ||
TagEngine.component( | ||
component, | ||
Map.merge( | ||
component_assigns, | ||
prerender_slots(assigns) | ||
), | ||
{__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line} | ||
) | ||
end | ||
|
||
def render(assigns) do | ||
new_inner_assigns = Map.put_new(assigns.assigns, :id, "some-unique-id") | ||
|
||
assigns = Map.put(assigns, :assigns, new_inner_assigns) | ||
|
||
~H""" | ||
<.live_component | ||
id={@assigns.id} | ||
module={@component} | ||
{@assigns} | ||
{@slots} | ||
/> | ||
""" | ||
end | ||
|
||
@doc false | ||
def handle_info(event, socket) do | ||
handle_info = socket |> Utils.store_agent_pid() |> StoreAgent.get_handle_info() | ||
original_assigns = socket.assigns | ||
|
||
{:noreply, socket} = handle_info.(event, Utils.normalize_socket(socket, original_assigns)) | ||
|
||
{:noreply, Utils.denormalize_socket(socket, original_assigns)} | ||
end | ||
|
||
@doc false | ||
def handle_event(event, params, socket) do | ||
handle_event = socket |> Utils.store_agent_pid() |> StoreAgent.get_handle_event() | ||
original_assigns = socket.assigns | ||
|
||
result = handle_event.(event, params, Utils.normalize_socket(socket, original_assigns)) | ||
|
||
Utils.send_to_test( | ||
socket, | ||
original_assigns, | ||
{LiveIsolatedComponent.MessageNames.handle_event_result_message(), self(), | ||
handle_event_result_as_event_param(result)} | ||
) | ||
|
||
denormalize_result(result, original_assigns) | ||
end | ||
|
||
defp handle_event_result_as_event_param({:noreply, _socket}), do: :noreply | ||
defp handle_event_result_as_event_param({:reply, map, _socket}), do: {:reply, map} | ||
|
||
defp denormalize_result({:noreply, socket}, original_assigns), | ||
do: {:noreply, Utils.denormalize_socket(socket, original_assigns)} | ||
|
||
defp denormalize_result({:reply, map, socket}, original_assigns), | ||
do: {:reply, map, Utils.denormalize_socket(socket, original_assigns)} | ||
|
||
defp run_on_mount(socket, params, session, opts), | ||
do: run_on_mount(socket.assigns.store_agent, params, session, socket, opts) | ||
|
||
defp run_on_mount(agent, params, session, socket, opts) do | ||
on_mount = if Keyword.get(opts, :on_mount, true), do: StoreAgent.get_on_mount(agent), else: [] | ||
|
||
on_mount | ||
|> add_lic_hooks() | ||
|> Enum.reduce(socket, &do_run_on_mount(&1, params, session, &2)) | ||
end | ||
|
||
defp do_run_on_mount({module, first}, params, session, socket) do | ||
{:cont, socket} = module.on_mount(first, params, session, socket) | ||
socket | ||
end | ||
|
||
defp do_run_on_mount(module, params, session, socket), | ||
do: do_run_on_mount({module, :default}, params, session, socket) | ||
|
||
defp add_lic_hooks(list), | ||
do: [Hooks.HandleEventSpyHook, Hooks.HandleInfoSpyHook, Hooks.AssignsUpdateSpyHook | list] | ||
end |