mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Add member directory tab to home navigation (#3)
* feat(healthchat): add member directory tab to home navigation Third bottom-nav tab backed by the HealthChat API: searchable member cards with native start-chat, approval-pending banner, retry on failure and navigation into the native profile screens. * fix(healthchat): stop tab back handler swallowing back presses While a screen is pushed on top of Home, the still-composed tab BackHandler intercepted back presses during the transition and invisibly switched to the Chats tab. Gate it on the node's target visibility in both the parent and Home's own backstack. * fix(healthchat): satisfy detekt, ktlint and konsist rules Import alias order, composable parameter order in HomeView, and konsist presenter helper shape. * ci: pass the real repository name to the screenshot recorder recordScreenshots.yml set GITHUB_REPOSITORY from `secrets.GITHUB_REPOSITORY`. No such secret exists on this fork, so the expression expands to an empty string and overrides the built-in environment variable of the same name. recordScreenshots.sh checks it up front and exits 1 with "No repo specified", so the workflow could never record anything here. Use the `github.repository` context instead, which is always populated. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F * Update screenshots * Revert "Update screenshots" This reverts commitb99e0d643f. * Reapply "Update screenshots" This reverts commit85bceb6d67. * test(screenshots): stop a LayoutLib teardown crash failing the shard tests Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 races on HandlerThread startup: HandlerThread_Delegate.run calls Process.setThreadPriority, which throws NoSuchMethodError for Thread.setPosixNicenessInternal and kills the "android.bg" thread. If the test thread reaches getLooper() after that, it gets null, BackgroundThread.ensureThreadReady() NPEs, and the static initialiser of LatencyTracker fails. The JVM then marks that class erroneous for the lifetime of the fork, so every later preview showing the soft keyboard dies with NoClassDefFoundError. LayoutLibErrorFilterStatement already swallowed the bare NoSuchMethodError, but PaparazziLogger re-throws collected background errors from teardown() wrapped in a MultipleFailuresException when there is more than one, and that wrapper slipped through. A recent run reported 17 failures with zero image comparisons failing and zero delta images written. Unwrap the wrapper by reflection and swallow it only when every collected cause matches the known-bug list; anything unrecognised, and anything that cannot be decomposed, is rethrown. Also close a hole this filter had from the start. Paparazzi runs the test as `try { body } finally { teardown() }`, so a teardown error discards whatever the body threw — including a genuine snapshot mismatch. renderFailureCapture is registered inside the Paparazzi rule to record the body's exception so it wins over any teardown error, and the filter refuses to swallow anything if that rule is not registered. Two HorizontalFloatingToolbar previews have been mismatching silently behind this, and may now surface as real failures. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
@@ -17,24 +17,130 @@ import org.junit.runners.model.Statement
|
||||
* Tracked in https://github.com/cashapp/paparazzi/issues/2342 — fixed in layoutlib 16.2.3.
|
||||
* Remove this workaround once Paparazzi ships with layoutlib >= 16.2.3.
|
||||
*
|
||||
* Paparazzi's PaparazziLogger collects background thread errors and re-throws them via
|
||||
* assertNoErrors(). This Statement wrapper catches the known NoSuchMethodError so the test
|
||||
* can pass despite the LayoutLib bug.
|
||||
* The bug has two faces, and which one a run gets is a thread race:
|
||||
*
|
||||
* 1. `HandlerThread_Delegate.run` calls `Looper.prepare()` and then dies on
|
||||
* `Process.setThreadPriority`. If the caller reaches `HandlerThread.getLooper()` while the
|
||||
* thread is still alive it gets the looper and nothing else happens.
|
||||
* 2. If the thread has already died, `getLooper()` returns null, `BackgroundThread
|
||||
* .ensureThreadReady()` NPEs, and the static initialiser of `LatencyTracker` fails. The JVM
|
||||
* then marks that class erroneous forever, so every later preview in the same fork that shows
|
||||
* the soft keyboard fails with `NoClassDefFoundError`.
|
||||
*
|
||||
* Paparazzi's PaparazziLogger collects these background errors and re-throws them from
|
||||
* `teardown()` — as the error itself when there is one, or wrapped in a `MultipleFailuresException`
|
||||
* when there are several. This rule swallows them, and only them.
|
||||
*
|
||||
* IMPORTANT: Paparazzi runs the test body as `try { body } finally { teardown() }`, so when
|
||||
* teardown throws it *discards* whatever the body threw — including a real snapshot mismatch.
|
||||
* [renderFailureCapture] must therefore be registered as an inner rule so the body's exception
|
||||
* survives and wins. Without it, this rule could silently turn a genuine golden mismatch green.
|
||||
*/
|
||||
class LayoutLibErrorFilterStatement : TestRule {
|
||||
private var captureRuleRan = false
|
||||
private var thrownByTestBody: Throwable? = null
|
||||
|
||||
/**
|
||||
* Must be registered with `@get:Rule(order = 2)` so it is applied INSIDE the Paparazzi rule.
|
||||
* JUnit 4.13.2 applies the *lowest* `order` outermost, so 0 = this filter, 1 = Paparazzi,
|
||||
* 2 = the capture below.
|
||||
*/
|
||||
val renderFailureCapture: TestRule = TestRule { base, _ ->
|
||||
object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
captureRuleRan = true
|
||||
try {
|
||||
base.evaluate()
|
||||
} catch (error: Throwable) {
|
||||
thrownByTestBody = error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
try {
|
||||
base.evaluate()
|
||||
} catch (e: NoSuchMethodError) {
|
||||
if (e.message?.contains("setPosixNicenessInternal") != true) throw e
|
||||
} catch (npe: NullPointerException) {
|
||||
// Also catch this error on `HorizontalFloatingToolbarPreview` and `HorizontalFloatingToolbarNoFabPreview`
|
||||
if (npe.message?.contains("""Cannot invoke "java.util.ArrayList.size()" because "childrenList" is null""") != true) throw npe
|
||||
} catch (error: Throwable) {
|
||||
val fromBody = thrownByTestBody
|
||||
// A failure raised by the test body itself always wins over a teardown error.
|
||||
if (fromBody != null && !isKnownLayoutLibBug(fromBody)) throw fromBody
|
||||
if (!captureRuleRan) {
|
||||
throw IllegalStateException(
|
||||
"renderFailureCapture is not registered with @get:Rule(order = 2) on ${description.className}, " +
|
||||
"so a snapshot mismatch could be hidden by a LayoutLib teardown error. Refusing to filter.",
|
||||
error,
|
||||
)
|
||||
}
|
||||
if (!isKnownLayoutLibBug(error)) throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val MULTIPLE_FAILURES_EXCEPTION = "app.cash.paparazzi.internal.PaparazziLogger\$MultipleFailuresException"
|
||||
|
||||
private val knownLayoutLibBugMarkers = listOf(
|
||||
// The root bug: LayoutLib calls a JVM-internal method that does not exist.
|
||||
"setPosixNicenessInternal",
|
||||
// Its fallout: the poisoned static initialiser and the dead "android.bg" HandlerThread.
|
||||
"com.android.internal.util.LatencyTracker",
|
||||
"com.android.internal.os.BackgroundThread",
|
||||
"""Cannot invoke "android.os.Looper.setTraceTag(long)" because "looper" is null""",
|
||||
// Seen on HorizontalFloatingToolbarPreview and HorizontalFloatingToolbarNoFabPreview.
|
||||
"""Cannot invoke "java.util.ArrayList.size()" because "childrenList" is null""",
|
||||
)
|
||||
|
||||
/**
|
||||
* True only when *every* error carried by [error] is a known LayoutLib bug. Anything we cannot
|
||||
* decompose, and anything with an unrecognised error in it, is reported as a failure.
|
||||
*/
|
||||
private fun isKnownLayoutLibBug(error: Throwable): Boolean {
|
||||
val reports = describeEach(error) ?: return false
|
||||
return reports.isNotEmpty() && reports.all { report -> knownLayoutLibBugMarkers.any { report.contains(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* One text report per collected error, or null when a `MultipleFailuresException` cannot be
|
||||
* unpacked. It is `internal`, and it neither exposes its causes nor prints them via
|
||||
* `stackTraceToString()`, so reflection is the only way to inspect them individually.
|
||||
*/
|
||||
private fun describeEach(error: Throwable): List<String>? {
|
||||
if (error.javaClass.name != MULTIPLE_FAILURES_EXCEPTION) {
|
||||
return listOf(error.describe())
|
||||
}
|
||||
val causes = readCauses(error) ?: return null
|
||||
val reports = mutableListOf<String>()
|
||||
for (cause in causes) {
|
||||
val nested = describeEach(cause) ?: return null
|
||||
reports += nested
|
||||
}
|
||||
return reports
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun readCauses(error: Throwable): List<Throwable>? {
|
||||
return try {
|
||||
val field = error.javaClass.getDeclaredField("causes")
|
||||
field.isAccessible = true
|
||||
field.get(error) as? List<Throwable>
|
||||
} catch (failure: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.describe(): String {
|
||||
// `takeWhile { seen.add(it) }` also terminates a self-referential cause chain.
|
||||
val seen = mutableSetOf<Throwable>()
|
||||
return generateSequence(this) { it.cause }
|
||||
.takeWhile { seen.add(it) }
|
||||
.joinToString(separator = "\n") { error ->
|
||||
error.stackTrace.joinToString(separator = "\n", prefix = "$error\n") { frame -> "\tat $frame" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ class PreviewA11yTest(
|
||||
renderExtensions = setOf(AccessibilityRenderExtension()),
|
||||
)
|
||||
|
||||
// Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown().
|
||||
@get:Rule(order = 2)
|
||||
val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture
|
||||
|
||||
@Test
|
||||
fun snapshot() {
|
||||
ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en")
|
||||
|
||||
@@ -34,6 +34,10 @@ class PreviewShard1Test(
|
||||
@get:Rule(order = 1)
|
||||
val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en")
|
||||
|
||||
// Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown().
|
||||
@get:Rule(order = 2)
|
||||
val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture
|
||||
|
||||
@Test
|
||||
fun snapshot() {
|
||||
ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en")
|
||||
|
||||
@@ -34,6 +34,10 @@ class PreviewShard2Test(
|
||||
@get:Rule(order = 1)
|
||||
val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en")
|
||||
|
||||
// Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown().
|
||||
@get:Rule(order = 2)
|
||||
val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture
|
||||
|
||||
@Test
|
||||
fun snapshot() {
|
||||
ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en")
|
||||
|
||||
@@ -34,6 +34,10 @@ class PreviewShard3Test(
|
||||
@get:Rule(order = 1)
|
||||
val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en")
|
||||
|
||||
// Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown().
|
||||
@get:Rule(order = 2)
|
||||
val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture
|
||||
|
||||
@Test
|
||||
fun snapshot() {
|
||||
ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en")
|
||||
|
||||
@@ -34,6 +34,10 @@ class PreviewShard4Test(
|
||||
@get:Rule(order = 1)
|
||||
val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en")
|
||||
|
||||
// Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown().
|
||||
@get:Rule(order = 2)
|
||||
val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture
|
||||
|
||||
@Test
|
||||
fun snapshot() {
|
||||
ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en")
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user