blob: 40d0074921f8b4d7c7ac964ac8bd4558aaba7f72 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.test
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.util.annotation.IntRange
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.toSize
internal expect fun printToLog(tag: String, message: String)
/**
* Prints all the semantics nodes information it holds into string.
*
* By default this also prints all the sub-hierarchy. This can be changed by setting a custom max
* depth in [maxDepth].
*
* Note that this will fetch the latest snapshot of nodes it sees in the hierarchy for the IDs it
* collected before. So the output can change over time if the tree changes.
*
* @param maxDepth Max depth of the nodes in hierarchy to print. Zero will print just this node.
*/
fun SemanticsNodeInteraction.printToString(
@IntRange(from = 0) maxDepth: Int = Int.MAX_VALUE
): String {
val result = fetchSemanticsNode()
return result.printToString(maxDepth)
}
/**
* Prints all the semantics nodes information into logs (as debug level).
*
* By default this also prints all the sub-hierarchy. This can be changed by setting a custom max
* depth in [maxDepth].
*
* Note that this will fetch the latest snapshot of nodes it sees in the hierarchy for the IDs it
* collected before. So the output can change over time if the tree changes.
*
* @param tag The tag to be used in the log messages.
* @param maxDepth Max depth of the nodes in hierarchy to print. Zero will print just this node.
*/
fun SemanticsNodeInteraction.printToLog(
tag: String,
@IntRange(from = 0) maxDepth: Int = Int.MAX_VALUE
) {
val result = "printToLog:\n" + printToString(maxDepth)
printToLog(tag, result)
}
/**
* Prints all the semantics nodes information it holds into string.
*
* By default this does not print nodes sub-hierarchies. This can be changed by setting a custom max
* depth in [maxDepth].
*
* Note that this will fetch the latest snapshot of nodes it sees in the hierarchy for the IDs it
* collected before. So the output can change over time if the tree changes.
*
* @param maxDepth Max depth of the nodes in hierarchy to print. Zero will print nodes in this
* collection only.
*/
fun SemanticsNodeInteractionCollection.printToString(
@IntRange(from = 0) maxDepth: Int = 0
): String {
val nodes = fetchSemanticsNodes()
return if (nodes.isEmpty()) {
"There were 0 nodes found!"
} else {
nodes.printToString(maxDepth)
}
}
/**
* Prints all the semantics nodes information into logs (as debug level).
*
* By default this does not print nodes sub-hierarchies. This can be changed by setting a custom max
* depth in [maxDepth].
*
* Note that this will fetch the latest snapshot of nodes it sees in the hierarchy for the IDs it
* collected before. So the output can change over time if the tree changes.
*
* @param tag The tag to be used in the log messages.
* @param maxDepth Max depth of the nodes in hierarchy to print. Zero will print nodes in this
* collection only.
*/
fun SemanticsNodeInteractionCollection.printToLog(
tag: String,
@IntRange(from = 0) maxDepth: Int = 0
) {
val result = "printToLog:\n" + printToString(maxDepth)
printToLog(tag, result)
}
internal fun Collection<SemanticsNode>.printToString(maxDepth: Int = 0): String {
var sb = StringBuilder()
var i = 1
forEach {
if (size > 1) {
sb.append(i)
sb.append(") ")
}
sb.append(it.printToString(maxDepth))
if (i < size) {
sb.appendLine()
}
++i
}
return sb.toString()
}
internal fun SemanticsNode.printToString(maxDepth: Int = 0): String {
val sb = StringBuilder()
printToStringInner(
sb = sb,
maxDepth = maxDepth,
nestingLevel = 0,
nestingIndent = "",
isFollowedBySibling = false
)
return sb.toString()
}
private fun SemanticsNode.printToStringInner(
sb: StringBuilder,
maxDepth: Int,
nestingLevel: Int,
nestingIndent: String,
isFollowedBySibling: Boolean
) {
val newIndent = if (nestingLevel == 0) {
""
} else if (isFollowedBySibling) {
"$nestingIndent | "
} else {
"$nestingIndent "
}
if (nestingLevel > 0) {
sb.append("$nestingIndent |-")
}
sb.append("Node #$id at ")
sb.append(rectToShortString(unclippedGlobalBounds))
if (config.contains(SemanticsProperties.TestTag)) {
sb.append(", Tag: '")
sb.append(config[SemanticsProperties.TestTag])
sb.append("'")
}
val maxLevelReached = nestingLevel == maxDepth
sb.appendConfigInfo(config, newIndent)
if (maxLevelReached) {
val childrenCount = children.size
val siblingsCount = (parent?.children?.size ?: 1) - 1
if (childrenCount > 0 || (siblingsCount > 0 && nestingLevel == 0)) {
sb.appendLine()
sb.append(newIndent)
sb.append("Has ")
if (childrenCount > 1) {
sb.append("$childrenCount children")
} else if (childrenCount == 1) {
sb.append("$childrenCount child")
}
if (siblingsCount > 0 && nestingLevel == 0) {
if (childrenCount > 0) {
sb.append(", ")
}
if (siblingsCount > 1) {
sb.append("$siblingsCount siblings")
} else {
sb.append("$siblingsCount sibling")
}
}
}
return
}
val childrenLevel = nestingLevel + 1
val children = this.children.toList()
children.forEachIndexed { index, child ->
val hasSibling = index < children.size - 1
sb.appendLine()
child.printToStringInner(sb, maxDepth, childrenLevel, newIndent, hasSibling)
}
}
private val SemanticsNode.unclippedGlobalBounds: Rect
get() {
return Rect(globalPosition, size.toSize())
}
private fun rectToShortString(rect: Rect): String {
return "(${rect.left}, ${rect.top}, ${rect.right}, ${rect.bottom})px"
}
private fun StringBuilder.appendConfigInfo(config: SemanticsConfiguration, indent: String = "") {
for ((key, value) in config) {
if (key == SemanticsProperties.TestTag) {
continue
}
appendLine()
append(indent)
append(key.name)
append(" = '")
if (value is AnnotatedString) {
if (value.paragraphStyles.isEmpty() && value.spanStyles.isEmpty() && value
.getStringAnnotations(0, value.text.length).isEmpty()) {
append(value.text)
} else {
// Save space if we there is text only in the object
append(value)
}
} else {
append(value)
}
append("'")
}
if (config.isMergingSemanticsOfDescendants) {
appendLine()
append(indent)
append("MergeDescendants = 'true'")
}
}