Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import android.view.MotionEvent
import android.view.inspector.WindowInspector
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsSelected
Expand Down Expand Up @@ -47,7 +50,7 @@ class AndroidComposerMenuTest {

@After fun closeBridge() { bridge?.let { ui.runOnIdle { it.close() } } }

private fun composer() {
private fun composer(scheme: ColorScheme = LogseqTheme.lightScheme) {
lateinit var native: LogseqNativeBridge
val backend = LuiBackend(
onEvent = { dispatchLuiEvent(it, native) },
Expand All @@ -62,14 +65,14 @@ class AndroidComposerMenuTest {
filter.applyJson(native.applySnapshot("""{"ok":true,"result":{
"graphName":"Composer test","selectedGraphId":"composer-test",
"isGraphUnlocked":true,"outlinerRows":[]}}"""))
content(backend, root)
content(backend, root, scheme)
ui.onNodeWithTag("button.composer.expand").performClick()
ui.onNodeWithTag("field.composer").performTextInput("Unsent draft 中文")
}

private fun content(backend: LuiBackend, root: Long) {
private fun content(backend: LuiBackend, root: Long, scheme: ColorScheme = LogseqTheme.lightScheme) {
ui.setContent {
MaterialTheme(colorScheme = LogseqTheme.lightScheme, typography = LogseqTheme.typography) {
MaterialTheme(colorScheme = scheme, typography = LogseqTheme.typography) {
Box(Modifier.fillMaxSize()) { backend.Content(root) }
}
}
Expand Down Expand Up @@ -139,6 +142,25 @@ class AndroidComposerMenuTest {
.assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.RadioButton))
}

@Test fun selectedMenuItemUsesTheLightSelectionPalette() {
assertSelectedMenuColor(LogseqTheme.lightScheme)
}

@Test fun selectedMenuItemUsesTheDarkSelectionPalette() {
assertSelectedMenuColor(LogseqTheme.darkScheme)
}

private fun assertSelectedMenuColor(scheme: ColorScheme) {
composer(scheme)
val status = ui.onNodeWithTag("button.task-status")
val todo = ui.onNodeWithText("Todo")
status.performClick()
todo.performClick()
status.performClick()
val pixels = todo.assertIsSelected().captureToImage().toPixelMap()
assertEquals(scheme.secondaryContainer, pixels[pixels.width * 9 / 10, pixels.height / 2])
}

@Test fun tappingOutsideTheComposerClosesItAndPreservesTheDraft() {
composer()
ui.onNodeWithTag("surface.composer.dismiss").performClick()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.logseq.logseq

import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.text.TextRange
Expand Down Expand Up @@ -68,15 +69,18 @@ class AndroidEditorKeyboardTest {
editor.performKeyInput { pressKey(Key.Enter) }
acknowledge("keyboard-block", "Hello ", 6)
editor.assertTextEquals("Hello 中文 📝")
editor.assert(SemanticsMatcher.expectValue(SemanticsProperties.TextSelectionRange, TextRange(6)))
acknowledge("next-block", "中文 📝", 0)
ui.onNodeWithTag("field.outliner.block.next-block").assertIsFocused().assertTextEquals("中文 📝")
.assert(SemanticsMatcher.expectValue(SemanticsProperties.TextSelectionRange, TextRange(0)))
}

@Test fun typingDuringSplitIsMergedAtTheNewCaretAndPublishedForTheNewBlock() {
val editor = render("Hello 中文 📝")
editor.performTextInputSelection(TextRange(6))
editor.performKeyInput { pressKey(Key.Enter) }
editor.performTextInput("新📝")
editor.assertTextEquals("Hello 中文 📝")
acknowledge("keyboard-block", "Hello ", 6)
acknowledge("next-block", "中文 📝", 0)
ui.onNodeWithTag("field.outliner.block.next-block").assertIsFocused().assertTextEquals("新📝中文 📝")
Expand Down Expand Up @@ -129,12 +133,33 @@ class AndroidEditorKeyboardTest {
} finally { ui.mainClock.autoAdvance = true }
}

@Test fun tappingAnotherBlockAppliesItsTextAndCaretInOneFrame() {
render("Source block")
ui.mainClock.autoAdvance = false
try {
ui.runOnUiThread {
val ops = JSONArray()
mapOf("block-id" to "target-block", "title" to "Target 中文 📝",
"caret-utf16-offset" to 7).forEach { (name, value) ->
ops.put(JSONObject().put("op", "set-extension-prop").put("id", 2)
.put("property", name).put("value", value))
}
assertTrue(backend.applyJson(JSONObject().put("generation", ++generation).put("ops", ops).toString()))
}
ui.mainClock.advanceTimeByFrame()
ui.onNodeWithTag("field.outliner.block.target-block").assertIsFocused().assertTextEquals("Target 中文 📝")
.assert(SemanticsMatcher.expectValue(SemanticsProperties.TextSelectionRange, TextRange(7)))
} finally { ui.mainClock.autoAdvance = true }
}

@Test fun softwareKeyboardNextCreatesOneBlockWithoutCommittingANewline() {
val editor = render("Native keyboard")
val editor = render("")
assertEquals(android.view.inputmethod.EditorInfo.IME_ACTION_NEXT,
editorInfo().imeOptions and android.view.inputmethod.EditorInfo.IME_MASK_ACTION)
editor.performTextInput("Native keyboard")
editor.performImeAction()
editor.assertTextEquals("Native keyboard")
editor.assert(SemanticsMatcher.expectValue(SemanticsProperties.TextSelectionRange, TextRange("Native keyboard".length)))
ui.runOnIdle { assertEquals(1, events.filterIsInstance<LuiEvent.Extension>().count { it.name == "return" }) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.assertIsDisplayed
Expand All @@ -15,6 +17,9 @@ import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.click
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
Expand Down Expand Up @@ -45,7 +50,7 @@ class AndroidOutlinerSpacingTest {

@After fun closeBridge() { bridge?.let { ui.runOnIdle { it.close() } } }

private fun snapshot(editing: Boolean = false, priority: Boolean = false, status: Boolean = false, dates: Boolean = false, hasChildren: Boolean = true): String {
private fun snapshot(editing: Boolean = false, priority: Boolean = false, status: Boolean = false, dates: Boolean = false, hasChildren: Boolean = true, editingUuid: String = "parent"): String {
fun row(id: String, text: String, depth: Int): JSONObject {
val block = JSONObject().put("uuid", id).put("title", text).put("pageId", "spacing-journal")
.put("journalTitle", journal).put("journalDay", 20261022)
Expand All @@ -61,8 +66,8 @@ class AndroidOutlinerSpacingTest {
return JSONObject().put("depth", depth).put("hasChildren", depth == 0 && hasChildren).put("block", block)
}
val state = JSONObject()
if (editing) state.put("editing", JSONObject().put("uuid", "parent")
.put("title", "Parent note").put("caretUTF16Offset", 0))
if (editing) state.put("editing", JSONObject().put("uuid", editingUuid)
.put("title", if (editingUuid == "parent") "Parent note" else "Child note").put("caretUTF16Offset", 0))
return JSONObject().put("ok", true).put("result", JSONObject()
.put("graphName", "Spacing test").put("selectedGraphId", "spacing-test")
.put("isGraphUnlocked", true).put("outlinerState", state)
Expand Down Expand Up @@ -101,6 +106,45 @@ class AndroidOutlinerSpacingTest {
assertTrue("Editing does not reserve an empty priority gap: ${gap(true)}", gap(true) in 0.dp..4.dp)
}

@Test fun tappingBlockTextKeepsTheTextLayoutWhenTheEditorOpens() {
render()
val content = ui.onNodeWithTag("outliner-block-content-parent")
val body = ui.onNodeWithText("Parent note")
val before = mutableListOf<TextLayoutResult>()
body.performSemanticsAction(SemanticsActions.GetTextLayoutResult) { it(before) }
val initialLeft = body.getUnclippedBoundsInRoot().left
content.performTouchInput { click(center) }
ui.runOnIdle {
assertTrue(events.filterIsInstance<LuiEvent.Extension>().any { it.name == "edit" })
filter.applyJson(requireNotNull(bridge).applySnapshot(snapshot(editing = true)))
}
val editor = ui.onNodeWithTag("field.outliner.block.parent").assertIsFocused()
val after = mutableListOf<TextLayoutResult>()
editor.performSemanticsAction(SemanticsActions.GetTextLayoutResult) { it(after) }
assertEquals(initialLeft, editor.getUnclippedBoundsInRoot().left)
assertEquals(before.single().getLineRight(0), after.single().getLineRight(0), 0.5f)
assertEquals(before.single().firstBaseline, after.single().firstBaseline, 0.5f)
}

@Test fun tappingBetweenBlocksKeepsEachTitleInTheFirstEditorFrame() {
render(editing = true)
val editorId = ui.onNodeWithTag("field.outliner.block.parent").assertIsFocused().fetchSemanticsNode().id
for (uuid in listOf("child", "parent", "child")) {
ui.onNodeWithTag("outliner-block-content-$uuid").performTouchInput { click(center) }
ui.mainClock.autoAdvance = false
try {
ui.runOnUiThread {
filter.applyJson(requireNotNull(bridge).applySnapshot(snapshot(editing = true, editingUuid = uuid)))
}
ui.mainClock.advanceTimeByFrame()
val editor = ui.onNodeWithTag("field.outliner.block.$uuid").assertIsFocused()
.assertTextEquals(if (uuid == "parent") "Parent note" else "Child note")
assertEquals("Switching blocks reuses the native editor", editorId, editor.fetchSemanticsNode().id)
ui.onNodeWithTag("outliner-block-content-${if (uuid == "parent") "child" else "parent"}").assertIsDisplayed()
} finally { ui.mainClock.autoAdvance = true }
}
}

@Test fun theBulletCenterAlignsWithTheJournalTitleLeadingEdge() {
render()
val heading = ui.onAllNodesWithText(journal, useUnmergedTree = true).fetchSemanticsNodes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
Expand Down Expand Up @@ -116,6 +120,11 @@ class AndroidOutlinerToolbarTest {
}
capture("editor-overflow")
reference.assertIsDisplayed()
ui.onNode(
hasContentDescription("Page reference") and
hasAnyAncestor(hasTestTag("button.outliner.editor.pageReference")),
useUnmergedTree = true,
).assertHeightIsEqualTo(24.dp)
assertEquals(before, hide.fetchSemanticsNode().boundsInRoot)
ui.runOnIdle { events.clear() }
hide.performClick()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class AndroidReviewLayoutTest {

@After fun closeBridge() { bridge?.let { ui.runOnIdle { it.close() } } }

private fun app(error: String? = null) {
private fun app(error: String? = null, projection: JSONObject? = null) {
lateinit var native: LogseqNativeBridge
val backend = LuiBackend(onEvent = { events.add(it); dispatchLuiEvent(it, native) },
extensions = logseqExtensionRegistry { it }, icons = logseqIconResolver)
Expand All @@ -59,6 +59,7 @@ class AndroidReviewLayoutTest {
val result = JSONObject().put("graphName", "Review graph").put("selectedGraphId", "review-layout")
.put("isGraphUnlocked", true).put("syncConnected", false).put("outlinerRows", JSONArray())
if (error != null) result.put("pendingSyncError", error)
projection?.keys()?.forEach { result.put(it, projection.get(it)) }
filter.applyJson(native.applySnapshot(JSONObject().put("ok", true).put("result", result).toString()))
ui.setContent {
MaterialExpressiveTheme(colorScheme = LogseqTheme.lightScheme, typography = LogseqTheme.typography) {
Expand All @@ -80,6 +81,41 @@ class AndroidReviewLayoutTest {
ui.onNodeWithTag("button.graph-add").performClick()
}

@Test fun completedFlashcardReviewIsCenteredAcrossTheScreen() {
app(projection = JSONObject().put("flashcardDecks",
JSONObject().put("completed", true).put("totalCards", 1)))
ui.onNodeWithTag("button.sidebar").performClick()
ui.onNodeWithTag("link.sidebar.flashcards").performClick()
val screen = ui.onNodeWithTag("screen.flashcards").fetchSemanticsNode().boundsInRoot
val complete = ui.onNodeWithTag("flashcards.complete").fetchSemanticsNode().boundsInRoot
assertEquals("The completion state is centered on the screen", screen.center.x, complete.center.x, 1f)
}

@Test fun graphUnlockFormFillsTheSheetWithEvenInsets() {
app(projection = JSONObject().put("isGraphEncrypted", true).put("isGraphUnlocked", false))
val sheet = ui.onNodeWithTag("sheet.graph-unlock").getUnclippedBoundsInRoot()
val field = ui.onNodeWithTag("field.graph-password").getUnclippedBoundsInRoot()
assertTrue("The password field has a leading inset", field.left - sheet.left >= 20.dp)
assertTrue("The password field fills the sheet", sheet.right - field.right <= 32.dp)
ui.onNodeWithTag("field.graph-password").performClick().performTextInput("local-layout-check")
ui.onNodeWithTag("button.graph-unlock.cancel").assertIsDisplayed()
ui.onNodeWithTag("button.graph-unlock").assertIsDisplayed()
}

@Test fun sidebarFavoritesAndRecentPagesUseCompactRows() {
fun pages(prefix: String) = JSONArray().apply {
repeat(3) { put(JSONObject().put("uuid", "$prefix-$it").put("title", "$prefix $it")) }
}
app(projection = JSONObject().put("favorites", pages("favorite")).put("recentPages", pages("recent")))
ui.onNodeWithTag("button.sidebar").performClick()
for (prefix in listOf("favorite", "recent")) {
val first = ui.onNodeWithTag("link.sidebar.page.$prefix-0").getUnclippedBoundsInRoot()
val second = ui.onNodeWithTag("link.sidebar.page.$prefix-1").getUnclippedBoundsInRoot()
assertTrue("Adjacent $prefix rows stay compact", second.top - first.top <= 50.dp)
assertTrue("Page rows retain a 48 dp height", first.bottom - first.top >= 48.dp)
}
}

@Test fun graphActionsAreVisibleOnFirstOpen() {
graphForm()
ui.onNodeWithTag("button.graph-add.cancel").assertIsDisplayed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,30 @@ private val copyReference: ImageVector = ImageVector.Builder(
lineTo(15f, 13f)
}.build()

private val pageReference: ImageVector = ImageVector.Builder(
name = "PageReference",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
).path(
fill = null,
stroke = SolidColor(Color.Black),
strokeLineWidth = 1.8f,
strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round,
) {
fun bracket(edge: Float, tip: Float) {
moveTo(tip, 4f)
lineTo(edge, 4f)
lineTo(edge, 20f)
lineTo(tip, 20f)
}
bracket(2.5f, 5.5f)
bracket(8.5f, 11f)
bracket(15.5f, 13f)
bracket(21.5f, 18.5f)
}.build()

// App icon vocabulary (the former logseq_icons.dart) mapped onto
// Material Symbols vectors — same symbolic names the core emits, matched
// to the SF Symbols / asset meanings the iOS renderer uses.
Expand Down Expand Up @@ -204,6 +228,7 @@ internal val logseqAppIcons: Map<String, ImageVector> = mapOf(
"toolbar-move-down" to Icons.Filled.ArrowDownward,
"toolbar-move-up" to Icons.Filled.ArrowUpward,
"toolbar-outdent" to Icons.AutoMirrored.Filled.ArrowBack,
"toolbar-page-reference" to pageReference,
"toolbar-tag" to Icons.Outlined.Tag,
"toolbar-task" to Icons.Outlined.CheckCircle,
"toolbar-unselect" to Icons.Filled.Clear,
Expand Down
10 changes: 8 additions & 2 deletions android/app/src/main/kotlin/com/logseq/logseq/ui/LogseqTheme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ internal object LogseqTheme {
val lightSurface = Color(0xfff8f9ff)
val darkSurface = Color(0xff101318)

// Material Expressive menus read the tertiary container roles for selection.
private fun ColorScheme.withMenuSelectionColors() = copy(
tertiaryContainer = secondaryContainer,
onTertiaryContainer = onSecondaryContainer,
)

val lightScheme = lightColorScheme(
primary = accent,
onPrimary = Color.White,
Expand All @@ -75,7 +81,7 @@ internal object LogseqTheme {
surfaceContainer = Color(0xffeceef4),
surfaceContainerHigh = Color(0xffe6e8ef),
surfaceContainerHighest = Color(0xffe0e2ea),
)
).withMenuSelectionColors()

val darkScheme = darkColorScheme(
primary = Color(0xff9bd3d4),
Expand All @@ -92,7 +98,7 @@ internal object LogseqTheme {
surfaceContainer = Color(0xff181c22),
surfaceContainerHigh = Color(0xff1d2229),
surfaceContainerHighest = Color(0xff22272f),
)
).withMenuSelectionColors()

fun semanticColors(scheme: ColorScheme): Map<String, Color> = mapOf(
"background" to scheme.background,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ import org.json.JSONObject
// android_embedded_media.dart, android_math.dart, android_cloze.dart.

// Block text matches iOS `.body` (17pt, ~22pt leading).
private val outlinerBodyStyle: androidx.compose.ui.text.TextStyle
internal val outlinerBodyStyle: TextStyle
@Composable get() = MaterialTheme.typography.bodyLarge.copy(
fontSize = if (LocalPropertyValue.current) 16.sp else 17.sp,
lineHeight = if (LocalPropertyValue.current) 21.sp else 22.sp,
Expand Down
Loading
Loading