Powered by AppSignal & Oban Pro

Sensocto Testing & Accessibility Assessment

test-accessibility-assessment.livemd

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, &amp;"- ✅ #{&amp;1}\n") |> Enum.join()}

  #{if data.issues != [] do
    """
    **Issues:**
    #{Enum.map(data.issues, &amp;"- ❌ #{&amp;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, &amp;"- [ ] #{&amp;1}\n") |> Enum.join()}

### Accessibility Tasks (7 hours)
#{Enum.map(sprint_1.accessibility, &amp;"- [ ] #{&amp;1}\n") |> Enum.join()}

### Usability Tasks (8 hours)
#{Enum.map(sprint_1.usability, &amp;"- [ ] #{&amp;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], &amp;[task_id | &amp;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! 🚀
""")