Custom Rules

Link to Custom Rules copied to clipboard

Supported within:
XML Compose

The custom rules feature lets you create new rules to test against and change key behaviors within existing rules.

note

Custom rules are supported in both XML and Compose Layouts. The implementation of a custom rule itself will vary from XML to Compose, but both will extend the AxeRuleViewHierarchy class. The below example shows the XML implementation.

Let's create a custom rule that checks for the name property of a view. The result criteria will be as follows:

  • Fail: the name is empty.
  • Success: the name has some length.

Creating the Rule

  1. Set up a new class object that inherits from AxeDevToolsRule for XML or AxeDevToolsComposeRule for Compose.

You can set up the rule configuration within the initializer to specify the impact, and what standard you're testing against, and add a summary.

XML:

class ExampleRule : AxeDevToolsRule(
    AxeStandard.WCAG_21,
    AxeImpact.MINOR.value,
    "Ensure AT and non-AT users have the same experience of understanding labels",
    false
) {
    
}

Compose:

class ComposeCustomCheckboxRule : AxeDevToolsComposeRule (
	AxeStandard.WCAG_20,
	AxeImpact.CRITICAL.value,
	“Checkbox should have a name attached to it.,
    true
) {

}
  1. You'll need to collect the view's properties to test. For our example, we need to access the name property.

XML:

override fun collectProps(
    axeDevToolsView: AxeDevToolsView, 
    axeProps: AxeProps
) {
    axeProps[AxeProps.Name.VISIBLE_TEXT] = axeDevToolsView.text
}

Compose:

override fun collectProps(
    axeDevToolsComposeView: AxeDevToolsComposeView, 
    axeProps: AxeProps
) {
    axeProps[ComposeProps.ROLE] = getRole(axeDevToolsComposeView)
    axeProps[ComposeProps.TEXT] = axeDevToolsComposeView.text
}
note

In the above Compose example, you may see error messages on axeProps[ComposeProps.ROLE]. To resolve the error, add the following import statements:

  • import com.deque.mobile.devtools.wrappers.get
  • import com.deque.mobile.devtools.wrappers.set

The getRole() is a helper function from our library to grab a role from the Compose view.

  1. Determine if the rule applies to the view by ensuring the visible text is not null.

XML:

override fun isApplicable(axeProps: AxeProps): Boolean {
    val visibleText = axeProps.get(AxeProps.Name.VISIBLE_TEXT, String::class.java)
    return visibleText != null && super.isApplicable(axeProps)
}

Compose:

override fun isApplicable(axeProps: AxeProps?): Boolean {
   return !(axeProps?.get(ComposeProps.VISIBLE_TEXT) as String).isNullOrBlank()
}
  1. Lastly, we'll specify in the runRule function what we're testing against to determine whether the rule has succeeded or failed.

XML:

override fun runRule(axeProps: AxeProps): String {
    if (super.runRule(axeProps).isNotEmpty()) {
        return super.runRule(axeProps)
    }

    val visibleText = axeProps.get(AxeProps.Name.VISIBLE_TEXT, String::class.java)

    return if (visibleText.isEmpty()) {
        AxeStatus.FAIL
    } else {
        AxeStatus.PASS
    }
}

Compose:

override fun runRule(axeProps: AxeProps?): String {
   if (axeProps == null) {
       return AxeStatus.INAPPLICABLE
   }

   val text = axeProps[ComposeProps.TEXT] as String

   return if (text.isEmpty() || text.isBlank()) {
        AxeStatus.FAIL
   } else {
        AxeStatus.PASS
   }
}

Add to axe DevTools

Once the custom rule is ready, add it to the rule list of the initialized axe DevTools object, AxeDevTools for XML or AxeDevToolsCompose for Compose. Set configurations before any tests run.

val axe = AxeDevTools() //or AxeDevToolsCompose()

init {
    axe.addCustomRule(CustomRule::class.java)
} 

Complete XML Custom Rule Example

class ExampleRule : AxeDevToolsRule(
    AxeStandard.WCAG_21,
    AxeImpact.MINOR.value,
    "Ensure AT and non-AT users have the same experience of understanding labels",
    false
) {
    override fun collectProps(axeDevToolsView: AxeDevToolsView, axeProps: AxeProps) {
        axeProps[AxeProps.Name.VISIBLE_TEXT] = axeDevToolsView.text
    }

    override fun isApplicable(axeProps: AxeProps): Boolean {
        val visibleText = axeProps.get(AxeProps.Name.VISIBLE_TEXT, String::class.java)
        return visibleText != null && super.isApplicable(axeProps)
    }

    override fun runRule(axeProps: AxeProps): String {
        if (super.runRule(axeProps).isNotEmpty()) {
            return super.runRule(axeProps)
        }
        val visibleText = axeProps.get(AxeProps.Name.VISIBLE_TEXT, String::class.java)
        return if (visibleText.isEmpty()) {
            AxeStatus.FAIL
        } else {
            AxeStatus.PASS
        }
    }
}

Complete Compose Custom Rule Example

import com.deque.axe.android.constants.AxeImpact
import com.deque.axe.android.constants.AxeStandard
import com.deque.axe.android.constants.AxeStatus
import com.deque.axe.android.wrappers.AxeProps
import com.deque.mobile.devtools.AxeDevToolsComposeRule
import com.deque.mobile.devtools.AxeDevToolsComposeView
import com.deque.mobile.devtools.wrappers.ComposeProps
import com.deque.mobile.devtools.wrappers.get
import com.deque.mobile.devtools.wrappers.set

class ComposeCheckbox : AxeDevToolsComposeRule(
    AxeStandard.WCAG_20,
    AxeImpact.CRITICAL.value,
    "Active Controls should not be nested together -- they need to be individually focusable by TalkBack.",
    true
)  {
    override fun collectProps(axeDevToolsComposeView: AxeDevToolsComposeView, axeProps: AxeProps) {
        axeProps[ComposeProps.ROLE] = getRole(axeDevToolsComposeView)
        axeProps[ComposeProps.TEXT] = axeDevToolsComposeView.text
    }

    override fun isApplicable(axeProps: AxeProps?): Boolean {
        return axeProps?.get(ComposeProps.ROLE) as String == "Checkbox"
    }

    override fun runRule(axeProps: AxeProps?): String {
        if (axeProps == null) {
            return AxeStatus.INAPPLICABLE
        }

        val text = axeProps[ComposeProps.TEXT] as String

        return if (text.isEmpty() || text.isBlank()) {
            AxeStatus.FAIL
        } else {
            AxeStatus.PASS
        }
    }
}