Implements feature request #3504 - Inline audio playback in WebChat UI Changes: - Add InlineAudioParser to detect MEDIA: prefixed paths pointing to audio files - Add InlineAudioPlayerView SwiftUI component with play/pause controls - Integrate audio player rendering into ChatMarkdownRenderer - Add comprehensive unit tests for audio path parsing Supported audio formats: .mp3, .opus, .m4a, .ogg, .oga, .wav, .aac, .flac The inline audio player displays: - Play/pause button - Audio file name - Progress bar with duration - Graceful error handling for missing files Closes #3504
124 lines
4.6 KiB
Swift
124 lines
4.6 KiB
Swift
import MoltbotChatUI
|
|
import Foundation
|
|
import Testing
|
|
|
|
@Suite struct InlineAudioParserTests {
|
|
@Test func parsesEmptyText() {
|
|
let result = InlineAudioParser.parse("")
|
|
#expect(result.cleaned.isEmpty)
|
|
#expect(result.audioFiles.isEmpty)
|
|
}
|
|
|
|
@Test func parsesTextWithoutMediaPaths() {
|
|
let text = "Hello, this is a regular message without any media."
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.cleaned == text)
|
|
#expect(result.audioFiles.isEmpty)
|
|
}
|
|
|
|
@Test func detectsMP3MediaPath() {
|
|
let text = "Here is your audio: MEDIA:/tmp/voice-12345.mp3"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.cleaned == "Here is your audio:")
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.audioFiles[0].path == "/tmp/voice-12345.mp3")
|
|
#expect(result.audioFiles[0].displayName == "voice-12345.mp3")
|
|
}
|
|
|
|
@Test func detectsOpusMediaPath() {
|
|
let text = "Voice message: MEDIA:/var/data/message.opus"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.cleaned == "Voice message:")
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.audioFiles[0].path == "/var/data/message.opus")
|
|
}
|
|
|
|
@Test func detectsM4AMediaPath() {
|
|
let text = "MEDIA:/path/to/audio.m4a is ready"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.cleaned == "is ready")
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.audioFiles[0].path == "/path/to/audio.m4a")
|
|
}
|
|
|
|
@Test func detectsOGGMediaPath() {
|
|
let text = "Listen: MEDIA:/files/sound.ogg"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.audioFiles[0].path == "/files/sound.ogg")
|
|
}
|
|
|
|
@Test func detectsWAVMediaPath() {
|
|
let text = "Audio clip: MEDIA:/recordings/clip.wav"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.audioFiles[0].path == "/recordings/clip.wav")
|
|
}
|
|
|
|
@Test func detectsMultipleMediaPaths() {
|
|
let text = """
|
|
Here are two audio files:
|
|
First: MEDIA:/tmp/voice1.mp3
|
|
Second: MEDIA:/tmp/voice2.opus
|
|
Enjoy!
|
|
"""
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 2)
|
|
#expect(result.audioFiles[0].path == "/tmp/voice1.mp3")
|
|
#expect(result.audioFiles[1].path == "/tmp/voice2.opus")
|
|
#expect(result.cleaned.contains("Here are two audio files:"))
|
|
#expect(result.cleaned.contains("Enjoy!"))
|
|
#expect(!result.cleaned.contains("MEDIA:"))
|
|
}
|
|
|
|
@Test func handlesMediaPathWithDashesAndUnderscores() {
|
|
let text = "MEDIA:/path/to/voice-message_2024-01-15.mp3"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.audioFiles[0].path == "/path/to/voice-message_2024-01-15.mp3")
|
|
}
|
|
|
|
@Test func isCaseInsensitive() {
|
|
let text1 = "media:/tmp/test.mp3"
|
|
let text2 = "Media:/tmp/test.mp3"
|
|
let text3 = "MEDIA:/tmp/test.mp3"
|
|
|
|
let result1 = InlineAudioParser.parse(text1)
|
|
let result2 = InlineAudioParser.parse(text2)
|
|
let result3 = InlineAudioParser.parse(text3)
|
|
|
|
#expect(result1.audioFiles.count == 1)
|
|
#expect(result2.audioFiles.count == 1)
|
|
#expect(result3.audioFiles.count == 1)
|
|
}
|
|
|
|
@Test func ignoresNonAudioMediaPaths() {
|
|
// The parser should only detect audio extensions
|
|
let text = "Image: MEDIA:/tmp/photo.jpg"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.isEmpty)
|
|
#expect(result.cleaned == text) // Text unchanged for non-audio
|
|
}
|
|
|
|
@Test func preservesTextAroundMediaPath() {
|
|
let text = "Before MEDIA:/tmp/audio.mp3 After"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.cleaned == "Before After" || result.cleaned == "Before After")
|
|
}
|
|
|
|
@Test func handlesMediaPathAtStartOfLine() {
|
|
let text = "MEDIA:/tmp/audio.mp3\nSome text after"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.cleaned.contains("Some text after"))
|
|
}
|
|
|
|
@Test func handlesMediaPathAtEndOfLine() {
|
|
let text = "Check this out: MEDIA:/tmp/audio.mp3"
|
|
let result = InlineAudioParser.parse(text)
|
|
#expect(result.audioFiles.count == 1)
|
|
#expect(result.cleaned == "Check this out:")
|
|
}
|
|
}
|