test: add previews and unit tests for call feature
Previews (CallScreenPreviews.kt): - PreviewCallingScreen: outgoing call "Calling..." state - PreviewConnectingScreen: WebRTC connecting state - PreviewCallEndedScreen: call ended state - PreviewIncomingCallScreen: incoming call with accept/reject - PreviewConnectedCallScreen: active call with controls - PreviewConnectedCallMuted: muted mic + speaker on state All use ThemeComparisonColumn for dark/light side-by-side Unit tests - quartz (CallTagsTest.kt): - CallIdTag parse/assemble/roundtrip, edge cases (empty, wrong name) - CallTypeTag parse voice/video, unknown types, roundtrip - CallType.fromString validation Unit tests - quartz (CallEventsTest.kt): - All 6 event kinds have correct constants (25050-25055) - CallOfferEvent.build includes call-id, call-type, p, expiration tags - Expiration is 20 seconds - Answer/ICE/Hangup/Reject/Renegotiate template content verification Unit tests - amethyst (IceCandidateSerializationTest.kt): - Parse ICE candidate JSON with all fields - Parse with different sdpMLineIndex - Parse with missing fields (defaults) - Serialize produces valid JSON - Roundtrip serialization preserves all fields https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nipACWebRtcCalls
|
||||
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CallEventsTest {
|
||||
@Test
|
||||
fun callOfferEventHasCorrectKind() {
|
||||
assertEquals(25050, CallOfferEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callAnswerEventHasCorrectKind() {
|
||||
assertEquals(25051, CallAnswerEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callIceCandidateEventHasCorrectKind() {
|
||||
assertEquals(25052, CallIceCandidateEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callHangupEventHasCorrectKind() {
|
||||
assertEquals(25053, CallHangupEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callRejectEventHasCorrectKind() {
|
||||
assertEquals(25054, CallRejectEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callRenegotiateEventHasCorrectKind() {
|
||||
assertEquals(25055, CallRenegotiateEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callOfferBuildIncludesCallIdTag() {
|
||||
val template =
|
||||
CallOfferEvent.build(
|
||||
sdpOffer = "v=0\r\n...",
|
||||
calleePubKey = "abc123",
|
||||
callId = "test-call-id",
|
||||
type = CallType.VOICE,
|
||||
)
|
||||
assertEquals(CallOfferEvent.KIND, template.kind)
|
||||
assertEquals("v=0\r\n...", template.content)
|
||||
|
||||
val callIdTag = template.tags.firstOrNull { it[0] == "call-id" }
|
||||
assertEquals("test-call-id", callIdTag?.get(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callOfferBuildIncludesCallTypeTag() {
|
||||
val template =
|
||||
CallOfferEvent.build(
|
||||
sdpOffer = "sdp",
|
||||
calleePubKey = "abc123",
|
||||
callId = "id",
|
||||
type = CallType.VIDEO,
|
||||
)
|
||||
val callTypeTag = template.tags.firstOrNull { it[0] == "call-type" }
|
||||
assertEquals("video", callTypeTag?.get(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callOfferBuildIncludesPTag() {
|
||||
val template =
|
||||
CallOfferEvent.build(
|
||||
sdpOffer = "sdp",
|
||||
calleePubKey = "recipient-hex-key",
|
||||
callId = "id",
|
||||
type = CallType.VOICE,
|
||||
)
|
||||
val pTag = template.tags.firstOrNull { it[0] == "p" }
|
||||
assertEquals("recipient-hex-key", pTag?.get(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callOfferBuildIncludesExpirationTag() {
|
||||
val template =
|
||||
CallOfferEvent.build(
|
||||
sdpOffer = "sdp",
|
||||
calleePubKey = "abc",
|
||||
callId = "id",
|
||||
type = CallType.VOICE,
|
||||
createdAt = 1000L,
|
||||
)
|
||||
val expirationTag = template.tags.firstOrNull { it[0] == "expiration" }
|
||||
assertEquals((1000L + CallOfferEvent.EXPIRATION_SECONDS).toString(), expirationTag?.get(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callOfferExpirationIs20Seconds() {
|
||||
assertEquals(20L, CallOfferEvent.EXPIRATION_SECONDS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callAnswerBuildContainsSdp() {
|
||||
val template =
|
||||
CallAnswerEvent.build(
|
||||
sdpAnswer = "answer-sdp",
|
||||
callerPubKey = "caller",
|
||||
callId = "call-1",
|
||||
)
|
||||
assertEquals("answer-sdp", template.content)
|
||||
assertEquals(CallAnswerEvent.KIND, template.kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callIceCandidateBuildContainsJson() {
|
||||
val candidateJson = """{"candidate":"candidate:1","sdpMid":"0","sdpMLineIndex":0}"""
|
||||
val template =
|
||||
CallIceCandidateEvent.build(
|
||||
candidateJson = candidateJson,
|
||||
peerPubKey = "peer",
|
||||
callId = "call-1",
|
||||
)
|
||||
assertEquals(candidateJson, template.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callHangupBuildAllowsEmptyReason() {
|
||||
val template =
|
||||
CallHangupEvent.build(
|
||||
peerPubKey = "peer",
|
||||
callId = "call-1",
|
||||
)
|
||||
assertEquals("", template.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callHangupBuildAllowsCustomReason() {
|
||||
val template =
|
||||
CallHangupEvent.build(
|
||||
peerPubKey = "peer",
|
||||
callId = "call-1",
|
||||
reason = "busy",
|
||||
)
|
||||
assertEquals("busy", template.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callRejectBuildHasCorrectKind() {
|
||||
val template =
|
||||
CallRejectEvent.build(
|
||||
callerPubKey = "caller",
|
||||
callId = "call-1",
|
||||
)
|
||||
assertEquals(CallRejectEvent.KIND, template.kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callRenegotiateBuildContainsSdp() {
|
||||
val template =
|
||||
CallRenegotiateEvent.build(
|
||||
sdpOffer = "new-sdp-offer",
|
||||
peerPubKey = "peer",
|
||||
callId = "call-1",
|
||||
)
|
||||
assertEquals("new-sdp-offer", template.content)
|
||||
assertEquals(CallRenegotiateEvent.KIND, template.kind)
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nipACWebRtcCalls
|
||||
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType
|
||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallTypeTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class CallTagsTest {
|
||||
@Test
|
||||
fun parseCallIdTag() {
|
||||
val tag = arrayOf("call-id", "550e8400-e29b-41d4-a716-446655440000")
|
||||
val result = CallIdTag.parse(tag)
|
||||
assertNotNull(result)
|
||||
assertEquals("550e8400-e29b-41d4-a716-446655440000", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallIdTagRejectsEmpty() {
|
||||
val tag = arrayOf("call-id", "")
|
||||
assertNull(CallIdTag.parse(tag))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallIdTagRejectsMissingValue() {
|
||||
val tag = arrayOf("call-id")
|
||||
assertNull(CallIdTag.parse(tag))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallIdTagRejectsWrongName() {
|
||||
val tag = arrayOf("other", "some-id")
|
||||
assertNull(CallIdTag.parse(tag))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assembleCallIdTag() {
|
||||
val tag = CallIdTag.assemble("test-id-123")
|
||||
assertEquals("call-id", tag[0])
|
||||
assertEquals("test-id-123", tag[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roundTripCallIdTag() {
|
||||
val original = "550e8400-e29b-41d4-a716-446655440000"
|
||||
val assembled = CallIdTag.assemble(original)
|
||||
val parsed = CallIdTag.parse(assembled)
|
||||
assertEquals(original, parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallTypeVoice() {
|
||||
val tag = arrayOf("call-type", "voice")
|
||||
val result = CallTypeTag.parse(tag)
|
||||
assertNotNull(result)
|
||||
assertEquals(CallType.VOICE, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallTypeVideo() {
|
||||
val tag = arrayOf("call-type", "video")
|
||||
val result = CallTypeTag.parse(tag)
|
||||
assertNotNull(result)
|
||||
assertEquals(CallType.VIDEO, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallTypeRejectsUnknown() {
|
||||
val tag = arrayOf("call-type", "screenshare")
|
||||
assertNull(CallTypeTag.parse(tag))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCallTypeRejectsMissing() {
|
||||
val tag = arrayOf("call-type")
|
||||
assertNull(CallTypeTag.parse(tag))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assembleCallTypeVoice() {
|
||||
val tag = CallTypeTag.assemble(CallType.VOICE)
|
||||
assertEquals("call-type", tag[0])
|
||||
assertEquals("voice", tag[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assembleCallTypeVideo() {
|
||||
val tag = CallTypeTag.assemble(CallType.VIDEO)
|
||||
assertEquals("call-type", tag[0])
|
||||
assertEquals("video", tag[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roundTripCallType() {
|
||||
for (type in CallType.entries) {
|
||||
val assembled = CallTypeTag.assemble(type)
|
||||
val parsed = CallTypeTag.parse(assembled)
|
||||
assertEquals(type, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callTypeFromString() {
|
||||
assertEquals(CallType.VOICE, CallType.fromString("voice"))
|
||||
assertEquals(CallType.VIDEO, CallType.fromString("video"))
|
||||
assertNull(CallType.fromString("audio"))
|
||||
assertNull(CallType.fromString(""))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user