Skip to content

Commit afae846

Browse files
David Bertetfacebook-github-bot
authored andcommitted
Add spannables into view hierarchy dump
Summary: ### Context I wanted to write an E2E test for the SEV S499191 I noticed this is not possible as spannable aren't exposed to Jest E2E {F1977521046} While looking around, I found it's been done earlier this month on WA https://fb.workplace.com/groups/watai.fyi/permalink/2107714926342676/ In diffs D70791455 / D71190837 ### This diff Implement something similar to expose Spannables in view dump, so Jest E2E can leverage it Reviewed By: zielinskimz Differential Revision: D73817242 fbshipit-source-id: 6b4431b0369c812b159dc2a4448eb7376eed1032
1 parent 10f20be commit afae846

File tree

4 files changed

+154
-9
lines changed

4 files changed

+154
-9
lines changed

litho-core/src/main/java/com/facebook/litho/DebugComponentDescriptionHelper.kt

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.facebook.litho
1818

1919
import android.view.View
2020
import com.facebook.litho.DebugComponentDescriptionHelper.ExtraDescription
21+
import com.facebook.litho.LithoRenderUnit.Companion.getRenderUnit
2122
import com.facebook.litho.annotations.Prop
2223
import com.facebook.litho.annotations.ResType
2324
import java.lang.Exception
@@ -66,6 +67,22 @@ object DebugComponentDescriptionHelper {
6667
addViewDescription(debugComponent, sb, leftOffset, topOffset, embedded, withProps, null)
6768
}
6869

70+
private fun writeViewFlags(
71+
sb: StringBuilder,
72+
layout: DebugLayoutNode?,
73+
lithoView: BaseMountingView?
74+
) {
75+
sb.append(" ")
76+
sb.append(if (lithoView != null && lithoView.visibility == View.VISIBLE) "V" else ".")
77+
sb.append(if (layout != null && layout.focusable) "F" else ".")
78+
sb.append(if (lithoView != null && lithoView.isEnabled) "E" else ".")
79+
sb.append(".")
80+
sb.append(if (lithoView != null && lithoView.isHorizontalScrollBarEnabled) "H" else ".")
81+
sb.append(if (lithoView != null && lithoView.isVerticalScrollBarEnabled) "V" else ".")
82+
sb.append(if (layout?.clickHandler != null) "C" else ".")
83+
sb.append(". ..")
84+
}
85+
6986
/**
7087
* Appends a compact description of a [DebugComponent] for debugging purposes.
7188
*
@@ -100,17 +117,10 @@ object DebugComponentDescriptionHelper {
100117
sb.append(debugComponent.component.simpleName)
101118
sb.append('{')
102119
sb.append(Integer.toHexString(debugComponent.hashCode()))
103-
sb.append(' ')
104120
val lithoView = debugComponent.lithoView
105121
val layout = debugComponent.layoutNode
106-
sb.append(if (lithoView != null && lithoView.visibility == View.VISIBLE) "V" else ".")
107-
sb.append(if (layout != null && layout.focusable) "F" else ".")
108-
sb.append(if (lithoView != null && lithoView.isEnabled) "E" else ".")
109-
sb.append(".")
110-
sb.append(if (lithoView != null && lithoView.isHorizontalScrollBarEnabled) "H" else ".")
111-
sb.append(if (lithoView != null && lithoView.isVerticalScrollBarEnabled) "V" else ".")
112-
sb.append(if (layout?.clickHandler != null) "C" else ".")
113-
sb.append(". .. ")
122+
writeViewFlags(sb, layout, lithoView)
123+
sb.append(' ')
114124
// using position relative to litho view host to handle relative position issues
115125
// the offset is for the parent component to create proper relative coordinates
116126
val bounds = debugComponent.boundsInLithoView
@@ -139,6 +149,41 @@ object DebugComponentDescriptionHelper {
139149
sb.append('}')
140150
}
141151

152+
fun getSyntheticViewDescriptions(debugComponent: DebugComponent): List<String> {
153+
val lithoView = debugComponent.lithoView ?: return emptyList()
154+
val result = mutableListOf<String>()
155+
val mountDelegateTarget = lithoView.mountDelegateTarget
156+
for (i in 0 until mountDelegateTarget.getMountItemCount()) {
157+
val mountItem = mountDelegateTarget.getMountItemAt(i)
158+
val mountItemComponent = mountItem?.let { getRenderUnit(it).component }
159+
if (mountItemComponent?.id == debugComponent.component.id) {
160+
val content = mountItem.content
161+
if (content is TextContent) {
162+
for (item in content.items) {
163+
for (spannable in item.spannables) {
164+
result.add(
165+
buildString {
166+
append(spannable.className)
167+
append("{")
168+
append(spannable.hash)
169+
writeViewFlags(this, debugComponent.layoutNode, lithoView)
170+
val spanBounds = spannable.bounds
171+
append(
172+
" ${spanBounds.left},${spanBounds.top}-${spanBounds.right},${spanBounds.bottom}")
173+
append(" text=\"")
174+
append(spannable.text)
175+
append("\"")
176+
append(" synthetic=\"true\"")
177+
append("}")
178+
})
179+
}
180+
}
181+
}
182+
}
183+
}
184+
return result
185+
}
186+
142187
private fun addExtraProps(node: Any, sb: StringBuilder) {
143188
val props = getExtraProps(node)
144189
if (props.length() > 0) {

litho-core/src/main/java/com/facebook/litho/LithoViewTestHelper.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@ object LithoViewTestHelper {
161161
DebugComponentDescriptionHelper.addViewDescription(
162162
component, sb, leftOffset, topOffset, embedded, withProps, extraDescription)
163163
sb.append("\n")
164+
165+
val spannedTextContent = DebugComponentDescriptionHelper.getSyntheticViewDescriptions(component)
166+
// for each line of text, we need to add an extra indent
167+
spannedTextContent.forEach { line ->
168+
writeIndentByDepth(sb, depth + 1)
169+
sb.append(line)
170+
sb.append("\n")
171+
}
172+
164173
val bounds = component.boundsInLithoView
165174
for (child in component.childComponents) {
166175
viewToString(

litho-core/src/main/java/com/facebook/litho/TextContent.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.facebook.litho
1818

19+
import android.graphics.Rect
1920
import android.graphics.Typeface
2021
import com.facebook.proguard.annotations.DoNotStrip
2122

@@ -52,5 +53,17 @@ interface TextContent {
5253
val color: Int
5354

5455
val linesCount: Int
56+
57+
val spannables: List<SpannableItem>
58+
}
59+
60+
interface SpannableItem {
61+
val className: String
62+
63+
val hash: String
64+
65+
val text: String
66+
67+
val bounds: Rect
5568
}
5669
}

litho-widget/src/main/java/com/facebook/litho/widget/TextDrawable.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import com.facebook.litho.Touchable;
5252
import com.facebook.litho.config.ComponentsConfiguration;
5353
import com.facebook.litho.utils.VersionedAndroidApis;
54+
import java.util.ArrayList;
5455
import java.util.Collections;
5556
import java.util.List;
5657
import javax.annotation.Nullable;
@@ -941,6 +942,83 @@ public float getFontLineHeight() {
941942
public int getLinesCount() {
942943
return linesCount;
943944
}
945+
946+
@Override
947+
public List<TextContent.SpannableItem> getSpannables() {
948+
List<TextContent.SpannableItem> result = new ArrayList<>();
949+
if (text instanceof Spanned) {
950+
final Spanned spanned = (Spanned) text;
951+
final Object[] spans = spanned.getSpans(0, text.length(), Object.class);
952+
for (final Object span : spans) {
953+
final int start = spanned.getSpanStart(span);
954+
final int end = spanned.getSpanEnd(span);
955+
if (start != -1 && end != -1 && start != end) {
956+
result.add(
957+
new TextContent.SpannableItem() {
958+
@Override
959+
public String getClassName() {
960+
return span.getClass().getName();
961+
}
962+
963+
@Override
964+
public String getHash() {
965+
return Integer.toHexString(span.hashCode());
966+
}
967+
968+
@Override
969+
public String getText() {
970+
return text.subSequence(start, end).toString();
971+
}
972+
973+
@Override
974+
public Rect getBounds() {
975+
if (mLayout == null) {
976+
return new Rect(0, 0, 0, 0);
977+
}
978+
try {
979+
final int startLine = mLayout.getLineForOffset(start);
980+
final int endLine = mLayout.getLineForOffset(end);
981+
982+
final int startX = (int) mLayout.getPrimaryHorizontal(start);
983+
final int startY = mLayout.getLineTop(startLine);
984+
985+
final int endX;
986+
final int endY;
987+
if (startLine == endLine) {
988+
// Spannable fits into a single line, using regular bounds
989+
endX = (int) mLayout.getPrimaryHorizontal(end);
990+
endY = mLayout.getLineBottom(endLine);
991+
} else {
992+
// Spannable does not fit into a single line and it may have a
993+
// non-rectangular shape.
994+
// Using bounds of the spannable object from the starting line to
995+
// guarantee that all
996+
// the coordinates within these bounds belong to the spannable
997+
endX =
998+
(int)
999+
mLayout.getPrimaryHorizontal(
1000+
mLayout.getLineEnd(startLine) - 1);
1001+
endY = mLayout.getLineBottom(startLine);
1002+
}
1003+
1004+
final int absoluteStartX = startX;
1005+
final int absoluteStartY = startY;
1006+
final int absoluteEndX = endX;
1007+
final int absoluteEndY = endY;
1008+
1009+
return new Rect(
1010+
absoluteStartX, absoluteStartY, absoluteEndX, absoluteEndY);
1011+
1012+
} catch (IndexOutOfBoundsException e) {
1013+
return new Rect(0, 0, 0, 0);
1014+
}
1015+
}
1016+
});
1017+
}
1018+
}
1019+
}
1020+
return result;
1021+
}
9441022
};
9451023
}
9461024

0 commit comments

Comments
 (0)