LiveVue provides a robust testing module LiveVue.Test that makes it easy to test Vue components within your Phoenix LiveView tests.
Overview
Testing LiveVue components differs from traditional Phoenix LiveView testing in a key way:
Traditional LiveView testing uses render_component/2 to get final HTML
LiveVue testing provides helpers to inspect the Vue component configuration before client-side rendering
Dependencies
The LiveVue.Test module requires the lazy_html package for parsing HTML. Add it to your test dependencies:
defp deps do
[
# ... other deps
{ :lazy_html , ">= 0.1.0" , only: :test }
]
end
Don’t forget to run:
Testing configuration
For comprehensive testing, you should disable props diffing in your test environment to ensure LiveVue.Test.get_vue/2 always returns complete props data:
config :live_vue ,
enable_props_diff: false
When props diffing is enabled (the default), LiveVue only sends changed properties to optimize performance. However, during testing, you typically want to inspect the complete component state rather than just the incremental changes.
This configuration should be set globally for the test environment rather than per-component, as it affects the behavior of the testing helpers.
Basic component testing
Let’s start with a simple component test:
defmodule MyAppWeb . CounterTest do
use MyAppWeb . ConnCase
alias LiveVue . Test
test "renders counter component with initial props" , %{ conn: conn} do
{ :ok , view, _html } = live (conn, "/counter" )
vue = Test . get_vue (view)
assert vue.component == "Counter"
assert vue.props == %{ "count" => 0 }
end
end
The get_vue/2 function returns a map containing:
:component - Vue component name
:id - Unique component identifier
:props - Decoded props
:handlers - Event handlers and operations
:slots - Slot content
:ssr - SSR status
:class - CSS classes
Testing multiple components
When your view contains multiple Vue components, you can specify which one to test:
# Find by component name
vue = Test . get_vue (view, name: "UserProfile" )
# Find by ID
vue = Test . get_vue (view, id: "profile-1" )
Example with multiple components:
def render (assigns) do
~H"""
< div >
<.vue id="profile-1" name="John" v-component="UserProfile" />
<.vue id="card-1" name="Jane" v-component="UserCard" />
</ div >
"""
end
test "finds specific component" do
html = render_component ( & my_component / 1 )
# Get UserCard component
vue = Test . get_vue (html, name: "UserCard" )
assert vue.props == %{ "name" => "Jane" }
# Get by ID
vue = Test . get_vue (html, id: "profile-1" )
assert vue.component == "UserProfile"
end
Testing event handlers
You can verify event handlers are properly configured:
test "component has correct event handlers" do
vue = Test . get_vue ( render_component ( & my_component / 1 ))
assert vue.handlers == %{
"click" => JS . push ( "click" , value: %{ "abc" => "def" }),
"submit" => JS . push ( "submit" )
}
end
Testing slots
LiveVue provides tools to test both default and named slots:
def component_with_slots (assigns) do
~H"""
<.vue v-component="WithSlots">
Default content
<:header>Header content</:header>
<:footer>Footer content</:footer>
</.vue>
"""
end
test "renders slots correctly" do
vue = Test . get_vue ( render_component ( & component_with_slots / 1 ))
assert vue.slots == %{
"default" => "Default content" ,
"header" => "Header content" ,
"footer" => "Footer content"
}
end
Important notes about slots :
Use <:inner_block> instead of <:default> for default content
Slots are automatically Base64 encoded in the HTML
The test helper decodes them for easier assertions
Testing SSR configuration
Verify server-side rendering settings:
test "respects SSR configuration" do
vue = Test . get_vue ( render_component ( & my_component / 1 ))
assert vue.ssr == true
# Or with SSR disabled
vue = Test . get_vue ( render_component ( & ssr_disabled_component / 1 ))
assert vue.ssr == false
end
Testing CSS classes
Check applied styling:
test "applies correct CSS classes" do
vue = Test . get_vue ( render_component ( & my_component / 1 ))
assert vue.class == "bg-blue-500 rounded"
end
Integration testing with Playwright
For full integration tests with client-side Vue rendering, use a headless browser with Playwright.
Playwright setup
LiveVue’s E2E tests use Playwright. Here’s a typical test structure:
tests/e2e/example.spec.js
import { test , expect } from "@playwright/test"
// Helper to wait for LiveView connection
const syncLV = async page => {
await Promise . all ([
expect ( page . locator ( ".phx-connected" ). first ()). toBeVisible (),
expect ( page . locator ( ".phx-change-loading" )). toHaveCount ( 0 ),
new Promise ( resolve => setTimeout ( resolve , 50 )),
])
}
test ( "Vue component renders and responds to events" , async ({ page }) => {
await page . goto ( "/counter" )
await syncLV ( page )
// Verify Vue component is mounted
await expect ( page . locator ( '[phx-hook="VueHook"]' )). toBeVisible ()
// Check initial state
await expect ( page . locator ( "[data-testid='count']" )). toHaveText ( "0" )
// Trigger event and verify update
await page . click ( "button" )
await syncLV ( page )
await expect ( page . locator ( "[data-testid='count']" )). toHaveText ( "1" )
})
Tips for E2E tests
Wait for LiveView
Always use syncLV() after navigation or events to ensure LiveView has finished processing.
Use data attributes
Add data-testid or data-pw-* attributes for reliable selectors that won’t break with styling changes.
Test Vue + LiveView interaction
Verify props update correctly after server events to ensure the integration is working properly.
Best practices
Component isolation
Test Vue components in isolation when possible
Use render_component/1 for focused tests
Mock external dependencies
Clear assertions
Test one aspect per test
Use descriptive test names
Assert specific properties rather than entire component structure
# Good
test "displays user name" do
vue = Test . get_vue (view)
assert vue.props[ "name" ] == "John"
end
# Avoid
test "component works" do
vue = Test . get_vue (view)
assert vue == %{ .. .} # Too broad
end
Integration testing
Test full component interaction in LiveView context
Verify both server and client-side behavior
Test error cases and edge conditions
Maintainable tests
Use helper functions for common assertions
Keep test setup minimal and clear
Document complex test scenarios
Example: Shared test helpers
defmodule MyAppWeb . VueTestHelpers do
alias LiveVue . Test
def assert_vue_component (view, name, expected_props) do
vue = Test . get_vue (view, name: name)
assert vue.component == name
assert vue.props == expected_props
end
def assert_event_handler (view, name, event_name) do
vue = Test . get_vue (view, name: name)
assert Map . has_key? (vue.handlers, event_name)
end
end
Common testing patterns
Testing dynamic props
test "updates props when state changes" , %{ conn: conn} do
{ :ok , view, _html } = live (conn, "/counter" )
# Initial state
vue = Test . get_vue (view)
assert vue.props[ "count" ] == 0
# Trigger update
view |> element ( "button" , "Increment" ) |> render_click ()
# Verify update
vue = Test . get_vue (view)
assert vue.props[ "count" ] == 1
end
Testing with streams
test "handles stream updates" , %{ conn: conn} do
{ :ok , view, _html } = live (conn, "/items" )
vue = Test . get_vue (view, name: "ItemList" )
assert length (vue.props[ "items" ]) == 0
# Add item via LiveView event
view |> element ( "form" ) |> render_submit (%{ item: %{ name: "New Item" }})
vue = Test . get_vue (view, name: "ItemList" )
assert length (vue.props[ "items" ]) == 1
end
Testing error states
test "displays error message on invalid input" , %{ conn: conn} do
{ :ok , view, _html } = live (conn, "/form" )
# Submit invalid data
view |> element ( "form" ) |> render_submit (%{ email: "invalid" })
vue = Test . get_vue (view, name: "FormComponent" )
assert vue.props[ "error" ] == "Invalid email format"
end
Next steps
Deployment Deploy your LiveVue app to production
Architecture Understand how LiveVue works under the hood
Configuration Configure testing and development settings
API reference Explore the Elixir API documentation