Compare commits
3 Commits
b6da868103
...
29e859bc01
| Author | SHA1 | Date | |
|---|---|---|---|
| 29e859bc01 | |||
| 784f92bd40 | |||
| 11a368b7c8 |
@@ -18,6 +18,7 @@ import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
|
||||
import androidx.test.espresso.intent.VerificationModes.times
|
||||
import androidx.test.espresso.matcher.RootMatchers.isDialog
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
@@ -578,6 +579,30 @@ class BoardsFlowTest {
|
||||
onView(withId(R.id.settingsSaveAndCloseButton)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun boardTapIsIgnoredWhileSettingsOpenIsPending() {
|
||||
MainActivity.dependencies.apiClientFactory = {
|
||||
FakeBoardsApiClient(
|
||||
boards = mutableListOf(BoardSummary("1", "Alpha")),
|
||||
templates = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
|
||||
scenario.onActivity { activity ->
|
||||
val field = activity.javaClass.getDeclaredField("pendingOpenSettingsAfterDrawerClose")
|
||||
field.isAccessible = true
|
||||
field.setBoolean(activity, true)
|
||||
}
|
||||
|
||||
onView(withText("Alpha")).perform(click())
|
||||
|
||||
Intents.intended(
|
||||
hasComponent(space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity::class.java.name),
|
||||
times(0),
|
||||
)
|
||||
}
|
||||
|
||||
private fun openSettingsFromDrawer() {
|
||||
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
|
||||
onView(withId(R.id.drawerSettingsButton)).perform(click())
|
||||
|
||||
@@ -43,6 +43,7 @@ import space.hackenslacker.kanbn4droid.app.boards.BoardsUiState
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsViewModel
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsDrawerWidthCalculator
|
||||
import space.hackenslacker.kanbn4droid.app.boards.DrawerDataErrorCode
|
||||
import space.hackenslacker.kanbn4droid.app.boards.shouldIgnoreBoardClick
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity
|
||||
import space.hackenslacker.kanbn4droid.app.settings.SettingsDialogFragment
|
||||
|
||||
@@ -140,7 +141,11 @@ class BoardsActivity : AppCompatActivity() {
|
||||
|
||||
private fun setupRecycler() {
|
||||
boardsAdapter = BoardsAdapter(
|
||||
onBoardClick = { board -> navigateToBoard(board) },
|
||||
onBoardClick = { board ->
|
||||
if (!shouldIgnoreBoardClick()) {
|
||||
navigateToBoard(board)
|
||||
}
|
||||
},
|
||||
onBoardLongClick = { board -> showDeleteConfirmation(board) },
|
||||
)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
@@ -458,6 +463,13 @@ class BoardsActivity : AppCompatActivity() {
|
||||
.show(supportFragmentManager, SettingsDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private fun shouldIgnoreBoardClick(): Boolean {
|
||||
return shouldIgnoreBoardClick(
|
||||
pendingOpenSettingsAfterDrawerClose = pendingOpenSettingsAfterDrawerClose,
|
||||
isDrawerOpen = drawerLayout.isDrawerOpen(GravityCompat.START),
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateToBoard(board: BoardSummary) {
|
||||
startActivity(
|
||||
Intent(this, BoardDetailActivity::class.java)
|
||||
|
||||
@@ -387,25 +387,47 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
tagPublicIds: List<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val payloadFields = mutableListOf(
|
||||
"\"listPublicId\":\"${jsonEscape(listPublicId)}\"",
|
||||
"\"title\":\"${jsonEscape(title)}\"",
|
||||
"\"index\":0",
|
||||
"\"labelPublicIds\":${jsonStringArray(tagPublicIds)}",
|
||||
val canonicalPayload = buildCreateCardPayload(
|
||||
listPublicId = listPublicId,
|
||||
title = title,
|
||||
description = description,
|
||||
dueDate = dueDate,
|
||||
tagPublicIds = tagPublicIds,
|
||||
aliasKeys = false,
|
||||
)
|
||||
if (description != null) {
|
||||
payloadFields += "\"description\":\"${jsonEscape(description)}\""
|
||||
val canonicalResult = request(
|
||||
baseUrl = baseUrl,
|
||||
path = "/api/v1/cards",
|
||||
method = "POST",
|
||||
apiKey = apiKey,
|
||||
body = canonicalPayload,
|
||||
) { code, body ->
|
||||
if (code in 200..299) {
|
||||
parseCreatedEntityRef(body)
|
||||
?.let { BoardsApiResult.Success(it) }
|
||||
?: BoardsApiResult.Failure("Malformed create card response.")
|
||||
} else {
|
||||
BoardsApiResult.Failure(serverMessage(body, code))
|
||||
}
|
||||
}
|
||||
if (dueDate != null) {
|
||||
payloadFields += "\"dueDate\":\"${dueDate}T00:00:00Z\""
|
||||
if (canonicalResult is BoardsApiResult.Success) {
|
||||
return@withContext canonicalResult
|
||||
}
|
||||
val payload = "{${payloadFields.joinToString(",")}}"
|
||||
|
||||
val aliasPayload = buildCreateCardPayload(
|
||||
listPublicId = listPublicId,
|
||||
title = title,
|
||||
description = description,
|
||||
dueDate = dueDate,
|
||||
tagPublicIds = tagPublicIds,
|
||||
aliasKeys = true,
|
||||
)
|
||||
request(
|
||||
baseUrl = baseUrl,
|
||||
path = "/api/v1/cards",
|
||||
method = "POST",
|
||||
apiKey = apiKey,
|
||||
body = payload,
|
||||
body = aliasPayload,
|
||||
) { code, body ->
|
||||
if (code in 200..299) {
|
||||
parseCreatedEntityRef(body)
|
||||
@@ -418,6 +440,44 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCreateCardPayload(
|
||||
listPublicId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: LocalDate?,
|
||||
tagPublicIds: List<String>,
|
||||
aliasKeys: Boolean,
|
||||
): String {
|
||||
val payloadFields = mutableListOf(
|
||||
"\"listPublicId\":\"${jsonEscape(listPublicId)}\"",
|
||||
if (aliasKeys) {
|
||||
"\"name\":\"${jsonEscape(title)}\""
|
||||
} else {
|
||||
"\"title\":\"${jsonEscape(title)}\""
|
||||
},
|
||||
"\"description\":\"${jsonEscape(description.orEmpty())}\"",
|
||||
"\"labelPublicIds\":${jsonStringArray(tagPublicIds)}",
|
||||
"\"memberPublicIds\":[]",
|
||||
"\"position\":\"start\"",
|
||||
)
|
||||
if (aliasKeys) {
|
||||
payloadFields += if (aliasKeys) {
|
||||
"\"body\":\"${jsonEscape(description.orEmpty())}\""
|
||||
} else {
|
||||
"\"description\":\"${jsonEscape(description.orEmpty())}\""
|
||||
}
|
||||
payloadFields += "\"index\":0"
|
||||
}
|
||||
if (dueDate != null) {
|
||||
payloadFields += if (aliasKeys) {
|
||||
"\"dueAt\":\"${dueDate}T00:00:00Z\""
|
||||
} else {
|
||||
"\"dueDate\":\"${dueDate}T00:00:00Z\""
|
||||
}
|
||||
}
|
||||
return "{${payloadFields.joinToString(",")}}"
|
||||
}
|
||||
|
||||
override suspend fun moveCard(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package space.hackenslacker.kanbn4droid.app.boards
|
||||
|
||||
internal fun shouldIgnoreBoardClick(
|
||||
pendingOpenSettingsAfterDrawerClose: Boolean,
|
||||
isDrawerOpen: Boolean,
|
||||
): Boolean {
|
||||
return pendingOpenSettingsAfterDrawerClose || isDrawerOpen
|
||||
}
|
||||
+9
-8
@@ -47,14 +47,6 @@ class SettingsDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_settings, null)
|
||||
|
||||
childFragmentManager.commitNow {
|
||||
replace(
|
||||
R.id.settingsFragmentContainer,
|
||||
SettingsPreferencesFragment(),
|
||||
SETTINGS_PREFS_TAG,
|
||||
)
|
||||
}
|
||||
|
||||
progress = dialogView.findViewById(R.id.settingsApplyProgress)
|
||||
errorText = dialogView.findViewById(R.id.settingsErrorText)
|
||||
saveButton = dialogView.findViewById(R.id.settingsSaveAndCloseButton)
|
||||
@@ -69,6 +61,15 @@ class SettingsDialogFragment : DialogFragment() {
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
|
||||
dialog.setOnShowListener {
|
||||
if (findPreferencesFragment() == null) {
|
||||
childFragmentManager.commitNow {
|
||||
replace(
|
||||
R.id.settingsFragmentContainer,
|
||||
SettingsPreferencesFragment(),
|
||||
SETTINGS_PREFS_TAG,
|
||||
)
|
||||
}
|
||||
}
|
||||
saveButton?.setOnClickListener {
|
||||
onSaveClicked()
|
||||
}
|
||||
|
||||
+34
-3
@@ -99,7 +99,7 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createCard_sendsTopIndex_andOmittedDueDateWhenNull() = runTest {
|
||||
fun createCard_sendsRequiredValidationFields_whenOptionalsAreNull() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.register(path = "/api/v1/cards", method = "POST", status = 200, responseBody = """{"publicId":"card-new"}""")
|
||||
|
||||
@@ -116,9 +116,10 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
val request = server.findRequest("POST", "/api/v1/cards")
|
||||
assertNotNull(request)
|
||||
assertTrue(request?.body?.contains("\"index\":0") == true)
|
||||
assertTrue(request?.body?.contains("\"position\":\"start\"") == true)
|
||||
assertTrue(request?.body?.contains("\"memberPublicIds\":[]") == true)
|
||||
assertTrue(request?.body?.contains("\"description\":\"\"") == true)
|
||||
assertTrue(request?.body?.contains("\"dueDate\"") == false)
|
||||
assertTrue(request?.body?.contains("\"description\"") == false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +173,36 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createCard_retriesWithAliasPayloadWhenServerRejectsCanonicalFields() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.registerSequence(
|
||||
path = "/api/v1/cards",
|
||||
method = "POST",
|
||||
responses = listOf(
|
||||
400 to "{}",
|
||||
200 to """{"card":{"publicId":"card-new"}}""",
|
||||
),
|
||||
)
|
||||
|
||||
val result = HttpKanbnApiClient().createCard(
|
||||
baseUrl = server.baseUrl,
|
||||
apiKey = "api",
|
||||
listPublicId = "list-1",
|
||||
title = "Card",
|
||||
description = "Description",
|
||||
dueDate = LocalDate.of(2026, 3, 16),
|
||||
tagPublicIds = listOf("tag-1"),
|
||||
)
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
val requests = server.findRequests("POST", "/api/v1/cards")
|
||||
assertEquals(2, requests.size)
|
||||
assertTrue(requests[0].body.contains("\"title\":\"Card\""))
|
||||
assertTrue(requests[1].body.contains("\"name\":\"Card\""))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoardDetailParsesWrappedPayloadWithDueDateVariants() = runTest {
|
||||
TestServer().use { server ->
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package space.hackenslacker.kanbn4droid.app.boards
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BoardsClickGuardTest {
|
||||
@Test
|
||||
fun ignoresBoardClickWhenSettingsOpenIsPending() {
|
||||
assertTrue(
|
||||
shouldIgnoreBoardClick(
|
||||
pendingOpenSettingsAfterDrawerClose = true,
|
||||
isDrawerOpen = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresBoardClickWhenDrawerIsOpen() {
|
||||
assertTrue(
|
||||
shouldIgnoreBoardClick(
|
||||
pendingOpenSettingsAfterDrawerClose = false,
|
||||
isDrawerOpen = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allowsBoardClickWhenNoSettingsOpenIsPendingAndDrawerClosed() {
|
||||
assertFalse(
|
||||
shouldIgnoreBoardClick(
|
||||
pendingOpenSettingsAfterDrawerClose = false,
|
||||
isDrawerOpen = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user