SHA256
feat(android): align native preview with measured PWA design
This commit is contained in:
1 parent
bd0e4cc4fb
commit
7ac9882307
30 files changed
+2793
-750
No files matched your search
+111
-14
@@ -1,7 +1,10 @@
|
||||
package community.yuvomi.mobile
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
@@ -11,7 +14,6 @@ import androidx.compose.ui.test.isEnabled
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
@@ -33,6 +35,9 @@ class NativeUpstreamTest {
|
||||
val reset = object : ExternalResource() {
|
||||
override fun before() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
context.getSystemService(UiModeManager::class.java).setApplicationNightMode(UiModeManager.MODE_NIGHT_NO)
|
||||
}
|
||||
context.getSharedPreferences("native-server", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
context.getSharedPreferences("native-session", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
if (android.os.Build.VERSION.SDK_INT >= 37) {
|
||||
@@ -72,10 +77,31 @@ class NativeUpstreamTest {
|
||||
org.json.JSONObject().put("name", "Wochenmarkt"),
|
||||
).getJSONObject("data").getLong("id")
|
||||
seed.request("/shopping/$seedList/items", "POST", org.json.JSONObject().put("name", "Äpfel").put("quantity", "4"))
|
||||
seed.request(
|
||||
"/preferences",
|
||||
"PUT",
|
||||
org.json.JSONObject()
|
||||
.put(
|
||||
"dashboard_widgets",
|
||||
org.json.JSONArray(
|
||||
listOf("tasks", "calendar", "shopping", "notes").mapIndexed { order, id ->
|
||||
org.json.JSONObject().put("id", id).put("visible", true).put("order", order).put("size", "1x1")
|
||||
},
|
||||
),
|
||||
)
|
||||
.put("mobile_nav_order", org.json.JSONArray(listOf("tasks", "kitchen", "notes")))
|
||||
.put("timezone", "Europe/Berlin"),
|
||||
)
|
||||
seed.request("/auth/logout", "POST")
|
||||
compose.onNodeWithTag("server-url").performTextInput(origin)
|
||||
compose.onNodeWithTag("connect").performClick()
|
||||
awaitTag("username")
|
||||
captureFixtureScreen("login-light.png")
|
||||
nightMode(true)
|
||||
awaitTag("username")
|
||||
captureFixtureScreen("login-dark.png")
|
||||
nightMode(false)
|
||||
awaitTag("username")
|
||||
compose.onNodeWithTag("username").performTextInput("mobiletest")
|
||||
compose.onNodeWithTag("password").performTextInput(password + "-invalid")
|
||||
compose.onNodeWithTag("login").performScrollTo().performClick()
|
||||
@@ -87,15 +113,23 @@ class NativeUpstreamTest {
|
||||
compose.activityRule.scenario.onActivity {
|
||||
assertEquals(null, ViewModelProvider(it)[NativeController::class.java].state.error)
|
||||
}
|
||||
captureFixtureDashboard()
|
||||
captureFixtureScreen("native-dashboard.png")
|
||||
captureFixtureScreen("dashboard-light.png")
|
||||
captureModuleGallery("light")
|
||||
nightMode(true)
|
||||
awaitTag("native-dashboard")
|
||||
captureFixtureScreen("dashboard-dark.png")
|
||||
captureModuleGallery("dark")
|
||||
nightMode(false)
|
||||
awaitTag("native-dashboard")
|
||||
compose.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
|
||||
compose.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
compose.activityRule.scenario.recreate()
|
||||
awaitTag("native-dashboard")
|
||||
|
||||
compose.onNodeWithTag("nav-TASKS").performClick()
|
||||
awaitText("Hinzufügen")
|
||||
compose.onNodeWithText("Hinzufügen").performClick()
|
||||
awaitTag("add-entry")
|
||||
compose.onNodeWithTag("add-entry").performClick()
|
||||
compose.onNodeWithTag("entry-title").performTextInput("Native task regression")
|
||||
compose.onNodeWithTag("entry-detail").performTextInput("Created with native controls")
|
||||
compose.onNodeWithTag("entry-save").performClick()
|
||||
@@ -103,8 +137,8 @@ class NativeUpstreamTest {
|
||||
assertTrue(readApi(origin).request("/tasks").rows().any { it.text("title") == "Native task regression" })
|
||||
|
||||
compose.onNodeWithTag("nav-NOTES").performClick()
|
||||
awaitText("Hinzufügen")
|
||||
compose.onNodeWithText("Hinzufügen").performClick()
|
||||
awaitTag("add-entry")
|
||||
compose.onNodeWithTag("add-entry").performClick()
|
||||
compose.onNodeWithTag("entry-title").performTextInput("Native note regression")
|
||||
compose.onNodeWithTag("entry-detail").performTextInput("Not an embedded web page")
|
||||
compose.onNodeWithTag("entry-save").performClick()
|
||||
@@ -112,14 +146,14 @@ class NativeUpstreamTest {
|
||||
assertTrue(readApi(origin).request("/notes").rows().any { it.text("content") == "Not an embedded web page" })
|
||||
|
||||
compose.onNodeWithTag("nav-SHOPPING").performClick()
|
||||
awaitText("Neue Liste")
|
||||
compose.onNodeWithText("Neue Liste").performClick()
|
||||
awaitTag("add-list")
|
||||
compose.onNodeWithTag("add-list").performClick()
|
||||
compose.onNodeWithTag("entry-title").performTextInput("Native shopping regression")
|
||||
compose.onNodeWithTag("entry-save").performClick()
|
||||
awaitText("Native shopping regression")
|
||||
compose.onNodeWithText("Native shopping regression").performClick()
|
||||
awaitText("Hinzufügen")
|
||||
compose.onNodeWithText("Hinzufügen").performClick()
|
||||
awaitTag("add-entry")
|
||||
compose.onNodeWithTag("add-entry").performClick()
|
||||
compose.onNodeWithTag("entry-title").performTextInput("Native apples")
|
||||
compose.onNodeWithTag("entry-detail").performTextInput("3")
|
||||
compose.onNodeWithTag("entry-save").performClick()
|
||||
@@ -157,15 +191,67 @@ class NativeUpstreamTest {
|
||||
return api
|
||||
}
|
||||
|
||||
private fun captureFixtureDashboard() {
|
||||
private fun nightMode(dark: Boolean) {
|
||||
check(Build.VERSION.SDK_INT >= 31) { "The reference gallery requires per-app night mode (API 31+)" }
|
||||
compose.activityRule.scenario.onActivity {
|
||||
it.getSystemService(UiModeManager::class.java).setApplicationNightMode(
|
||||
if (dark) UiModeManager.MODE_NIGHT_YES else UiModeManager.MODE_NIGHT_NO,
|
||||
)
|
||||
}
|
||||
compose.waitUntil(10_000) {
|
||||
var matches = false
|
||||
compose.activityRule.scenario.onActivity {
|
||||
matches = (
|
||||
it.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK ==
|
||||
android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
) == dark
|
||||
}
|
||||
matches
|
||||
}
|
||||
compose.waitForIdle()
|
||||
}
|
||||
|
||||
private fun captureModuleGallery(theme: String) {
|
||||
listOf(NativePage.TASKS, NativePage.NOTES, NativePage.SHOPPING).forEach { page ->
|
||||
compose.onNodeWithTag("nav-${page.name}").performClick()
|
||||
compose.waitUntil(30_000) {
|
||||
var ready = false
|
||||
compose.activityRule.scenario.onActivity {
|
||||
val state = ViewModelProvider(it)[NativeController::class.java].state
|
||||
ready = !state.busy && state.page == page && state.rows.isNotEmpty()
|
||||
}
|
||||
ready
|
||||
}
|
||||
captureFixtureScreen("${page.name.lowercase()}-$theme.png")
|
||||
}
|
||||
compose.onNodeWithTag("nav-HOME").performClick()
|
||||
awaitTag("native-dashboard")
|
||||
}
|
||||
|
||||
private fun captureFixtureScreen(name: String) {
|
||||
// Only this temporary-fixture test clears the screenshot restriction.
|
||||
// Ordinary builds always retain FLAG_SECURE, including native login.
|
||||
compose.activityRule.scenario.onActivity { it.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
|
||||
compose.activityRule.scenario.onActivity {
|
||||
it.currentFocus?.clearFocus()
|
||||
it.window.insetsController?.hide(android.view.WindowInsets.Type.ime())
|
||||
it.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
try {
|
||||
compose.waitForIdle()
|
||||
compose.waitUntil(10_000) {
|
||||
var hidden = false
|
||||
compose.activityRule.scenario.onActivity {
|
||||
hidden = it.window.decorView.rootWindowInsets?.isVisible(android.view.WindowInsets.Type.ime()) != true
|
||||
}
|
||||
hidden
|
||||
}
|
||||
// PixelCopy must run after the real window/inset animation, not only Compose idleness.
|
||||
SystemClock.sleep(600)
|
||||
// Capture the native window through PixelCopy, not a potentially stale
|
||||
// secure compositor frame returned by UiAutomation immediately after clearing.
|
||||
val image = compose.onRoot().captureToImage().asAndroidBitmap()
|
||||
val node = compose.onNodeWithTag("native-screen")
|
||||
val bounds = node.fetchSemanticsNode().boundsInRoot
|
||||
val image = node.captureToImage().asAndroidBitmap()
|
||||
try {
|
||||
val first = image.getPixel(0, 0)
|
||||
assertTrue(
|
||||
@@ -175,9 +261,20 @@ class NativeUpstreamTest {
|
||||
},
|
||||
)
|
||||
// AGP collects this test output before uninstalling the fixture APK.
|
||||
PlatformTestStorageRegistry.getInstance().openOutputFile("native-dashboard.png").use {
|
||||
PlatformTestStorageRegistry.getInstance().openOutputFile(name).use {
|
||||
check(image.compress(Bitmap.CompressFormat.PNG, 100, it))
|
||||
}
|
||||
if (name == "native-dashboard.png") {
|
||||
val resources = compose.activity.resources
|
||||
val metadata = org.json.JSONObject()
|
||||
.put("widthPx", image.width).put("heightPx", image.height)
|
||||
.put("density", resources.displayMetrics.density).put("fontScale", resources.configuration.fontScale)
|
||||
.put("sdk", Build.VERSION.SDK_INT)
|
||||
.put("boundsInRoot", org.json.JSONArray(listOf(bounds.left, bounds.top, bounds.right, bounds.bottom)))
|
||||
PlatformTestStorageRegistry.getInstance().openOutputFile("viewport.json").use {
|
||||
it.write(metadata.toString(2).toByteArray())
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
image.recycle()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user