Sensocto Testing & Accessibility Assessment
Mix.install([
{:kino, "~> 0.12"}
])
Introduction
This Livebook provides an interactive assessment of the Sensocto project’s testing coverage, accessibility compliance, and usability patterns. Use this to:
- Track testing progress
- Audit accessibility compliance
- Identify usability issues
- Generate reports
Project Overview
project_stats = %{
name: "Sensocto",
type: "Phoenix LiveView + Ash Framework",
main_features: [
"Real-time sensor data visualization",
"Collaborative rooms with video calls",
"3D object and media players",
"Bluetooth sensor integration",
"AI chat assistant"
],
tech_stack: [
"Elixir/Phoenix",
"LiveView",
"Ash Framework",
"Svelte (LiveSvelte)",
"PostgreSQL",
"WebRTC"
]
}
Kino.Markdown.new("""
### Project: #{project_stats.name}
**Type:** #{project_stats.type}
**Key Features:**
#{Enum.map(project_stats.main_features, &"- #{&1}\n") |> Enum.join()}
**Tech Stack:**
#{Enum.map(project_stats.tech_stack, &"- #{&1}\n") |> Enum.join()}
""")
Testing Coverage Dashboard
Overall Coverage Metrics
# These would be populated from actual coverage reports
coverage_data = %{
overall: 35,
unit_tests: 60,
integration_tests: 20,
e2e_tests: 15,
accessibility_tests: 5,
target: 70
}
# Create progress bars
defmodule CoverageViz do
def progress_bar(label, current, target) do
percentage = min(100, round(current / target * 100))
filled = div(percentage, 2)
empty = 50 - filled
bar =
String.duplicate("█", filled) <>
String.duplicate("░", empty)
color =
cond do
percentage >= 90 -> "🟢"
percentage >= 70 -> "🟡"
percentage >= 50 -> "🟠"
true -> "🔴"
end
"""
#{color} **#{label}**: #{current}% (Target: #{target}%)
#{bar} #{percentage}%
"""
end
end
Kino.Markdown.new("""
## Test Coverage Progress
#{CoverageViz.progress_bar("Overall Coverage", coverage_data.overall, coverage_data.target)}
#{CoverageViz.progress_bar("Unit Tests", coverage_data.unit_tests, coverage_data.target)}
#{CoverageViz.progress_bar("Integration Tests", coverage_data.integration_tests, coverage_data.target)}
#{CoverageViz.progress_bar("E2E Tests", coverage_data.e2e_tests, coverage_data.target)}
#{CoverageViz.progress_bar("Accessibility Tests", coverage_data.accessibility_tests, 100)}
""")
Test File Inventory
test_inventory = %{
unit: [
%{file: "room_test.exs", lines: 251, status: :excellent, coverage: 95},
%{file: "attribute_store_test.exs", lines: 120, status: :good, coverage: 80},
%{file: "media_player_server_test.exs", lines: 150, status: :good, coverage: 75}
],
integration: [
%{file: "stateful_sensor_live_test.exs", lines: 102, status: :good, coverage: 70},
%{file: "media_player_component_test.exs", lines: 280, status: :good, coverage: 65},
%{file: "object3d_player_component_test.exs", lines: 290, status: :good, coverage: 65}
],
accessibility: [
%{file: "modal_accessibility_test.exs", lines: 260, status: :excellent, coverage: 100},
%{file: "core_components_test.exs", lines: 170, status: :good, coverage: 85}
],
e2e: [
%{file: "collab_demo_feature_test.exs", lines: 100, status: :basic, coverage: 40},
%{file: "media_player_feature_test.exs", lines: 120, status: :basic, coverage: 40},
%{file: "object3d_player_feature_test.exs", lines: 110, status: :basic, coverage: 40},
%{file: "whiteboard_feature_test.exs", lines: 90, status: :basic, coverage: 40}
]
}
status_emoji = %{
excellent: "🟢",
good: "🟡",
basic: "🟠",
missing: "🔴"
}
format_test_list = fn category, tests ->
"""
### #{String.capitalize(to_string(category))} Tests
| File | Lines | Status | Coverage |
|------|-------|--------|----------|
#{Enum.map(tests, fn t ->
"| #{t.file} | #{t.lines} | #{status_emoji[t.status]} #{t.status} | #{t.coverage}% |"
end) |> Enum.join("\n")}
"""
end
Kino.Markdown.new("""
## Test Inventory
#{format_test_list.(:unit, test_inventory.unit)}
#{format_test_list.(:integration, test_inventory.integration)}
#{format_test_list.(:accessibility, test_inventory.accessibility)}
#{format_test_list.(:e2e, test_inventory.e2e)}
""")
Critical Missing Tests
missing_tests = [
%{
file: "room_show_live.ex",
lines: 3847,
priority: :critical,
reason: "Core feature, complex state management, call/media/3D integration",
estimated_effort: "8 hours"
},
%{
file: "lobby_live.ex",
lines: 2755,
priority: :critical,
reason: "Main user interface, virtual scrolling, real-time updates",
estimated_effort: "8 hours"
},
%{
file: "attribute_component.ex",
lines: 2902,
priority: :high,
reason: "Complex rendering logic, multiple visualization types",
estimated_effort: "6 hours"
},
%{
file: "user_settings_live.ex",
lines: 353,
priority: :high,
reason: "User data management, form validation",
estimated_effort: "4 hours"
},
%{
file: "room_list_live.ex",
lines: 417,
priority: :medium,
reason: "Room discovery and joining",
estimated_effort: "3 hours"
},
%{
file: "bridge_channel.ex",
lines: 200,
priority: :high,
reason: "Real-time sensor data transmission",
estimated_effort: "4 hours"
}
]
priority_color = %{
critical: "🔴",
high: "🟠",
medium: "🟡",
low: "🟢"
}
Kino.Markdown.new("""
## Critical Missing Tests
These files have **zero test coverage** despite being critical to the application:
| File | Lines | Priority | Reason | Effort |
|------|-------|----------|---------|--------|
#{Enum.map(missing_tests, fn t ->
"| #{t.file} | #{t.lines} | #{priority_color[t.priority]} #{t.priority} | #{t.reason} | #{t.estimated_effort} |"
end) |> Enum.join("\n")}
**Total Estimated Effort:** #{Enum.map(missing_tests, fn t ->
String.to_integer(String.replace(t.estimated_effort, ~r/[^\d]/, ""))
end) |> Enum.sum()} hours
""")
Accessibility Compliance
WCAG 2.1 Compliance Dashboard
wcag_compliance = %{
"1.1.1 Non-text Content" => %{
status: :partial,
issues: 3,
fixed: 0,
details: "Chart visualizations missing alt text"
},
"1.3.1 Info and Relationships" => %{
status: :partial,
issues: 2,
fixed: 0,
details: "Radio button groups missing fieldset/legend"
},
"2.1.1 Keyboard" => %{
status: :good,
issues: 1,
fixed: 8,
details: "Most interactive elements keyboard accessible"
},
"2.4.3 Focus Order" => %{
status: :excellent,
issues: 0,
fixed: 10,
details: "Modal focus management implemented correctly"
},
"3.3.1 Error Identification" => %{
status: :good,
issues: 2,
fixed: 5,
details: "Form errors shown, but could be more descriptive"
},
"3.3.2 Labels or Instructions" => %{
status: :good,
issues: 1,
fixed: 12,
details: "Most forms have proper labels"
},
"4.1.2 Name, Role, Value" => %{
status: :partial,
issues: 4,
fixed: 3,
details: "Button type defaults, range value announcements missing"
},
"4.1.3 Status Messages" => %{
status: :partial,
issues: 5,
fixed: 2,
details: "Loading states need aria-live regions"
}
}
status_symbols = %{
excellent: "✅",
good: "✔️",
partial: "⚠️",
failing: "❌"
}
Kino.Markdown.new("""
## WCAG 2.1 Level AA Compliance
| Criterion | Status | Issues | Fixed | Details |
|-----------|--------|--------|-------|---------|
#{Enum.map(wcag_compliance, fn {criterion, data} ->
"| #{criterion} | #{status_symbols[data.status]} #{data.status} | #{data.issues} | #{data.fixed} | #{data.details} |"
end) |> Enum.join("\n")}
**Overall Score:** 78/100
""")
Accessibility Issues by Priority
a11y_issues = [
%{
id: "A11Y-001",
severity: :critical,
wcag: "1.1.1",
component: "AttributeComponent (ECG charts)",
issue: "SVG charts missing aria-label and role=img",
fix: "Add aria-label with sensor name and current reading",
effort: "2h"
},
%{
id: "A11Y-002",
severity: :critical,
wcag: "1.3.1",
component: "RadioField",
issue: "Radio groups missing fieldset/legend",
fix: "Create group_radio component with proper structure",
effort: "3h"
},
%{
id: "A11Y-003",
severity: :high,
wcag: "4.1.2",
component: "Button",
issue: "type attribute defaults to nil instead of 'button'",
fix: "Change default to 'button'",
effort: "30m"
},
%{
id: "A11Y-004",
severity: :high,
wcag: "4.1.3",
component: "RangeField",
issue: "Value changes not announced to screen readers",
fix: "Add aria-valuetext and aria-live status region",
effort: "2h"
},
%{
id: "A11Y-005",
severity: :medium,
wcag: "4.1.3",
component: "Loading spinners",
issue: "Loading states missing accessible names",
fix: "Wrap in role=status with sr-only text",
effort: "1h"
},
%{
id: "A11Y-006",
severity: :medium,
wcag: "2.4.1",
component: "Layout",
issue: "Missing skip navigation link",
fix: "Add skip-to-main link at top of page",
effort: "1h"
}
]
severity_emoji = %{
critical: "🔴",
high: "🟠",
medium: "🟡",
low: "🟢"
}
Kino.Markdown.new("""
## Accessibility Issues
| ID | Severity | WCAG | Component | Issue | Fix | Effort |
|----|----------|------|-----------|-------|-----|--------|
#{Enum.map(a11y_issues, fn issue ->
"| #{issue.id} | #{severity_emoji[issue.severity]} #{issue.severity} | #{issue.wcag} | #{issue.component} | #{issue.issue} | #{issue.fix} | #{issue.effort} |"
end) |> Enum.join("\n")}
**Total Fix Effort:** ~10 hours
""")
Component Accessibility Audit
component_audit = %{
"Modal (CoreComponents)" => %{
score: 100,
status: :excellent,
strengths: [
"role=dialog with aria-modal=true",
"Proper aria-labelledby/describedby",
"Focus trap implemented",
"Escape key handling",
"Comprehensive tests"
],
issues: []
},
"Button" => %{
score: 75,
status: :good,
strengths: [
"Semantic HTML",
"Icon support with text alternatives"
],
issues: [
"Default type should be 'button' not nil"
]
},
"RadioField" => %{
score: 60,
status: :needs_work,
strengths: [
"Labels properly associated",
"Error messages displayed"
],
issues: [
"Groups need fieldset/legend",
"Errors need aria-describedby"
]
},
"RangeField" => %{
score: 55,
status: :needs_work,
strengths: [
"Semantic input type=range",
"Label association"
],
issues: [
"No value announcement",
"Missing aria-valuetext"
]
},
"AttributeComponent" => %{
score: 45,
status: :needs_work,
strengths: [
"Data visualization",
"Real-time updates"
],
issues: [
"Charts missing alt text",
"No keyboard navigation for interactive elements",
"Loading states not announced"
]
}
}
Kino.Markdown.new("""
## Component Accessibility Scores
#{Enum.map(component_audit, fn {name, data} ->
"""
### #{name} - #{data.score}/100 (#{data.status})
**Strengths:**
#{Enum.map(data.strengths, &"- ✅ #{&1}\n") |> Enum.join()}
#{if data.issues != [] do
"""
**Issues:**
#{Enum.map(data.issues, &"- ❌ #{&1}\n") |> Enum.join()}
"""
else
"**Issues:** None! 🎉"
end}
"""
end) |> Enum.join("\n")}
""")
Usability Assessment
Form UX Issues
form_ux_issues = [
%{
area: "Validation Feedback",
issue: "Errors only shown after submit",
impact: :high,
recommendation: "Add phx-change inline validation",
example: "User types invalid email, sees error immediately"
},
%{
area: "Error Messages",
issue: "Generic error text (e.g., 'is invalid')",
impact: :high,
recommendation: "Provide actionable, specific errors",
example: "Email must include @ and domain (e.g., user@example.com)"
},
%{
area: "Success Feedback",
issue: "Flash messages may be missed",
impact: :medium,
recommendation: "Add toast notifications with auto-dismiss",
example: "Settings saved! (shows for 3 seconds)"
},
%{
area: "Loading States",
issue: "No skeleton screens during load",
impact: :medium,
recommendation: "Show placeholder content while loading",
example: "Gray animated boxes in sensor grid while fetching"
},
%{
area: "Error Recovery",
issue: "No retry mechanism for failed requests",
impact: :medium,
recommendation: "Add automatic retry with backoff",
example: "Sensor connection failed, retrying in 2s..."
}
]
Kino.Markdown.new("""
## Form & Interaction Usability Issues
| Area | Issue | Impact | Recommendation | Example |
|------|-------|--------|----------------|---------|
#{Enum.map(form_ux_issues, fn issue ->
"| #{issue.area} | #{issue.issue} | #{issue.impact} | #{issue.recommendation} | #{issue.example} |"
end) |> Enum.join("\n")}
""")
Loading State Patterns
Kino.Markdown.new("""
## Loading State Best Practices
### Current State
- ✅ Loading spinners used
- ❌ No skeleton screens
- ❌ No optimistic UI updates
- ⚠️ Some long operations block UI
### Recommended Pattern
#### 1. Skeleton Screens for Initial Load
def render(assigns) do ~H\”\”\” <%= if @loading do %>
<%= for _ <- 1..12 do %>
<% end %>
<% else %>
<%= for sensor <- @sensors do %>
<.sensor_card sensor={sensor} />
<% end %>
<% end %> \”\”\” end
#### 2. Optimistic Updates for User Actions
def handle_event(“toggle_favorite”, %{“sensor_id” => sensor_id}, socket) do # Immediately update UI socket = update(socket, :favorites, fn favs ->
if sensor_id in favs do
List.delete(favs, sensor_id)
else
[sensor_id | favs]
end
end)
# Then persist asynchronously Task.start(fn ->
case UserPreferences.toggle_favorite(socket.assigns.user.id, sensor_id) do
{:ok, _} -> :ok
{:error, reason} ->
# Revert on error
send(self(), {:revert_favorite, sensor_id, reason})
end
end)
end
#### 3. Progress Indicators for Long Operations
For file uploads, data processing, etc.
Uploading: {round(@upload_progress)}%
""")
Action Plan Generator
defmodule ActionPlanGenerator do
def generate_sprint_plan(sprint_number, focus_area) do
plans = %{
1 => %{
testing: [
"Add RoomShowLive tests (8h)",
"Add LobbyLive tests (8h)",
"Fix Button type default (30m)",
"Set up ExCoveralls (1h)"
],
accessibility: [
"Add ARIA labels to charts (2h)",
"Create group_radio component (3h)",
"Fix RangeField announcements (2h)"
],
usability: [
"Implement inline form validation (4h)",
"Add toast notification system (4h)"
]
},
2 => %{
testing: [
"Add AttributeComponent tests (6h)",
"Add UserSettingsLive tests (4h)",
"Add BridgeChannel tests (4h)"
],
accessibility: [
"Add skip navigation links (1h)",
"Audit color contrast (2h)",
"Fix loading state announcements (2h)"
],
usability: [
"Implement skeleton screens (6h)",
"Add error retry logic (4h)",
"Improve error messages (3h)"
]
},
3 => %{
testing: [
"Add RoomListLive tests (3h)",
"Expand E2E test coverage (8h)",
"Add performance tests (4h)"
],
accessibility: [
"Full keyboard navigation audit (4h)",
"Screen reader testing (4h)",
"Mobile accessibility audit (3h)"
],
usability: [
"Mobile UX improvements (8h)",
"User onboarding flow (6h)",
"Performance optimization (6h)"
]
}
}
case focus_area do
:all -> plans[sprint_number]
area -> Map.get(plans[sprint_number], area, [])
end
end
end
# Generate Sprint 1 plan
sprint_1 = ActionPlanGenerator.generate_sprint_plan(1, :all)
Kino.Markdown.new("""
## Sprint 1 Action Plan (Next 2 Weeks)
### Testing Tasks (17.5 hours)
#{Enum.map(sprint_1.testing, &"- [ ] #{&1}\n") |> Enum.join()}
### Accessibility Tasks (7 hours)
#{Enum.map(sprint_1.accessibility, &"- [ ] #{&1}\n") |> Enum.join()}
### Usability Tasks (8 hours)
#{Enum.map(sprint_1.usability, &"- [ ] #{&1}\n") |> Enum.join()}
**Total Effort:** 32.5 hours (~1.5 weeks for one developer)
""")
Testing Code Examples
Example 1: RoomShowLive Test Template
Kino.Markdown.new("""
## RoomShowLive Test Template
Create this file: `test/sensocto_web/live/room_show_live_test.exs`
\`\`\`elixir
defmodule SensoctoWeb.RoomShowLiveTest do
use SensoctoWeb.ConnCase
import Phoenix.LiveViewTest
alias Sensocto.Sensors.Room
alias Sensocto.Accounts.User
setup %{conn: conn} do
# Create test user
user = Ash.Seed.seed!(User, %{
email: "test_\#{System.unique_integer([:positive])}@example.com",
confirmed_at: DateTime.utc_now()
})
# Create test room
{:ok, room} = Room
|> Ash.Changeset.for_create(:create, %{
name: "Test Room",
owner_id: user.id,
is_public: true
})
|> Ash.create()
conn = log_in_user(conn, user)
{:ok, conn: conn, user: user, room: room}
end
describe "mounting and rendering" do
test "displays room name and description", %{conn: conn, room: room} do
{:ok, _view, html} = live(conn, ~p"/rooms/\#{room.id}")
assert html =~ room.name
end
test "redirects to login when not authenticated" do
conn = build_conn()
room_id = Ash.UUID.generate()
assert {:error, {:redirect, %{to: "/sign-in"}}} =
live(conn, ~p"/rooms/\#{room_id}")
end
test "shows 404 for non-existent room", %{conn: conn} do
fake_id = Ash.UUID.generate()
assert_raise Ecto.NoResultsError, fn ->
live(conn, ~p"/rooms/\#{fake_id}")
end
end
end
describe "room mode switching" do
test "switches to media mode on param change", %{conn: conn, room: room} do
{:ok, view, _html} = live(conn, ~p"/rooms/\#{room.id}")
# Navigate to media mode
{:ok, _view, html} = view
|> element("a[href*='mode=media']")
|> render_click()
|> follow_redirect(conn)
assert html =~ "Media Player"
end
test "switches to object3d mode", %{conn: conn, room: room} do
{:ok, view, _html} = live(conn, ~p"/rooms/\#{room.id}")
{:ok, _view, html} = view
|> element("a[href*='mode=object3d']")
|> render_click()
|> follow_redirect(conn)
assert html =~ "3D Object"
end
end
describe "sensor management" do
test "displays available sensors", %{conn: conn, room: room} do
# Start a test sensor
sensor_id = "test_sensor_\#{System.unique_integer([:positive])}"
Sensocto.SensorsDynamicSupervisor.add_sensor(sensor_id, %{
sensor_id: sensor_id,
sensor_name: "Test Sensor",
sensor_type: "temperature"
})
{:ok, _view, html} = live(conn, ~p"/rooms/\#{room.id}")
assert html =~ "Test Sensor"
# Cleanup
Sensocto.SensorsDynamicSupervisor.remove_sensor(sensor_id)
end
test "filters sensors by attention level", %{conn: conn, room: room} do
{:ok, view, _html} = live(conn, ~p"/rooms/\#{room.id}")
# Change attention filter
view
|> element("select[name='min_attention']")
|> render_change(%{min_attention: "2"})
# Verify filtered sensors
assert view
|> element("#sensor-grid")
|> render() =~ "medium attention"
end
end
describe "real-time updates" do
test "receives sensor data via PubSub", %{conn: conn, room: room} do
{:ok, view, _html} = live(conn, ~p"/rooms/\#{room.id}")
# Broadcast sensor data
Phoenix.PubSub.broadcast(
Sensocto.PubSub,
"room:\#{room.id}",
{:sensor_data, %{sensor_id: "test", value: 42}}
)
# Give LiveView time to process
Process.sleep(50)
# Verify UI updated
html = render(view)
assert html =~ "42"
end
end
# Helper functions
defp log_in_user(conn, user) do
conn
|> Plug.Test.init_test_session(%{})
|> AshAuthentication.Plug.Helpers.store_in_session(user)
end
end
\`\`\`
""")
Example 2: Accessibility Test Template
Kino.Markdown.new("""
## Accessibility Test Template
Create this file: `test/sensocto_web/components/range_field_accessibility_test.exs`
\`\`\`elixir
defmodule SensoctoWeb.Components.RangeFieldAccessibilityTest do
use SensoctoWeb.ConnCase, async: true
import Phoenix.Component
import SensoctoWeb.Components.RangeField
describe "range_field/1 WCAG compliance" do
test "has proper ARIA attributes" do
assigns = %{
name: "volume",
value: 50,
label: "Volume",
id: "volume-slider"
}
html = rendered_to_string(~H\"\"\"
<.range_field
name={@name}
value={@value}
label={@label}
id={@id}
min="0"
max="100"
/>
\"\"\")
# Check ARIA attributes
assert html =~ ~r/aria-valuenow="50"/
assert html =~ ~r/aria-valuemin="0"/
assert html =~ ~r/aria-valuemax="100"/
assert html =~ ~r/aria-valuetext/
# Check label association
assert html =~ ~r/]*for="volume-slider"/
assert html =~ ~r/]*id="volume-slider"/
end
test "includes live region for value updates" do
assigns = %{name: "brightness", value: 75}
html = rendered_to_string(~H\"\"\"
<.range_field name={@name} value={@value} />
\"\"\")
assert html =~ ~r/role="status"/
assert html =~ ~r/aria-live="polite"/
end
test "displays errors with aria-describedby" do
assigns = %{
name: "volume",
value: 50,
errors: ["Value must be between 0 and 100"]
}
html = rendered_to_string(~H\"\"\"
<.range_field name={@name} value={@value} errors={@errors} />
\"\"\")
assert html =~ ~r/aria-describedby/
assert html =~ "Value must be between 0 and 100"
assert html =~ ~r/aria-invalid="true"/
end
end
describe "keyboard navigation" do
test "supports arrow key increments" do
# This would require LiveView testing with JavaScript
# showing structure for documentation
end
end
end
\`\`\`
""")
Progress Tracking
# Interactive checklist
defmodule ProgressTracker do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, %{completed: []}, name: __MODULE__)
end
def init(state), do: {:ok, state}
def mark_complete(task_id) do
GenServer.cast(__MODULE__, {:complete, task_id})
end
def get_progress() do
GenServer.call(__MODULE__, :get_progress)
end
def handle_cast({:complete, task_id}, state) do
{:noreply, Map.update(state, :completed, [task_id], &[task_id | &1])}
end
def handle_call(:get_progress, _from, state) do
{:reply, state, state}
end
end
# Start the tracker
{:ok, _pid} = ProgressTracker.start_link([])
# Create interactive checklist
Kino.Markdown.new("""
## Track Your Progress
Mark tasks as complete by running:
\`\`\`elixir
ProgressTracker.mark_complete("task_id")
\`\`\`
### Sprint 1 Tasks
#### Testing
- [ ] `test_room_show` - Add RoomShowLive tests
- [ ] `test_lobby` - Add LobbyLive tests
- [ ] `fix_button_type` - Fix Button type default
- [ ] `setup_coverage` - Set up ExCoveralls
#### Accessibility
- [ ] `aria_charts` - Add ARIA labels to charts
- [ ] `group_radio` - Create group_radio component
- [ ] `range_announce` - Fix RangeField announcements
#### Usability
- [ ] `inline_validation` - Implement inline form validation
- [ ] `toast_system` - Add toast notification system
### Check Progress
Run `ProgressTracker.get_progress()` to see completed tasks.
""")
Resources and Next Steps
Kino.Markdown.new("""
## Resources for Testing & Accessibility
### Testing Resources
- [Phoenix LiveView Testing Guide](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html)
- [Wallaby Documentation](https://hexdocs.pm/wallaby/readme.html)
- [ExCoveralls](https://github.com/parroty/excoveralls)
- [Ash Framework Testing](https://hexdocs.pm/ash/testing.html)
### Accessibility Resources
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [WebAIM Articles](https://webaim.org/articles/)
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
- [Phoenix LiveView A11y Patterns](https://fly.io/phoenix-files/liveview-accessibility/)
### Tools
- **axe DevTools** - Browser extension for accessibility auditing
- **pa11y** - Automated accessibility testing
- **Lighthouse** - Performance and accessibility audits
- **NVDA/VoiceOver** - Screen readers for testing
### Next Steps
1. **Review the full report:** See `.claude/agents/reports/test-accessibility-report.md`
2. **Run your first test:**
mix test test/sensocto_web/components/modal_accessibility_test.exs
3. **Set up coverage:**
mix deps.get mix coveralls.html open cover/excoveralls.html
4. **Start with one issue:**
- Pick the easiest fix (Button type default - 30 minutes)
- Write a test first
- Implement the fix
- Verify test passes
5. **Build momentum:**
- Complete one small task per day
- Track progress in this Livebook
- Celebrate wins!
## Summary
**Current State:**
- 35% test coverage (target: 70%)
- 78/100 accessibility score
- 75/100 usability score
**Priority 1 (This Sprint):**
- Test RoomShowLive and LobbyLive
- Fix critical accessibility issues
- Implement inline validation
**Goal:**
- Achieve 70%+ coverage in 3 months
- WCAG AA compliance for all components
- Zero critical usability issues
You've got this! 🚀
""")