From 68dd326d6c8100882d39ed95c2db96c8b35dcf66 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Wed, 1 May 2024 12:44:10 +0800 Subject: [PATCH] add tests for POST with multipart bodies --- .../hellohttp/ux/DropDownView.kt | 10 +- .../multiplatform/hellohttp/ux/FileDialog.kt | 18 ++ .../hellohttp/ux/KeyValueEditorView.kt | 3 +- .../hellohttp/ux/RequestEditorView.kt | 4 +- .../hellohttp/test/RequestResponseTest.kt | 177 +++++++++++++++++- ...1\344\270\255\346\226\207\345\255\227.txt" | 2 + .../src/test/resources/testFile2.txt | 4 + 7 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 "ux-and-transport-test/src/test/resources/testFile1\344\270\255\346\226\207\345\255\227.txt" create mode 100644 ux-and-transport-test/src/test/resources/testFile2.txt diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/DropDownView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/DropDownView.kt index 331bc0c3..d6028118 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/DropDownView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/DropDownView.kt @@ -53,7 +53,7 @@ fun DropDownView( ) }, arrowPadding: PaddingValues = PaddingValues(0.dp), - testTagPart: TestTagPart? = null, + testTagParts: Array? = null, selectedItem: T? = null, onClickItem: (T) -> Boolean, ) { @@ -78,8 +78,8 @@ fun DropDownView( .padding(horizontal = 8.dp, vertical = 4.dp) .fillMaxWidth() .run { - if (testTagPart != null) { - testTag(buildTestTag(testTagPart, TestTagPart.DropdownItem, item.displayText)!!) + if (testTagParts != null) { + testTag(buildTestTag(*testTagParts, TestTagPart.DropdownItem, item.displayText)!!) } else { this } @@ -103,8 +103,8 @@ fun DropDownView( this } }.run { - if (testTagPart != null) { - testTag(buildTestTag(testTagPart, TestTagPart.DropdownButton)!!) + if (testTagParts != null) { + testTag(buildTestTag(*testTagParts, TestTagPart.DropdownButton)!!) } else { this } diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/FileDialog.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/FileDialog.kt index 68f593e5..97b26187 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/FileDialog.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/FileDialog.kt @@ -1,16 +1,24 @@ package com.sunnychung.application.multiplatform.hellohttp.ux import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.window.AwtWindow import com.sunnychung.application.multiplatform.hellohttp.util.log import com.sunnychung.application.multiplatform.hellohttp.ux.viewmodel.FileDialogState import com.sunnychung.lib.multiplatform.kdatetime.KDuration import com.sunnychung.lib.multiplatform.kdatetime.KFixedTimeUnit import com.sunnychung.lib.multiplatform.kdatetime.KInstant +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.awt.FileDialog import java.awt.Frame import java.io.File +/** + * For UX test only + */ +var testChooseFile: File? = null + /** * Due to the bug stated in {@link FileDialogState}, result of onCloseRequest has 3 cases: * 1. non-empty list -> user selected a file @@ -27,6 +35,16 @@ fun FileDialog( onCloseRequest: (result: List?) -> Unit ) { log.d { "FileDialog 1" } + + testChooseFile?.let { + rememberCoroutineScope().launch { + delay(50L) + testChooseFile = null + onCloseRequest(listOf(it)) + } + return + } + val lastCloseTime = state.lastCloseTime.value if (lastCloseTime != null && KInstant.now() - lastCloseTime < KDuration.Companion.of(1, KFixedTimeUnit.Second)) { onCloseRequest(null) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/KeyValueEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/KeyValueEditorView.kt index ce564fbf..84c0080a 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/KeyValueEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/KeyValueEditorView.kt @@ -197,7 +197,7 @@ fun KeyValueEditorView( } else null, modifier = Modifier.weight(0.6f).border(width = 1.dp, color = colors.placeholder) .run { - buildTestTag(testTagPart1, testTagPart2, TestTagPart.FileButton, index)?.let { + buildTestTag(testTagPart1, testTagPart2, index, TestTagPart.FileButton)?.let { testTag(it) } ?: this }, @@ -216,6 +216,7 @@ fun KeyValueEditorView( onItemChange(index, it.copy(valueType = valueType)) true }, + testTagParts = arrayOf(testTagPart1, testTagPart2, index, TestTagPart.ValueTypeDropdown), modifier = Modifier.padding(horizontal = 4.dp) ) } diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/RequestEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/RequestEditorView.kt index c230c2c9..d0fab714 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/RequestEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/RequestEditorView.kt @@ -273,7 +273,7 @@ fun RequestEditorView( onRequestModified(request.copyForApplication(application = it.key.application, method = it.key.method)) true }, - testTagPart = TestTagPart.RequestMethodDropdown, + testTagParts = arrayOf(TestTagPart.RequestMethodDropdown), modifier = Modifier.fillMaxHeight() ) @@ -972,7 +972,7 @@ private fun RequestBodyEditor( ) true }, - testTagPart = TestTagPart.RequestBodyTypeDropdown, + testTagParts = arrayOf(TestTagPart.RequestBodyTypeDropdown), ) } else { AppText(selectedContentType.displayText) diff --git a/ux-and-transport-test/src/test/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/RequestResponseTest.kt b/ux-and-transport-test/src/test/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/RequestResponseTest.kt index 184a666f..30bab7b7 100644 --- a/ux-and-transport-test/src/test/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/RequestResponseTest.kt +++ b/ux-and-transport-test/src/test/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/RequestResponseTest.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.window.rememberWindowState import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.sunnychung.application.multiplatform.hellohttp.AppContext import com.sunnychung.application.multiplatform.hellohttp.model.ContentType +import com.sunnychung.application.multiplatform.hellohttp.model.FieldValueType import com.sunnychung.application.multiplatform.hellohttp.model.FileBody import com.sunnychung.application.multiplatform.hellohttp.model.FormUrlEncodedBody import com.sunnychung.application.multiplatform.hellohttp.model.GraphqlBody @@ -44,6 +45,7 @@ import com.sunnychung.application.multiplatform.hellohttp.ux.AppView import com.sunnychung.application.multiplatform.hellohttp.ux.TestTag import com.sunnychung.application.multiplatform.hellohttp.ux.TestTagPart import com.sunnychung.application.multiplatform.hellohttp.ux.buildTestTag +import com.sunnychung.application.multiplatform.hellohttp.ux.testChooseFile import com.sunnychung.lib.multiplatform.kdatetime.KDuration import com.sunnychung.lib.multiplatform.kdatetime.extension.seconds import kotlinx.coroutines.delay @@ -351,6 +353,112 @@ class RequestResponseTest { ) ) } + + @Test + fun echoPostWithMultipartStrings() = runTest { + createAndSendRestEchoRequestAndAssertResponse( + UserRequestTemplate( + id = uuidString(), + method = "POST", + url = echoUrl, + examples = listOf( + UserRequestExample( + id = uuidString(), + name = "Base", + contentType = ContentType.Multipart, + body = MultipartBody(listOf( + UserKeyValuePair("abcc", "中文字123"), + UserKeyValuePair("MyFormParam", "abcc def_gh+i=?j/k"), + UserKeyValuePair("emoj", "a\uD83D\uDE0EBC"), + )), + ) + ) + ) + ) + } + + @Test + fun echoPostWithMultipartFiles() = runTest { + createAndSendRestEchoRequestAndAssertResponse( + UserRequestTemplate( + id = uuidString(), + method = "POST", + url = echoUrl, + examples = listOf( + UserRequestExample( + id = uuidString(), + name = "Base", + contentType = ContentType.Multipart, + body = MultipartBody(listOf( + UserKeyValuePair("abcc", "中文字123"), + UserKeyValuePair( + id = uuidString(), + key = "file2", + value = "src/test/resources/testFile2.txt", + valueType = FieldValueType.File, + isEnabled = true, + ), + UserKeyValuePair( + id = uuidString(), + key = "file1", + value = "src/test/resources/testFile1中文字.txt", + valueType = FieldValueType.File, + isEnabled = true, + ), + UserKeyValuePair("MyFormParam", "abcc def_gh+i=?j/k"), + UserKeyValuePair("emoj", "a\uD83D\uDE0EBC"), + )), + ) + ) + ) + ) + } + + @Test + fun echoPostWithMultipartFilesAndHeaderAndQueryParameters() = runTest { + createAndSendRestEchoRequestAndAssertResponse( + UserRequestTemplate( + id = uuidString(), + method = "POST", + url = echoUrl, + examples = listOf( + UserRequestExample( + id = uuidString(), + name = "Base", + headers = listOf( + UserKeyValuePair("h1", "abcd"), + UserKeyValuePair("x-My-Header", "defg HIjk"), + ), + queryParameters = listOf( + UserKeyValuePair("abc", "中文字"), + UserKeyValuePair("MyQueryParam", "abc def_gh+i=?j/k"), + UserKeyValuePair("emoji", "A\uD83D\uDE0Eb"), + ), + contentType = ContentType.Multipart, + body = MultipartBody(listOf( + UserKeyValuePair("abcc", "中文字123"), + UserKeyValuePair( + id = uuidString(), + key = "file2", + value = "src/test/resources/testFile2.txt", + valueType = FieldValueType.File, + isEnabled = true, + ), + UserKeyValuePair( + id = uuidString(), + key = "file1", + value = "src/test/resources/testFile1中文字.txt", + valueType = FieldValueType.File, + isEnabled = true, + ), + UserKeyValuePair("MyFormParam", "abcc def_gh+i=?j/k"), + UserKeyValuePair("emoj", "a\uD83D\uDE0EBC"), + )), + ) + ) + ) + ) + } } fun runTest(testBlock: suspend ComposeUiTest.() -> Unit) = @@ -463,7 +571,54 @@ suspend fun ComposeUiTest.createAndSendHttpRequest(request: UserRequestTemplate, delayShort() } } - ContentType.Multipart -> TODO() + + ContentType.Multipart -> { + val body = (baseExample.body as MultipartBody).value + body.forEachIndexed { index, it -> + waitUntilExactlyOneExists(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, TestTagPart.Key, index)!!)) + waitUntilExactlyOneExists(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, TestTagPart.Value, index)!!)) + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, TestTagPart.Key, index)!!)) + .assertIsDisplayedWithRetry(this) + .performTextInput(it.key) + delayShort() + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, TestTagPart.Key, index)!!)) + .assertIsDisplayedWithRetry(this) + .assertTextEquals(it.key) + + when (it.valueType) { + FieldValueType.String -> { + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, TestTagPart.Value, index)!!)) + .assertIsDisplayedWithRetry(this) + .performTextInput(it.value) + delayShort() + } + FieldValueType.File -> { + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, index, TestTagPart.ValueTypeDropdown, TestTagPart.DropdownButton)!!)) + .assertIsDisplayedWithRetry(this) + .performClickWithRetry(this) + delayShort() + + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, index, TestTagPart.ValueTypeDropdown, TestTagPart.DropdownItem, "File")!!)) + .assertIsDisplayedWithRetry(this) + .performClickWithRetry(this) + delayShort() + + testChooseFile = File(it.value) + val filename = testChooseFile!!.name + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, index, TestTagPart.FileButton)!!)) + .assertIsDisplayedWithRetry(this) + .performClickWithRetry(this) + + delay(100) + delayShort() + onNode(hasTestTag(buildTestTag(TestTagPart.RequestBodyMultipartForm, TestTagPart.Current, index, TestTagPart.FileButton)!!)) + .assertTextEquals(filename, includeEditableText = false) + + } + } + } + } + ContentType.FormUrlEncoded -> { val body = (baseExample.body as FormUrlEncodedBody).value body.forEachIndexed { index, it -> @@ -574,7 +729,25 @@ suspend fun ComposeUiTest.createAndSendRestEchoRequestAndAssertResponse(request: } else { assertEquals(0, resp.formData.size) } - assertEquals(0, resp.multiparts.size) + if (baseExample.body is MultipartBody) { + val body = (baseExample.body as MultipartBody).value.sortedBy { it.key } + resp.multiparts.sortedBy { it.name }.forEachIndexed { index, part -> + val reqPart = body[index] + assertEquals(reqPart.key, part.name) + when (reqPart.valueType) { + FieldValueType.String -> assertEquals(reqPart.value, part.data) + FieldValueType.File -> { + val file = File(reqPart.value) + assertEquals(file.length().toInt(), part.size) + if (part.data != null) { + assertEquals(file.readText(), part.data) + } + } + } + } + } else { + assertEquals(0, resp.multiparts.size) + } when (val body = baseExample.body) { null, is FormUrlEncodedBody, is MultipartBody -> assertEquals(null, resp.body) is FileBody -> TODO() diff --git "a/ux-and-transport-test/src/test/resources/testFile1\344\270\255\346\226\207\345\255\227.txt" "b/ux-and-transport-test/src/test/resources/testFile1\344\270\255\346\226\207\345\255\227.txt" new file mode 100644 index 00000000..f1cf00d9 --- /dev/null +++ "b/ux-and-transport-test/src/test/resources/testFile1\344\270\255\346\226\207\345\255\227.txt" @@ -0,0 +1,2 @@ +There are some 中文字 here. +😎✌🏽Yeah! diff --git a/ux-and-transport-test/src/test/resources/testFile2.txt b/ux-and-transport-test/src/test/resources/testFile2.txt new file mode 100644 index 00000000..3963e2b9 --- /dev/null +++ b/ux-and-transport-test/src/test/resources/testFile2.txt @@ -0,0 +1,4 @@ +abcdef + +GHIjk ++=s-+#@$*&@(7567!()*&@(#_\n