Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
779a7c6
Implement shared native editor commands and lazy page placeholders
tiensonqin Oct 22, 2026
dd7745e
Resolve referenced display titles and select saved views with native …
tiensonqin Oct 22, 2026
1e31f50
Match master slash command groups and priority order
tiensonqin Oct 22, 2026
4d6f4ce
Match parent Enter placement and show task dates with shared properties
tiensonqin Oct 22, 2026
a4f7c43
Align editor boundaries and compact task properties with native controls
tiensonqin Oct 22, 2026
3794df1
Use compact typed native filters with master operator semantics
tiensonqin Oct 22, 2026
2e75d5c
Keep text and placeholder geometry stable across editing
tiensonqin Oct 22, 2026
122e9d9
Keep outliner rows compact and align task properties with text
tiensonqin Oct 22, 2026
4c50ca7
Keep task dates on one line with continuous outline guides
tiensonqin Oct 22, 2026
89b151a
Fix mobile editor stability and preserve structured block clipboard data
tiensonqin Oct 22, 2026
dd8bdd5
Support graph date formats in native settings and journal displays
tiensonqin Oct 22, 2026
59d14e6
Keep native navigation synchronized and verify composer tag flows
tiensonqin Oct 22, 2026
04c10b4
Match master link paste behavior and verify Recent selection
tiensonqin Oct 22, 2026
6df35e5
Record final interaction checks and iPhone install
tiensonqin Oct 22, 2026
ec84911
Reuse block disclosure icons for query results
tiensonqin Oct 22, 2026
466dfa8
Hide date format controls from native settings
tiensonqin Oct 22, 2026
e5c39f7
Protect page renames and enable table Name editing
tiensonqin Oct 22, 2026
e7b7157
Record native rename checks, device install and crash correlation
tiensonqin Oct 22, 2026
aa71b9c
Record isolated navigation crash experiments with ordered diagnostics
tiensonqin Oct 22, 2026
63b10d4
Fix retained editor focus and protect built-in page actions
tiensonqin Oct 22, 2026
e302dbb
Record final native focus checks and iPhone installation
tiensonqin Oct 22, 2026
72ae7c2
Fix native command form focus and outliner view controls
tiensonqin Oct 22, 2026
9531c0d
Pin Android custom button label validation and record native checks
tiensonqin Oct 22, 2026
7ea93ad
Record single-window background crash controls and device limits
tiensonqin Oct 22, 2026
ba2ef1f
Record physical recovery and mixed-content crash controls
tiensonqin Oct 22, 2026
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 @@ -30,6 +30,7 @@ import com.logseq.app.ui.LogseqTheme
import com.logseq.app.ui.extensions.logseqExtensionRegistry
import dev.lui.LuiBackend
import dev.lui.LuiEvent
import org.json.JSONObject
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
Expand All @@ -41,6 +42,7 @@ import org.junit.runner.RunWith
class AndroidComposerMenuTest {
@get:Rule val ui = createComposeRule()
private var bridge: LogseqNativeBridge? = null
private var patchFilter: LuiPatchFilter? = null
private val events = mutableListOf<LuiEvent>()

@After fun closeBridge() { bridge?.let { ui.runOnIdle { it.close() } } }
Expand All @@ -53,6 +55,7 @@ class AndroidComposerMenuTest {
icons = logseqIconResolver,
)
val filter = LuiPatchFilter(backend)
patchFilter = filter
native = LogseqNativeBridge(onPatch = { filter.applyJson(it) })
bridge = native
val root = native.initialize(authenticationCode = 3)
Expand Down Expand Up @@ -144,6 +147,108 @@ class AndroidComposerMenuTest {
ui.onNodeWithTag("field.composer").assertTextEquals("Unsent draft 中文")
}

private fun effect(kind: String): JSONObject {
var found: JSONObject? = null
ui.waitUntil(timeoutMillis = 5000) {
ui.runOnIdle {
repeat(24) {
val raw = bridge!!.takeEffect()
if (raw.isNotEmpty()) {
val result = JSONObject(raw)
result.optString("patch").takeIf { it.isNotEmpty() }?.let { patchFilter!!.applyJson(it) }
result.optJSONObject("effect")?.let { if (it.optString("kind") == kind) found = it }
}
}
}
found != null
}
return found!!
}

private fun resolve(effect: JSONObject, succeeded: Boolean = true) = ui.runOnIdle {
bridge!!.resolveEffect(effect.getLong("id"), succeeded, if (succeeded) "" else "Storage failed")
.takeIf { it.isNotEmpty() }?.let { patchFilter!!.applyJson(it) }
}

private fun tagCatalog(load: JSONObject, graph: String = "composer-test", tag: String = "project", title: String = "Project") {
ui.runOnIdle {
bridge!!.applySnapshot(JSONObject().put("ok", true).put("result", JSONObject()
.put("selectedGraphId", graph).put("composerTagChoices", org.json.JSONArray()
.put(JSONObject().put("uuid", tag).put("title", title)))).toString())
.takeIf { it.isNotEmpty() }?.let { patchFilter!!.applyJson(it) }
}
resolve(load)
}

@Test fun tagsUseTheSharedNativePickerAndRecoverFailedCaptures() {
composer()
val attachment = ui.onNodeWithTag("button.attachment").fetchSemanticsNode().boundsInRoot
val status = ui.onNodeWithTag("button.task-status").fetchSemanticsNode().boundsInRoot
val tags = ui.onNodeWithTag("button.composer.tags").fetchSemanticsNode().boundsInRoot
assertTrue("Hashtag follows attachment and task status", attachment.left < status.left && status.left < tags.left)
fun openTags() {
ui.onNodeWithTag("button.composer.tags").performClick()
tagCatalog(effect("load-composer-tags"))
ui.onNodeWithTag("view.edit.value.composer.tags").assertIsDisplayed()
}
openTags()
ui.onNodeWithTag("view.edit.option.composer.tags.project").performClick()
ui.onNodeWithTag("view.edit.save.composer.tags").performClick()
ui.onNodeWithTag("button.composer.tag.remove.project").assertIsDisplayed()
openTags()
ui.onNodeWithTag("view.edit.value.composer.tags").performTextInput("Mobile")
ui.onNodeWithTag("view.edit.create.composer.tags").performClick()
ui.onNodeWithTag("view.edit.save.composer.tags").performClick()
ui.onNodeWithTag("button.composer.tag.remove.new:mobile").assertIsDisplayed()
ui.onNodeWithTag("button.composer.tag.remove.project").performClick()
openTags()
ui.onNodeWithTag("view.edit.value.composer.tags").performTextInput("Discard")
ui.onNodeWithTag("view.edit.create.composer.tags").performClick()
ui.onNodeWithTag("view.edit.cancel.composer.tags").performClick()
ui.onNodeWithTag("button.composer.tag.remove.new:discard").assertDoesNotExist()
ui.onNodeWithTag("field.composer").assertTextEquals("Unsent draft 中文")
ui.onNodeWithTag("button.send").performClick()
val send = effect("send-tagged")
val payload = JSONObject(send.getString("text"))
assertEquals("Unsent draft 中文", payload.getString("text"))
assertEquals("Mobile", payload.getJSONObject("tags").getJSONArray("newValues").getString(0))
assertEquals(0, payload.getJSONObject("tags").getJSONArray("selectedValues").length())
resolve(send, succeeded = false)
ui.onNodeWithTag("field.composer").assertTextEquals("Unsent draft 中文")
ui.onNodeWithTag("button.composer.tag.remove.new:mobile").assertIsDisplayed().performClick()
ui.onNodeWithTag("button.send").performClick()
val retry = effect("send-capture")
assertEquals("Unsent draft 中文", retry.getString("text"))
resolve(retry)
ui.onNodeWithTag("field.composer").assert(SemanticsMatcher.expectValue(
SemanticsProperties.EditableText, androidx.compose.ui.text.AnnotatedString("")))
ui.onNodeWithTag("button.composer.tag.remove.new:mobile").assertDoesNotExist()
}

@Test fun changingGraphsDismissesOldTagsAndIgnoresAnOldCatalogFailure() {
composer()
ui.onNodeWithTag("button.composer.tags").performClick()
tagCatalog(effect("load-composer-tags"))
ui.onNodeWithTag("view.edit.option.composer.tags.project").performClick()
ui.onNodeWithTag("view.edit.save.composer.tags").performClick()
ui.onNodeWithTag("button.composer.tags").performClick()
val old = effect("load-composer-tags")
ui.runOnIdle {
bridge!!.applySnapshot("""{"ok":true,"result":{"selectedGraphId":"other","graphName":"Other","isGraphUnlocked":true}}""")
.takeIf { it.isNotEmpty() }?.let { patchFilter!!.applyJson(it) }
}
ui.onNodeWithTag("button.composer.tag.remove.project").assertDoesNotExist()
ui.onNodeWithTag("view.edit.value.composer.tags").assertDoesNotExist()
ui.onNodeWithTag("button.composer.tags").performClick()
val current = effect("load-composer-tags")
resolve(old, succeeded = false)
tagCatalog(current, graph = "other", tag = "book", title = "Book")
ui.onNodeWithTag("view.edit.option.composer.tags.project").assertDoesNotExist()
ui.onNodeWithTag("view.edit.option.composer.tags.book").assertIsDisplayed().performClick()
ui.onNodeWithTag("view.edit.save.composer.tags").performClick()
ui.onNodeWithTag("button.composer.tag.remove.book").assertIsDisplayed()
}

private fun retainedMenu(enabled: Boolean = true) {
val backend = LuiBackend(onEvent = { events.add(it) }, icons = logseqIconResolver)
assertTrue(backend.applyJson("""{"generation":1,"ops":[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.logseq.app

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.logseq.app.runtime.LogseqNativeBridge
import com.logseq.app.runtime.LuiPatchFilter
import com.logseq.app.runtime.dispatchLuiEvent
import com.logseq.app.runtime.logseqIconResolver
import com.logseq.app.ui.LogseqTheme
import com.logseq.app.ui.extensions.logseqExtensionRegistry
import dev.lui.LuiBackend
import org.json.JSONObject
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AndroidDateFormatTest {
@get:Rule val ui = createComposeRule()
private lateinit var bridge: LogseqNativeBridge
private lateinit var filter: LuiPatchFilter

@After fun closeBridge() { if (::bridge.isInitialized) ui.runOnIdle { bridge.close() } }

private fun snapshot(graph: String, format: String?): String = JSONObject().put("ok", true)
.put("result", JSONObject().put("selectedGraphId", graph).put("graphName", graph)
.apply { if (format != null) put("journalTitleFormat", format) }).toString()

@Test fun dateFormatIsAbsentWhileEditorSettingsRemainAvailable() {
val backend = LuiBackend(onEvent = { dispatchLuiEvent(it, bridge) },
extensions = logseqExtensionRegistry { it }, icons = logseqIconResolver)
filter = LuiPatchFilter(backend)
bridge = LogseqNativeBridge(onPatch = { if (it.isNotEmpty()) filter.applyJson(it) })
val root = bridge.initialize(authenticationCode = 3)
filter.applyJson(bridge.applySnapshot(snapshot("demo", "yyyy-MM-dd")))
bridge.applyHostUpdate("graph-loading", "false").takeIf { it.isNotEmpty() }?.let(filter::applyJson)
bridge.applyHostUpdate("restore-ui-session", """{"graphId":"demo","destination":"journals","assets":[],"composerExpanded":false,"searchOpen":false,"query":"","appPath":[],"searchPath":[],"selectedPageId":null,"settingsOpen":true,"recentGraphIds":[]}""")
.takeIf { it.isNotEmpty() }?.let(filter::applyJson)
ui.setContent {
MaterialExpressiveTheme(colorScheme = LogseqTheme.darkScheme, typography = LogseqTheme.typography) {
Surface(Modifier.fillMaxSize(), color = LogseqTheme.darkScheme.background) { backend.Content(root) }
}
}
ui.onNodeWithTag("picker.settings.date-format").assertDoesNotExist()
ui.onNodeWithText("Date format").assertDoesNotExist()
ui.onNodeWithTag("switch.settings.spell-check").assertIsDisplayed()
ui.runOnIdle { bridge.applySnapshot(snapshot("other", null)).takeIf { it.isNotEmpty() }?.let(filter::applyJson) }
ui.onNodeWithTag("picker.settings.date-format").assertDoesNotExist()
ui.onNodeWithText("MMM do, yyyy").assertDoesNotExist()
}
}
Loading
Loading