package session import ( "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRenderHTML_BasicSession(t *testing.T) { sess := &Session{ ID: "test-session-abcdef12", Path: "/tmp/test.jsonl", StartTime: baseTime, EndTime: baseTime.Add(5 * time.Minute), Events: []Event{ { Timestamp: baseTime, Type: "user", Input: "Please list files", }, { Timestamp: baseTime.Add(time.Second), Type: "tool_use", Tool: "Bash", ToolID: "tu_1", Input: "ls -la # list files", Output: "total 42\ndrwxr-xr-x 3 user user 4096 Feb 19 .", Duration: 2 * time.Second, Success: true, }, { Timestamp: baseTime.Add(3 * time.Second), Type: "assistant", Input: "The directory contains 42 items.", }, }, } dir := t.TempDir() outputPath := filepath.Join(dir, "out.html") err := RenderHTML(sess, outputPath) require.NoError(t, err) data, err := os.ReadFile(outputPath) require.NoError(t, err) html := string(data) t.Run("contains_doctype", func(t *testing.T) { assert.True(t, strings.HasPrefix(html, "")) }) t.Run("contains_session_id", func(t *testing.T) { assert.Contains(t, html, "test-ses") }) t.Run("contains_timestamp", func(t *testing.T) { assert.Contains(t, html, "2026-02-19 10:00:00") }) t.Run("contains_tool_count", func(t *testing.T) { assert.Contains(t, html, "1 tool calls") }) t.Run("contains_user_event", func(t *testing.T) { assert.Contains(t, html, "User") assert.Contains(t, html, "Please list files") }) t.Run("contains_bash_event", func(t *testing.T) { assert.Contains(t, html, "Bash") assert.Contains(t, html, "ls -la") }) t.Run("contains_assistant_event", func(t *testing.T) { assert.Contains(t, html, "Claude") }) t.Run("contains_js_functions", func(t *testing.T) { assert.Contains(t, html, "function toggle(") assert.Contains(t, html, "function filterEvents(") }) t.Run("contains_success_icon", func(t *testing.T) { assert.Contains(t, html, "✓") // Tick mark }) t.Run("html_ends_properly", func(t *testing.T) { assert.True(t, strings.HasSuffix(strings.TrimSpace(html), "")) }) } func TestRenderHTML_EmptySession(t *testing.T) { sess := &Session{ ID: "empty-session", Path: "/tmp/empty.jsonl", StartTime: baseTime, EndTime: baseTime, Events: nil, } dir := t.TempDir() outputPath := filepath.Join(dir, "empty.html") err := RenderHTML(sess, outputPath) require.NoError(t, err) data, err := os.ReadFile(outputPath) require.NoError(t, err) html := string(data) assert.Contains(t, html, "") assert.Contains(t, html, "0 tool calls") // No error count span should appear in the stats. assert.NotContains(t, html, `class="err">`) } func TestRenderHTML_WithErrors(t *testing.T) { sess := &Session{ ID: "error-session", Path: "/tmp/err.jsonl", StartTime: baseTime, EndTime: baseTime.Add(10 * time.Second), Events: []Event{ { Timestamp: baseTime, Type: "tool_use", Tool: "Bash", ToolID: "tu_ok", Input: "echo ok", Output: "ok", Duration: time.Second, Success: true, }, { Timestamp: baseTime.Add(2 * time.Second), Type: "tool_use", Tool: "Bash", ToolID: "tu_err", Input: "false", Output: "exit 1", Duration: time.Second, Success: false, ErrorMsg: "exit 1", }, }, } dir := t.TempDir() outputPath := filepath.Join(dir, "errors.html") err := RenderHTML(sess, outputPath) require.NoError(t, err) data, err := os.ReadFile(outputPath) require.NoError(t, err) html := string(data) assert.Contains(t, html, "2 tool calls") assert.Contains(t, html, "1 errors") assert.Contains(t, html, `class="err"`) assert.Contains(t, html, "✗") // Cross mark for error } func TestRenderHTML_DurationFormatting(t *testing.T) { sess := &Session{ ID: "dur-test", Path: "/tmp/dur.jsonl", StartTime: baseTime, EndTime: baseTime.Add(2 * time.Hour), Events: []Event{ { Timestamp: baseTime, Type: "tool_use", Tool: "Bash", ToolID: "tu_ms", Input: "fast cmd", Duration: 500 * time.Millisecond, Success: true, }, { Timestamp: baseTime.Add(time.Second), Type: "tool_use", Tool: "Bash", ToolID: "tu_sec", Input: "slow cmd", Duration: 45 * time.Second, Success: true, }, { Timestamp: baseTime.Add(time.Minute), Type: "tool_use", Tool: "Bash", ToolID: "tu_min", Input: "very slow cmd", Duration: 3*time.Minute + 30*time.Second, Success: true, }, }, } dir := t.TempDir() outputPath := filepath.Join(dir, "dur.html") err := RenderHTML(sess, outputPath) require.NoError(t, err) data, err := os.ReadFile(outputPath) require.NoError(t, err) html := string(data) assert.Contains(t, html, "500ms") assert.Contains(t, html, "45.0s") assert.Contains(t, html, "3m30s") assert.Contains(t, html, "2h0m") // Header duration } func TestRenderHTML_HTMLEscaping(t *testing.T) { sess := &Session{ ID: "escape-test", Path: "/tmp/esc.jsonl", StartTime: baseTime, EndTime: baseTime.Add(time.Second), Events: []Event{ { Timestamp: baseTime, Type: "user", Input: ``, }, { Timestamp: baseTime, Type: "tool_use", Tool: "Bash", ToolID: "tu_esc", Input: `echo "bold"`, Output: `bold`, Duration: time.Second, Success: true, }, }, } dir := t.TempDir() outputPath := filepath.Join(dir, "esc.html") err := RenderHTML(sess, outputPath) require.NoError(t, err) data, err := os.ReadFile(outputPath) require.NoError(t, err) html := string(data) // Raw angle brackets must be escaped. assert.NotContains(t, html, `