Skip to content

Commit

Permalink
added tests for slots and get_vue/2 accepts :id option
Browse files Browse the repository at this point in the history
  • Loading branch information
Valian committed Dec 4, 2024
1 parent 7f76a63 commit 995844d
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 36 deletions.
3 changes: 3 additions & 0 deletions lib/live_vue/slots.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ defmodule LiveVue.Slots do
into: %{},
do:
case(key) do
# we raise here because :inner_block is always there and we want to avoid
# it overriding the default slot content
:default -> raise "Instead of using <:default> use <:inner_block> slot"
:inner_block -> {:default, render(%{slot: slot})}
slot_name -> {slot_name, render(%{slot: slot})}
end
Expand Down
94 changes: 60 additions & 34 deletions lib/live_vue/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule LiveVue.Test do
Extracts Vue component information from a LiveView or HTML string.
When multiple Vue components are present, you can specify which one to extract using
the `:name` option.
either the `:name` or `:id` option.
Returns a map containing the component's configuration:
* `:component` - The Vue component name (from `v-component` attribute)
Expand All @@ -55,6 +55,10 @@ defmodule LiveVue.Test do
* `:ssr` - Boolean indicating if server-side rendering was performed
* `:class` - CSS classes applied to the component root element
## Options
* `:name` - Find component by name (from `v-component` attribute)
* `:id` - Find component by ID
## Examples
# From a LiveView, get first Vue component
Expand All @@ -64,14 +68,8 @@ defmodule LiveVue.Test do
# Get specific component by name
vue = LiveVue.Test.get_vue(view, name: "MyComponent")
# From HTML string with specific component
html = \"\"\"
<div>
<div phx-hook='VueHook' data-name='OtherComponent' ...></div>
<div phx-hook='VueHook' data-name='MyComponent' ...></div>
</div>
\"\"\"
vue = LiveVue.Test.get_vue(html, name: "MyComponent")
# Get specific component by ID
vue = LiveVue.Test.get_vue(view, id: "my-component-1")
"""
def get_vue(view, opts \\ [])

Expand All @@ -80,27 +78,21 @@ defmodule LiveVue.Test do
end

def get_vue(html, opts) when is_binary(html) do
doc =
vue =
html
|> Floki.parse_document!()
|> Floki.find("[phx-hook='VueHook']")
|> find_component(opts[:name])

case doc do
nil ->
nil

vue ->
%{
props: Jason.decode!(Floki.attribute(vue, "data-props") |> hd()),
component: Floki.attribute(vue, "data-name") |> hd(),
id: Floki.attribute(vue, "id") |> hd(),
handlers: extract_handlers(Floki.attribute(vue, "data-handlers") |> hd()),
slots: Floki.attribute(vue, "data-slots") |> hd() |> Jason.decode!(),
ssr: Floki.attribute(vue, "data-ssr") |> hd() |> String.to_existing_atom(),
class: Floki.attribute(vue, "class") |> List.first()
}
end
|> find_component!(opts)

%{
props: Jason.decode!(attr(vue, "data-props")),
component: attr(vue, "data-name"),
id: attr(vue, "id"),
handlers: extract_handlers(attr(vue, "data-handlers")),
slots: extract_base64_slots(attr(vue, "data-slots")),
ssr: attr(vue, "data-ssr") |> String.to_existing_atom(),
class: attr(vue, "class")
}
end

defp extract_handlers(handlers) do
Expand All @@ -110,6 +102,13 @@ defmodule LiveVue.Test do
|> Enum.into(%{})
end

defp extract_base64_slots(slots) do
slots
|> Jason.decode!()
|> Enum.map(fn {key, value} -> {key, Base.decode64!(value)} end)
|> Enum.into(%{})
end

defp extract_js_ops(ops) do
ops
|> Jason.decode!()
Expand All @@ -120,14 +119,41 @@ defmodule LiveVue.Test do
|> then(&%Phoenix.LiveView.JS{ops: &1})
end

defp find_component(doc, name) do
case name do
nil ->
doc
defp find_component!(components, opts) do
available =
components
|> Enum.map(&"#{attr(&1, "data-name")}##{attr(&1, "id")}")
|> Enum.join(", ")

components =
Enum.reduce(opts, components, fn
{:id, id}, result ->
with [] <- Enum.filter(result, &(attr(&1, "id") == id)) do
raise "No Vue component found with id=\"#{id}\". Available components: #{available}"
end

{:name, name}, result ->
with [] <- Enum.filter(result, &(attr(&1, "data-name") == name)) do
raise "No Vue component found with name=\"#{name}\". Available components: #{available}"
end

{key, _}, _result ->
raise ArgumentError, "invalid keyword option for get_vue/2: #{key}"
end)

case components do
[vue | _] ->
vue

[] ->
raise "No Vue components found in the rendered HTML"
end
end

name ->
doc
|> Enum.find(fn vue -> Floki.attribute(vue, "data-name") |> hd() == name end)
defp attr(element, name) do
case Floki.attribute(element, name) do
[value] -> value
[] -> nil
end
end
end
123 changes: 121 additions & 2 deletions test/live_vue_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ defmodule LiveVueTest do
def multi_component(assigns) do
~H"""
<div>
<.vue name="John" v-component="UserProfile" />
<.vue name="Jane" v-component="UserCard" />
<.vue id="profile-1" name="John" v-component="UserProfile" />
<.vue id="card-1" name="Jane" v-component="UserCard" />
</div>
"""
end
Expand All @@ -58,6 +58,34 @@ defmodule LiveVueTest do
assert vue.component == "UserCard"
assert vue.props == %{"name" => "Jane"}
end

test "finds specific component by id" do
html = render_component(&multi_component/1)
vue = Test.get_vue(html, id: "card-1")

assert vue.component == "UserCard"
assert vue.id == "card-1"
end

test "raises error when component with name not found" do
html = render_component(&multi_component/1)

assert_raise RuntimeError,
~r/No Vue component found with name="Unknown".*Available components: UserProfile#profile-1, UserCard#card-1/,
fn ->
Test.get_vue(html, name: "Unknown")
end
end

test "raises error when component with id not found" do
html = render_component(&multi_component/1)

assert_raise RuntimeError,
~r/No Vue component found with id="unknown-id".*Available components: UserProfile#profile-1, UserCard#card-1/,
fn ->
Test.get_vue(html, id: "unknown-id")
end
end
end

describe "event handlers" do
Expand Down Expand Up @@ -112,4 +140,95 @@ defmodule LiveVueTest do
assert vue.ssr == false
end
end

describe "slots" do
def component_with_slots(assigns) do
~H"""
<.vue v-component="WithSlots">
Default content
<:header>Header content</:header>
<:footer>
<div>Footer content</div>
<button>Click me</button>
</:footer>
</.vue>
"""
end

def component_with_default_slot(assigns) do
~H"""
<.vue v-component="WithSlots">
<:default>Simple content</:default>
</.vue>
"""
end

def component_with_inner_block(assigns) do
~H"""
<.vue v-component="WithSlots">
Simple content
</.vue>
"""
end

test "warns about usage of <:default> slot" do
assert_raise RuntimeError,
"Instead of using <:default> use <:inner_block> slot",
fn -> render_component(&component_with_default_slot/1) end
end

test "renders multiple slots" do
html = render_component(&component_with_slots/1)
vue = Test.get_vue(html)

assert vue.slots == %{
"default" => "Default content",
"header" => "Header content",
"footer" => "<div>Footer content</div>\n <button>Click me</button>"
}
end

test "renders default slot with inner_block" do
html = render_component(&component_with_inner_block/1)
vue = Test.get_vue(html)

assert vue.slots == %{"default" => "Simple content"}
end

test "encodes slots as base64" do
html = render_component(&component_with_slots/1)

# Get raw data-slots attribute to verify base64 encoding
doc = Floki.parse_fragment!(html)
slots_attr = Floki.attribute(doc, "data-slots") |> hd()

# JSON encoded map
assert slots_attr =~ ~r/^\{.*\}$/

slots =
slots_attr
|> Jason.decode!()
|> Enum.map(fn {key, value} -> {key, Base.decode64!(value)} end)
|> Enum.into(%{})

assert slots == %{
"default" => "Default content",
"header" => "Header content",
"footer" => "<div>Footer content</div>\n <button>Click me</button>"
}
end

test "handles empty slots" do
html =
render_component(fn assigns ->
~H"""
<.vue v-component="WithSlots" />
"""
end)

vue = Test.get_vue(html)

assert vue.slots == %{}
end
end
end

0 comments on commit 995844d

Please sign in to comment.