Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol docs components testing.md
Raw

docs/components/testing.md

# Component Testing Guide
Essential testing patterns and best practices for Raxol components.
## Quick Reference
- [Test Structure](#test-structure) - Test organization and setup
- [Lifecycle Testing](#lifecycle-testing) - Testing component lifecycle
- [Event Testing](#event-testing) - Testing event handling
- [State Testing](#state-testing) - Testing state management
- [Integration Testing](#integration-testing) - Testing component interactions
## Test Structure
### Basic Test Module
```elixir
defmodule Raxol.UI.Components.Input.TextInputTest do
use ExUnit.Case, async: true
import Raxol.ComponentTestHelpers
alias Raxol.UI.Components.Input.TextInput
describe "Component Lifecycle" do
test "initializes with props" do
props = %{value: "test", placeholder: "Enter text"}
component = create_component(TextInput, props)
assert component.state.value == "test"
assert component.state.placeholder == "Enter text"
end
test "mounts correctly" do
component = create_component(TextInput, %{})
{mounted, commands} = simulate_mount(component)
assert mounted.state.mounted == true
assert commands == []
end
test "unmounts correctly" do
component = create_component(TextInput, %{})
mounted = simulate_mount(component)
unmounted = simulate_unmount(mounted)
assert unmounted.state.mounted == false
end
end
describe "Event Handling" do
test "handles text change events" do
component = create_component(TextInput, %{})
event = %{type: :change, value: "new value"}
{updated, commands} = simulate_event(component, event)
assert updated.state.value == "new value"
assert commands == [{:command, :value_changed}]
end
test "handles focus events" do
component = create_component(TextInput, %{})
event = %{type: :focus}
{updated, commands} = simulate_event(component, event)
assert updated.state.focused == true
assert commands == [{:command, :focus_gained}]
end
test "handles invalid events gracefully" do
component = create_component(TextInput, %{})
event = %{type: :invalid_event}
{updated, commands} = simulate_event(component, event)
assert updated.state == component.state
assert commands == []
end
end
describe "State Management" do
test "updates state immutably" do
component = create_component(TextInput, %{})
message = {:set_value, "new value"}
{updated, commands} = simulate_update(component, message)
assert updated.state.value == "new value"
assert updated.state != component.state
end
test "validates state changes" do
component = create_component(TextInput, %{})
message = {:set_value, nil}
{updated, commands} = simulate_update(component, message)
assert updated.state.error != nil
assert commands == [{:command, :validation_failed}]
end
end
describe "Rendering" do
test "renders correctly" do
component = create_component(TextInput, %{value: "test"})
element = simulate_render(component)
assert element.type == :text_input
assert element.content == "test"
assert element.attributes.focused == false
end
test "renders with error state" do
component = create_component(TextInput, %{})
component = %{component | state: Map.put(component.state, :error, "Invalid input")}
element = simulate_render(component)
assert element.attributes.error == "Invalid input"
assert element.attributes.color == :red
end
end
end
```
## Lifecycle Testing
### Initialization Testing
```elixir
describe "Initialization" do
test "sets default values" do
component = create_component(TextInput, %{})
assert component.state.value == ""
assert component.state.focused == false
assert component.state.error == nil
end
test "validates required props" do
assert_raise ArgumentError, fn ->
create_component(TextInput, %{})
end
end
test "handles optional props" do
component = create_component(TextInput, %{
value: "test",
placeholder: "Enter text",
disabled: true
})
assert component.state.value == "test"
assert component.state.placeholder == "Enter text"
assert component.state.disabled == true
end
end
```
### Mount/Unmount Testing
```elixir
describe "Mount/Unmount" do
test "sets up resources on mount" do
component = create_component(TextInput, %{})
{mounted, commands} = simulate_mount(component)
assert mounted.state.mounted == true
assert commands == [{:command, :resources_setup}]
end
test "cleans up resources on unmount" do
component = create_component(TextInput, %{})
mounted = simulate_mount(component)
unmounted = simulate_unmount(mounted)
assert unmounted.state.mounted == false
assert unmounted.state.resources_cleaned == true
end
end
```
## Event Testing
### Input Event Testing
```elixir
describe "Input Events" do
test "handles text input changes" do
component = create_component(TextInput, %{})
events = [
%{type: :change, value: "h"},
%{type: :change, value: "he"},
%{type: :change, value: "hel"},
%{type: :change, value: "hell"},
%{type: :change, value: "hello"}
]
final_component = Enum.reduce(events, component, fn event, acc ->
{updated, _} = simulate_event(acc, event)
updated
end)
assert final_component.state.value == "hello"
end
test "handles keyboard events" do
component = create_component(TextInput, %{})
# Test enter key
{updated, commands} = simulate_event(component, %{type: :key_press, key: :enter})
assert commands == [{:command, :submitted}]
# Test escape key
{updated, commands} = simulate_event(updated, %{type: :key_press, key: :escape})
assert commands == [{:command, :cancelled}]
end
test "handles focus events" do
component = create_component(TextInput, %{})
# Focus
{focused, commands} = simulate_event(component, %{type: :focus})
assert focused.state.focused == true
assert commands == [{:command, :focus_gained}]
# Blur
{blurred, commands} = simulate_event(focused, %{type: :blur})
assert blurred.state.focused == false
assert commands == [{:command, :focus_lost}]
end
end
```
### Error Event Testing
```elixir
describe "Error Handling" do
test "handles validation errors" do
component = create_component(TextInput, %{})
event = %{type: :change, value: "invalid_value"}
{updated, commands} = simulate_event(component, event)
assert updated.state.error != nil
assert commands == [{:command, :validation_failed}]
end
test "recovers from errors" do
component = create_component(TextInput, %{})
component = %{component | state: Map.put(component.state, :error, "Invalid")}
event = %{type: :change, value: "valid_value"}
{updated, commands} = simulate_event(component, event)
assert updated.state.error == nil
assert commands == [{:command, :value_changed}]
end
end
```
## State Testing
### State Update Testing
```elixir
describe "State Updates" do
test "updates state immutably" do
component = create_component(TextInput, %{})
original_state = component.state
message = {:set_value, "new value"}
{updated, _} = simulate_update(component, message)
assert updated.state.value == "new value"
assert updated.state != original_state
end
test "batches multiple updates" do
component = create_component(TextInput, %{})
updates = [
{:set_value, "first"},
{:set_focused, true},
{:set_error, "test error"}
]
final_component = Enum.reduce(updates, component, fn update, acc ->
{updated, _} = simulate_update(acc, update)
updated
end)
assert final_component.state.value == "first"
assert final_component.state.focused == true
assert final_component.state.error == "test error"
end
end
```
### State Validation Testing
```elixir
describe "State Validation" do
test "validates state constraints" do
component = create_component(TextInput, %{})
# Test invalid state
invalid_state = Map.put(component.state, :value, nil)
component = %{component | state: invalid_state}
assert_raise ArgumentError, fn ->
simulate_render(component)
end
end
test "maintains state invariants" do
component = create_component(TextInput, %{})
# Test that focused and error can't both be true
component = %{component | state: %{
component.state |
focused: true,
error: "test error"
}}
{updated, _} = simulate_event(component, %{type: :focus})
assert updated.state.focused == true
assert updated.state.error == nil
end
end
```
## Integration Testing
### Component Interaction Testing
```elixir
describe "Component Interactions" do
test "communicates with parent component" do
parent = create_component(ParentComponent, %{})
child = create_component(TextInput, %{})
# Simulate child event
event = %{type: :change, value: "new value"}
{updated_child, commands} = simulate_event(child, event)
# Simulate parent receiving command
parent_event = {:child_command, commands}
{updated_parent, _} = simulate_event(parent, parent_event)
assert updated_parent.state.child_value == "new value"
end
test "handles multiple child components" do
parent = create_component(FormComponent, %{})
# Create multiple text inputs
inputs = [
create_component(TextInput, %{id: "name"}),
create_component(TextInput, %{id: "email"}),
create_component(TextInput, %{id: "phone"})
]
# Simulate events on each input
events = [
%{id: "name", type: :change, value: "John"},
%{id: "email", type: :change, value: "john@example.com"},
%{id: "phone", type: :change, value: "123-456-7890"}
]
final_parent = Enum.reduce(events, parent, fn event, acc ->
{updated, _} = simulate_event(acc, event)
updated
end)
assert final_parent.state.form_data.name == "John"
assert final_parent.state.form_data.email == "john@example.com"
assert final_parent.state.form_data.phone == "123-456-7890"
end
end
```
### Performance Testing
```elixir
describe "Performance" do
test "renders within time limit" do
component = create_component(TextInput, %{})
start_time = System.monotonic_time(:microsecond)
element = simulate_render(component)
end_time = System.monotonic_time(:microsecond)
render_time = end_time - start_time
assert render_time < 2000 # 2ms limit
assert element != nil
end
test "handles rapid events efficiently" do
component = create_component(TextInput, %{})
# Generate 100 rapid events
events = for i <- 1..100 do
%{type: :change, value: "value_#{i}"}
end
start_time = System.monotonic_time(:microsecond)
final_component = Enum.reduce(events, component, fn event, acc ->
{updated, _} = simulate_event(acc, event)
updated
end)
end_time = System.monotonic_time(:microsecond)
total_time = end_time - start_time
assert total_time < 100_000 # 100ms limit
assert final_component.state.value == "value_100"
end
end
```
## Test Helpers
### Common Test Utilities
```elixir
defmodule Raxol.ComponentTestHelpers do
import ExUnit.Assertions
def create_component(module, props) do
state = module.init(props)
%{
module: module,
state: state,
props: props
}
end
def simulate_mount(component) do
{state, commands} = component.module.mount(component.state)
{%{component | state: state}, commands}
end
def simulate_unmount(component) do
state = component.module.unmount(component.state)
%{component | state: state}
end
def simulate_update(component, message) do
{state, commands} = component.module.update(message, component.state)
{%{component | state: state}, commands}
end
def simulate_render(component) do
component.module.render(component.state)
end
def simulate_event(component, event) do
{state, commands} = component.module.handle_event(event, component.state)
{%{component | state: state}, commands}
end
def assert_state_unchanged(component, updated) do
assert updated.state == component.state
end
def assert_commands_contain(commands, expected_command) do
assert Enum.any?(commands, fn cmd -> cmd == expected_command end)
end
def assert_element_has_attribute(element, attribute, value) do
assert get_in(element, [:attributes, attribute]) == value
end
end
```
## Best Practices
### Test Organization
1. **Group Related Tests**: Use `describe` blocks to group related tests
2. **Clear Test Names**: Use descriptive test names that explain the scenario
3. **Arrange-Act-Assert**: Structure tests with clear setup, action, and verification
4. **Test One Thing**: Each test should verify one specific behavior
### Test Data
1. **Use Fixtures**: Create reusable test data
2. **Edge Cases**: Test boundary conditions and error cases
3. **Realistic Data**: Use realistic test data that matches production scenarios
4. **Randomization**: Use randomized data for stress testing
### Test Maintenance
1. **Keep Tests Simple**: Avoid complex test logic
2. **Update Tests**: Keep tests in sync with component changes
3. **Remove Dead Tests**: Delete tests for removed functionality
4. **Document Complex Tests**: Add comments for complex test scenarios
## Additional Resources
- [Component Guide](README.md) - Component development patterns
- [Style Guide](style_guide.md) - Styling and design patterns