diff --git a/README.md b/README.md index 331d11b..e1ecc62 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,6 @@ See [RULES.md](RULES.md) for complete behavior, fixes, WCAG references, and exam | Rule ID | Name | Severity | Details | | --- | --- | --- | --- | -| `touch-target-size` | Touch Target Size | Error | [RULES.md](RULES.md#touch-target-size---touch-target-size) | | `missing-content-description` | Missing Content Description | Error | [RULES.md](RULES.md#missing-content-description---missing-content-description) | | `duplicate-content-description` | Duplicate Content Description | Warning | [RULES.md](RULES.md#duplicate-content-description---duplicate-content-description) | | `focus-order` | Focus Order | Error | [RULES.md](RULES.md#focus-order---focus-order) | diff --git a/RULES.md b/RULES.md index 251bba9..7882440 100644 --- a/RULES.md +++ b/RULES.md @@ -2,43 +2,18 @@ Compose A11y Scanner ships the built-in rules registered by `ScannerRules.allRuleIds()`. -Implementation note: the current codebase exposes 7 built-in rules, not 8. This document covers every implemented built-in rule in `scanner-rules`. +Implementation note: the current codebase exposes 6 built-in rules. This document covers every implemented built-in rule in `scanner-rules`. ## Summary | Rule ID | Name | Severity | What It Checks | How To Fix | WCAG Reference | | --- | --- | --- | --- | --- | --- | -| `touch-target-size` | Touch Target Size | Error | Clickable/touch target nodes whose measured width or height is smaller than the configured minimum, 48dp by default. | Use `Modifier.minimumInteractiveComponentSize()` or add padding so the interactive area is at least the configured minimum in both dimensions. | WCAG 2.5.5 Target Size (Level AA) | | `missing-content-description` | Missing Content Description | Error | Interactive nodes and image-like nodes that do not expose a non-empty content description. | Add a meaningful `contentDescription` through semantics, or pass one directly to image composables that support it. | WCAG 1.1.1 Non-text Content (Level A) | | `duplicate-content-description` | Duplicate Content Description | Warning | Non-merged nodes at the same semantics depth that reuse the same non-empty content description. | Give each control or item a label that identifies its specific action, state, or content. | WCAG 2.4.6 Headings and Labels (Level AA) | | `focus-order` | Focus Order | Error | Focusable nodes whose semantics traversal jumps upward compared with the previous focusable node's visual position. | Reorder composables so focus follows the visual reading order, or set explicit traversal order with semantics. | WCAG 2.4.3 Focus Order (Level A) | | `text-scaling` | Text Scaling | Warning | Text nodes that may overflow or clip inside their parent when simulated at a larger font scale. | Avoid fixed-height containers for text; use flexible height, wrapping, or scrolling so scaled text can reflow. | WCAG 1.4.4 Resize Text (Level AA) | | `image-text-overlay` | Image With Text Overlay | Warning | Text nodes that significantly overlap image nodes, creating a contrast risk across dynamic images. | Add a scrim or solid text background, or otherwise guarantee sufficient contrast for every image state. | WCAG 1.4.3 Contrast Minimum (Level AA) | -| `clickable-role` | Clickable Role | Error | Clickable/touch target nodes that do not expose a semantic role, and clickable image roles without a content description. | Add the appropriate role, such as `Role.Button`, `Role.Checkbox`, or `Role.Image`; provide labels for clickable images. | WCAG 4.1.2 Name, Role, Value (Level A) | - -## `touch-target-size` - Touch Target Size - -**Severity:** Error - -**What it checks:** This rule evaluates clickable/touch target nodes that are not merged descendants. It reports a node when either its measured width or height is smaller than `ScannerConfig.minTouchTargetDp`, which defaults to 48dp. - -**How to fix:** Ensure the interactive area reaches the minimum size in both dimensions. Prefer Material's `Modifier.minimumInteractiveComponentSize()` for Material controls, or add padding around custom controls. - -**WCAG reference:** WCAG 2.5.5 Target Size (Level AA) - -**Code example:** - -```kotlin -IconButton( - onClick = onClose, - modifier = Modifier.minimumInteractiveComponentSize(), -) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Close", - ) -} -``` +| `clickable-role` | Clickable Role | Error | Clickable nodes with role-specific semantic requirements, currently clickable images without a content description. | Provide a meaningful label for clickable images. A generic clickable does not require a role when no predefined Compose role accurately applies. | WCAG 4.1.2 Name, Role, Value (Level A) | ## `missing-content-description` - Missing Content Description @@ -196,9 +171,9 @@ Box { **Severity:** Error -**What it checks:** This rule reports clickable/touch target nodes that are not merged descendants and do not expose a semantic role. It also reports clickable image roles when the content description is empty. +**What it checks:** This rule reports clickable image nodes when their content description is empty. A generic clickable node is not reported merely because its role is null: Compose permits an unspecified role when none of its predefined roles accurately describes the element, such as a clickable list row. -**How to fix:** Use Material components when possible because they usually provide roles automatically. For custom click targets, set the appropriate role through semantics or use clickable APIs that expose the role. +**How to fix:** Provide a meaningful, non-empty content description for a clickable image. For controls with a well-defined role, use the corresponding semantic API—for example, `selectable(role = Role.Tab, ...)` for a custom tab—but do not assign `Role.Button` to generic click targets solely to satisfy this rule. **WCAG reference:** WCAG 4.1.2 Name, Role, Value (Level A) @@ -211,8 +186,7 @@ Box( role = Role.Button contentDescription = "Retry loading" } - .clickable(onClick = onRetry) - .minimumInteractiveComponentSize(), + .clickable(onClick = onRetry), ) { Text("Retry") } diff --git a/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt b/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt index dd43e96..959ee96 100644 --- a/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt +++ b/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt @@ -3,7 +3,6 @@ package com.composea11yscanner.sample import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot -import androidx.compose.ui.unit.Density import com.composea11yscanner.core.model.ScannerConfig import com.composea11yscanner.rules.ScannerRules import com.composea11yscanner.sample.ui.theme.ScannerTheme @@ -50,7 +49,6 @@ class FixedScreenAccessibilityTest { val config = ScannerConfig(enabledRules = ScannerRules.allRuleIds().toSet()) val scanner = A11yScanner( rules = ScannerRules.buildRules(config, screenDensity = 1f), - density = Density(1f), ) val result = scanner.scan(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode()) diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 757cbe8..fce926b 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -10,9 +10,6 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ComposeA11yScanner"> - diff --git a/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt b/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt index 3a0ef1b..09ab23e 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt @@ -47,7 +47,7 @@ fun BrokenLoginScreen(onViewFixed: (() -> Unit)? = null) { .padding(20.dp), verticalArrangement = Arrangement.spacedBy(18.dp), ) { - SecureBankHeader() + SecureBankHeader(isFixed = false) LoginIntro() Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { @@ -135,7 +135,7 @@ fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { .padding(20.dp), verticalArrangement = Arrangement.spacedBy(18.dp), ) { - SecureBankHeader() + SecureBankHeader(isFixed = true) LoginIntro() Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { @@ -239,13 +239,22 @@ fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { } @Composable -private fun SecureBankHeader() { +private fun SecureBankHeader(isFixed: Boolean) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - BankLogoMark() + BankLogoMark( + modifier = if (isFixed) { + Modifier + .semantics { contentDescription = "Open SecureBank home" } + .clickable(role = Role.Image, onClick = {}) + } else { + // Intentionally broken: an actionable image needs an accessible description. + Modifier.clickable(role = Role.Image, onClick = {}) + }, + ) Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Text( text = "SecureBank", diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt index 4252950..35dd3e3 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt @@ -56,9 +56,9 @@ fun BrokenScreenCard( } @Composable -fun BankLogoMark() { +fun BankLogoMark(modifier: Modifier = Modifier) { Box( - modifier = Modifier + modifier = modifier .size(48.dp) .clip(RoundedCornerShape(14.dp)) .background(Color(0xFF6C63FF)), diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt index f68eb59..aa0a7d1 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull -import androidx.compose.ui.unit.Density import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.Rect import com.composea11yscanner.ui.A11yNodeExtractor @@ -33,7 +32,7 @@ internal fun ComponentActivity.extractBrokenSampleNodes(): List = ?: sampleRoot.boundsInRoot.let { Rect(it.left.roundToInt(), it.top.roundToInt(), it.right.roundToInt(), it.bottom.roundToInt()) } - A11yNodeExtractor(Density(this)) + A11yNodeExtractor() .extract(sampleRoot) .filterVisibleIn(viewport) }.getOrDefault(emptyList()) diff --git a/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt b/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt index 89a8d23..899b9ef 100644 --- a/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt +++ b/scanner-core/src/main/java/com/composea11yscanner/core/model/A11yNode.kt @@ -8,7 +8,6 @@ package com.composea11yscanner.core.model * @property bounds Pixel bounds relative to the scanned root. * @property contentDescription Accessible label exposed by the node, if any. * @property isTouchTarget True when the node exposes a click action. - * @property touchTargetSize Size used by touch target rules, expressed in dp. * @property textColor Foreground text color when it can be extracted. * @property backgroundColors Candidate background colors sampled behind the node. * @property isFocusable True when the node can participate in focus traversal. @@ -22,7 +21,6 @@ data class A11yNode( val bounds: Rect, val contentDescription: String?, val isTouchTarget: Boolean, - val touchTargetSize: DpSize, val textColor: Color?, val backgroundColors: List, val isFocusable: Boolean, diff --git a/scanner-core/src/main/java/com/composea11yscanner/core/model/DpSize.kt b/scanner-core/src/main/java/com/composea11yscanner/core/model/DpSize.kt deleted file mode 100644 index 2602234..0000000 --- a/scanner-core/src/main/java/com/composea11yscanner/core/model/DpSize.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.composea11yscanner.core.model - -/** - * Density-independent size in dp units. Mirrors the Compose DpSize API - * without pulling in the Compose dependency. - * - * @property width Width in dp. - * @property height Height in dp. - */ -data class DpSize(val width: Float, val height: Float) { - companion object { - /** A zero-width and zero-height size. */ - val Zero = DpSize(0f, 0f) - - /** Sentinel used when a size could not be measured. */ - val Unspecified = DpSize(Float.NaN, Float.NaN) - } -} diff --git a/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt b/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt index 1f8bd51..be109d6 100644 --- a/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt +++ b/scanner-core/src/main/java/com/composea11yscanner/core/model/ScannerConfig.kt @@ -4,14 +4,12 @@ package com.composea11yscanner.core.model * Runtime configuration used by the scanner engine and UI integration. * * @property enabledRules Rule ids that should be evaluated. - * @property minTouchTargetDp Minimum touch target size in dp. * @property minContrastRatio Minimum text contrast ratio used by contrast-related rules. * @property debugOverlay Whether scanner UI should display issue overlays. * @property autoScan Whether scanning should start automatically when the scanner attaches. */ data class ScannerConfig( val enabledRules: Set, - val minTouchTargetDp: Int = 48, val minContrastRatio: Float = 4.5f, val debugOverlay: Boolean = true, val autoScan: Boolean = true, diff --git a/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt b/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt index 6c0288a..db95426 100644 --- a/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt +++ b/scanner-core/src/test/java/com/composea11yscanner/core/A11yScanEngineTest.kt @@ -4,7 +4,6 @@ import app.cash.turbine.test import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScannerConfig import com.composea11yscanner.core.model.ScannerState @@ -29,7 +28,6 @@ class A11yScanEngineTest { bounds = Rect(0, 0, 100, 100), contentDescription = null, isTouchTarget = false, - touchTargetSize = DpSize(48f, 48f), textColor = null, backgroundColors = emptyList(), isFocusable = false, diff --git a/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt b/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt index 25cafce..5ea3e40 100644 --- a/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt +++ b/scanner-core/src/test/java/com/composea11yscanner/core/CoreModelAndRuleTest.kt @@ -4,7 +4,6 @@ import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11yRole import com.composea11yscanner.core.model.A11ySeverity import com.composea11yscanner.core.model.Color -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult import com.composea11yscanner.core.rule.BaseA11yRule @@ -24,7 +23,6 @@ class CoreModelAndRuleTest { bounds = Rect(0, 0, 100, 48), contentDescription = "Submit", isTouchTarget = true, - touchTargetSize = DpSize(100f, 48f), textColor = Color(0xFF000000), backgroundColors = emptyList(), isFocusable = true, diff --git a/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt b/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt index 1d6c515..48edefb 100644 --- a/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt +++ b/scanner-rules/src/main/java/com/composea11yscanner/rules/ClickableRoleRule.kt @@ -6,7 +6,7 @@ import com.composea11yscanner.core.model.A11yRole import com.composea11yscanner.core.model.A11ySeverity import com.composea11yscanner.core.rule.BaseA11yRule -/** Flags clickable nodes that do not expose an accessibility role. */ +/** Flags clickable images that do not expose an accessible description. */ class ClickableRoleRule : BaseA11yRule() { /** Stable id for the clickable role rule. */ @@ -15,30 +15,26 @@ class ClickableRoleRule : BaseA11yRule() { /** Human-readable rule name. */ override val ruleName = "Clickable Role" - /** Severity assigned to missing roles. */ + /** Severity assigned to invalid clickable-role semantics. */ override val severity = A11ySeverity.Error /** WCAG criterion associated with name, role, and value. */ override val wcagReference = "WCAG 4.1.2 Name, Role, Value (Level A)" - /** Evaluates a single node for role metadata. */ + /** Evaluates role-specific requirements for a clickable node. */ override fun check(node: A11yNode): A11yIssue? { if (node.isMergedDescendant) return null if (!node.isTouchTarget) return null - val missingRole = node.role == null - val imageWithoutDescription = - node.role == A11yRole.Image && node.contentDescription.isNullOrBlank() - - if (!missingRole && !imageWithoutDescription) return null + // Compose intentionally permits a null role when none of its predefined roles accurately + // describes a custom click target (for example, a clickable list row). Requiring a role for + // every OnClick node encourages misleading Role.Button semantics and creates false positives. + if (node.role != A11yRole.Image || !node.contentDescription.isNullOrBlank()) return null return issue( node = node, - message = "Clickable element has no semantic role. " + - "Add Modifier.semantics { role = Role.Button }", - howToFix = "Apply the appropriate role: Modifier.semantics { role = Role.Button } " + - "for buttons, Role.Checkbox for toggles, Role.Image for images. " + - "Clickable images also require a non-empty contentDescription.", + message = "Clickable image has no accessible description.", + howToFix = "Provide a meaningful, non-empty contentDescription for the clickable image.", ) } } diff --git a/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt b/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt index ffb4250..1acd8a1 100644 --- a/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt +++ b/scanner-rules/src/main/java/com/composea11yscanner/rules/ScannerRules.kt @@ -10,7 +10,6 @@ object ScannerRules { /** Returns every built-in rule id understood by [buildRules]. */ fun allRuleIds(): List = listOf( - "touch-target-size", "missing-content-description", "duplicate-content-description", "focus-order", @@ -27,8 +26,6 @@ object ScannerRules { * @return Rule instances that should run for the provided configuration. */ fun buildRules(config: ScannerConfig, screenDensity: Float): List = buildList { - if ("touch-target-size" in config.enabledRules) - add(TouchTargetRule(config.minTouchTargetDp)) if ("missing-content-description" in config.enabledRules) add(MissingContentDescriptionRule()) if ("duplicate-content-description" in config.enabledRules) diff --git a/scanner-rules/src/main/java/com/composea11yscanner/rules/TouchTargetRule.kt b/scanner-rules/src/main/java/com/composea11yscanner/rules/TouchTargetRule.kt deleted file mode 100644 index 254f814..0000000 --- a/scanner-rules/src/main/java/com/composea11yscanner/rules/TouchTargetRule.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.composea11yscanner.rules - -import com.composea11yscanner.core.model.A11yIssue -import com.composea11yscanner.core.model.A11yNode -import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.rule.BaseA11yRule - -/** - * Flags clickable nodes whose measured touch target is smaller than the configured minimum. - * - * @param minTouchTargetDp Minimum accepted width and height in dp. - */ -class TouchTargetRule( - private val minTouchTargetDp: Int = 48, -) : BaseA11yRule() { - /** Stable id for the touch target size rule. */ - override val ruleId = "touch-target-size" - - /** Human-readable rule name. */ - override val ruleName = "Touch Target Size" - - /** Severity assigned to undersized touch targets. */ - override val severity = A11ySeverity.Error - - /** WCAG criterion associated with target size. */ - override val wcagReference = "WCAG 2.5.5 Target Size (Level AA)" - - /** Evaluates a single node for touch target dimensions. */ - override fun check(node: A11yNode): A11yIssue? { - if (node.isMergedDescendant) return null - if (!node.isTouchTarget) return null - - val w = node.touchTargetSize.width - val h = node.touchTargetSize.height - // Pixel bounds converted back to dp can land infinitesimally below an exact dp value - // (for example, a 48 dp target may be reported as 47.999996 dp). - if ( - w + MeasurementToleranceDp >= minTouchTargetDp && - h + MeasurementToleranceDp >= minTouchTargetDp - ) return null - - return issue( - node = node, - message = "Touch target is ${"%.0f".format(w)}x${"%.0f".format(h)}dp. " + - "Minimum required is ${minTouchTargetDp}x${minTouchTargetDp}dp.", - howToFix = "Apply Modifier.minimumInteractiveComponentSize() or add padding so the " + - "composable reaches at least ${minTouchTargetDp}dp in both dimensions.", - ) - } - - private companion object { - const val MeasurementToleranceDp = 0.01f - } -} diff --git a/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt b/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt index 9d5bfcc..5403561 100644 --- a/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt +++ b/scanner-rules/src/test/java/com/composea11yscanner/rules/ClickableRoleRuleTest.kt @@ -50,8 +50,8 @@ class ClickableRoleRuleTest { // --- failing cases --- @Test - fun `touch target with null role fails`() { - assertNotNull(rule.evaluate(createNode(isTouchTarget = true, role = null))) + fun `touch target with null role passes when no predefined role applies`() { + assertNull(rule.evaluate(createNode(isTouchTarget = true, role = null))) } @Test @@ -90,13 +90,16 @@ class ClickableRoleRuleTest { createNode(isTouchTarget = false), createNode(isTouchTarget = true, role = A11yRole.Button), createNode(isTouchTarget = true, role = null), + createNode(isTouchTarget = true, role = A11yRole.Image, contentDescription = null), ) assertEquals(1, rule.evaluateAll(nodes).size) } @Test fun `issue carries correct rule metadata`() { - val issue = rule.evaluate(createNode(isTouchTarget = true, role = null))!! + val issue = rule.evaluate( + createNode(isTouchTarget = true, role = A11yRole.Image, contentDescription = null) + )!! assertEquals("clickable-role", issue.ruleId) assertEquals(A11ySeverity.Error, issue.severity) assertEquals("WCAG 4.1.2 Name, Role, Value (Level A)", issue.wcagReference) diff --git a/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt b/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt index 2b2bc40..227d4d5 100644 --- a/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt +++ b/scanner-rules/src/test/java/com/composea11yscanner/rules/FakeNodeBuilder.kt @@ -3,7 +3,6 @@ package com.composea11yscanner.rules import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11yRole import com.composea11yscanner.core.model.Color -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import java.util.concurrent.atomic.AtomicInteger @@ -14,7 +13,6 @@ fun createNode( bounds: Rect = Rect(0, 0, 100, 100), contentDescription: String? = null, isTouchTarget: Boolean = false, - touchTargetSize: DpSize = DpSize(48f, 48f), textColor: Color? = null, backgroundColors: List = emptyList(), isFocusable: Boolean = false, @@ -28,7 +26,6 @@ fun createNode( bounds = bounds, contentDescription = contentDescription, isTouchTarget = isTouchTarget, - touchTargetSize = touchTargetSize, textColor = textColor, backgroundColors = backgroundColors, isFocusable = isFocusable, diff --git a/scanner-rules/src/test/java/com/composea11yscanner/rules/TouchTargetRuleTest.kt b/scanner-rules/src/test/java/com/composea11yscanner/rules/TouchTargetRuleTest.kt deleted file mode 100644 index bde8244..0000000 --- a/scanner-rules/src/test/java/com/composea11yscanner/rules/TouchTargetRuleTest.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.composea11yscanner.rules - -import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test - -class TouchTargetRuleTest { - - private val rule = TouchTargetRule() - - // --- passing cases --- - - @Test - fun `non-interactive node is not evaluated`() { - assertNull(rule.evaluate(createNode(isTouchTarget = false, touchTargetSize = DpSize(40f, 40f)))) - } - - @Test - fun `touch target at exactly minimum size passes`() { - assertNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(48f, 48f)))) - } - - @Test - fun `floating point noise just below minimum size passes`() { - assertNull( - rule.evaluate( - createNode( - isTouchTarget = true, - touchTargetSize = DpSize(47.999996f, 47.999996f), - ) - ) - ) - } - - @Test - fun `touch target larger than minimum passes`() { - assertNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(56f, 64f)))) - } - - @Test - fun `merged descendant touch target is skipped`() { - assertNull( - rule.evaluate( - createNode( - isTouchTarget = true, - touchTargetSize = DpSize(32f, 32f), - isMergedDescendant = true, - ) - ) - ) - } - - // --- failing cases --- - - @Test - fun `width below minimum fails`() { - assertNotNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(44f, 48f)))) - } - - @Test - fun `meaningfully undersized target is not hidden by measurement tolerance`() { - assertNotNull( - rule.evaluate( - createNode( - isTouchTarget = true, - touchTargetSize = DpSize(47.98f, 48f), - ) - ) - ) - } - - @Test - fun `height below minimum fails`() { - assertNotNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(48f, 44f)))) - } - - @Test - fun `both dimensions below minimum fail`() { - assertNotNull(rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(32f, 32f)))) - } - - // --- edge cases --- - - @Test - fun `zero-size touch target fails with correct dimensions in message`() { - val issue = rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(0f, 0f))) - assertNotNull(issue) - assertTrue(issue!!.message.contains("0x0dp")) - } - - @Test - fun `message contains actual dimensions`() { - val issue = rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(32f, 40f))) - assertNotNull(issue) - assertTrue(issue!!.message.contains("32x40dp")) - } - - @Test - fun `custom threshold accepts larger node`() { - val rule36 = TouchTargetRule(minTouchTargetDp = 36) - assertNull(rule36.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(40f, 40f)))) - } - - @Test - fun `custom threshold rejects node below it`() { - val rule36 = TouchTargetRule(minTouchTargetDp = 36) - assertNotNull(rule36.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(30f, 30f)))) - } - - @Test - fun `issue carries correct rule metadata`() { - val issue = rule.evaluate(createNode(isTouchTarget = true, touchTargetSize = DpSize(40f, 40f)))!! - assertEquals("touch-target-size", issue.ruleId) - assertEquals(A11ySeverity.Error, issue.severity) - assertEquals("WCAG 2.5.5 Target Size (Level AA)", issue.wcagReference) - } - - @Test - fun `evaluateAll aggregates per-node results`() { - val nodes = listOf( - createNode(isTouchTarget = false), - createNode(isTouchTarget = true, touchTargetSize = DpSize(40f, 40f)), - createNode(isTouchTarget = true, touchTargetSize = DpSize(48f, 48f)), - ) - assertEquals(1, rule.evaluateAll(nodes).size) - } -} diff --git a/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt b/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt index 452c7bf..88b41bc 100644 --- a/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt +++ b/scanner-ui/src/androidTest/java/com/composea11yscanner/ui/A11yNodeExtractorTest.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -30,7 +29,7 @@ class A11yNodeExtractorTest { composeRule.waitForIdle() - val nodes = A11yNodeExtractor(Density(1f)) + val nodes = A11yNodeExtractor() .extract(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode()) val clickableNode = nodes.single { it.isTouchTarget && it.contentDescription == "Go" } @@ -45,7 +44,7 @@ class A11yNodeExtractorTest { composeRule.waitForIdle() - val nodes = A11yNodeExtractor(Density(1f)) + val nodes = A11yNodeExtractor() .extract(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode()) val clickableNode = nodes.single { it.isTouchTarget } @@ -53,25 +52,4 @@ class A11yNodeExtractorTest { assertTrue(clickableNode.contentDescription.isNullOrBlank()) } - @Test - fun compactClickable_usesExpandedTouchBounds() { - lateinit var density: Density - composeRule.setContent { - density = LocalDensity.current - Box( - modifier = Modifier - .size(width = 40.dp, height = 48.dp) - .clickable { }, - ) - } - - composeRule.waitForIdle() - - val nodes = A11yNodeExtractor(density) - .extract(composeRule.onRoot(useUnmergedTree = true).fetchSemanticsNode()) - val clickableNode = nodes.single { it.isTouchTarget } - - assertTrue(clickableNode.touchTargetSize.width >= 48f) - assertTrue(clickableNode.touchTargetSize.height >= 48f) - } } diff --git a/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt b/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt index 419e21f..d50ace8 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/A11yScannerInitializer.kt @@ -15,7 +15,6 @@ import com.composea11yscanner.rules.ScannerRules * AndroidX App Startup initializer for installing [ComposeA11yScanner] in debug builds. * * Configure the scanner from the app manifest with: - * - `a11y_scanner_min_touch_target` * - `a11y_scanner_min_contrast` * - `a11y_scanner_auto_scan` */ @@ -68,10 +67,6 @@ class A11yScannerInitializer : Initializer { val defaults = ScannerConfig(enabledRules = emptySet()) return ScannerConfig( enabledRules = ScannerRules.allRuleIds().toSet(), - minTouchTargetDp = metadata.intValue( - key = META_MIN_TOUCH_TARGET, - defaultValue = defaults.minTouchTargetDp, - ), minContrastRatio = metadata.floatValue( key = META_MIN_CONTRAST, defaultValue = defaults.minContrastRatio, @@ -88,14 +83,6 @@ class A11yScannerInitializer : Initializer { return appInfo.metaData ?: Bundle.EMPTY } - private fun Bundle.intValue(key: String, defaultValue: Int): Int = - when (val value = get(key)) { - is Int -> value - is Number -> value.toInt() - is String -> value.toIntOrNull() ?: defaultValue - else -> defaultValue - } - private fun Bundle.floatValue(key: String, defaultValue: Float): Float = when (val value = get(key)) { is Float -> value @@ -112,7 +99,6 @@ class A11yScannerInitializer : Initializer { } private companion object { - const val META_MIN_TOUCH_TARGET = "a11y_scanner_min_touch_target" const val META_MIN_CONTRAST = "a11y_scanner_min_contrast" const val META_AUTO_SCAN = "a11y_scanner_auto_scan" } diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt b/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt index d4cd275..680a691 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ComposeA11yScanner.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.unit.Density import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.composea11yscanner.core.model.A11yIssue @@ -204,7 +203,7 @@ object ComposeA11yScanner { ?.findFirstAbstractComposeView(excludeView = overlayView) ?: return emptyList() val semanticsOwner = hostView.findSemanticsOwner() ?: return emptyList() - return A11yNodeExtractor(Density(activity)).extract(semanticsOwner) + return A11yNodeExtractor().extract(semanticsOwner) } private fun AbstractComposeView.findSemanticsOwner(): SemanticsOwner? { diff --git a/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt b/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt index 191025b..a546a55 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/export/ScanResultExporter.kt @@ -3,7 +3,6 @@ package com.composea11yscanner.export import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity import com.composea11yscanner.core.model.Color -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult import java.util.Locale @@ -100,8 +99,6 @@ object ScanResultExporter { append(", ") appendJsonPair("isTouchTarget", isTouchTarget) append(", ") - appendJsonPair("touchTargetSize", touchTargetSize) - append(", ") appendJsonPair("textColor", textColor) append(", ") append("\"backgroundColors\": [") @@ -181,13 +178,6 @@ object ScanResultExporter { append(value.toJson()) } - private fun StringBuilder.appendJsonPair(name: String, value: DpSize) { - append("\"") - append(name.escapeJson()) - append("\": ") - append(value.toJson()) - } - private fun StringBuilder.appendJsonPair(name: String, value: Color?) { append("\"") append(name.escapeJson()) @@ -198,15 +188,9 @@ object ScanResultExporter { private fun Rect.toJson(): String = """{"left": $left, "top": $top, "right": $right, "bottom": $bottom, "width": $width, "height": $height}""" - private fun DpSize.toJson(): String = - """{"width": ${width.toJsonNumber()}, "height": ${height.toJsonNumber()}}""" - private fun Color.toJson(): String = """{"value": $value}""" - private fun Float.toJsonNumber(): String = - if (isFinite()) toString() else "null" - private fun Float.formatPercent(): String = String.format(Locale.US, "%.1f%%", this) diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt index 405b748..3e7c956 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yNodeExtractor.kt @@ -7,10 +7,8 @@ import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull -import androidx.compose.ui.unit.Density import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11yRole -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import kotlin.math.roundToInt @@ -21,9 +19,8 @@ import kotlin.math.roundToInt * - Test: `composeTestRule.onRoot(useUnmergedTree = true).fetchSemanticsNode()` * - Production: `SemanticsOwner.rootSemanticsNode` via [extract(SemanticsOwner)] * - * @param density Density used to convert pixel bounds into dp sizes. */ -class A11yNodeExtractor(private val density: Density) { +class A11yNodeExtractor { /** * Recursively extracts all nodes from the tree rooted at [rootNode]. @@ -76,10 +73,6 @@ class A11yNodeExtractor(private val density: Density) { null } val visualBounds = boundsInRoot - // Compose expands pointer input for small clickables to the platform minimum touch - // target without changing their visual/layout bounds. Measure the region that actually - // accepts input so compact controls such as a short FilterChip are not false positives. - val touchTargetBounds = touchBoundsInRoot val bounds = visualBounds.toCoreRect() return A11yNode( @@ -95,7 +88,6 @@ class A11yNodeExtractor(private val density: Density) { ?.joinToString(separator = ", ") ?: textLabel, isTouchTarget = isTouchTarget, - touchTargetSize = touchTargetBounds.toDpSize(), textColor = null, // not available via semantics backgroundColors = emptyList(), // not available via semantics isFocusable = config.contains(SemanticsActions.OnClick) @@ -164,12 +156,6 @@ class A11yNodeExtractor(private val density: Density) { bottom = bottom.roundToInt(), ) - private fun androidx.compose.ui.geometry.Rect.toDpSize(): DpSize = with(density) { - DpSize( - width = width.toDp().value, - height = height.toDp().value, - ) - } } // --- Role mapping --- diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt index 5e5dea0..1e4a7a9 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScanner.kt @@ -3,7 +3,6 @@ package com.composea11yscanner.ui import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.unit.Density import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.ScanResult import com.composea11yscanner.core.rule.A11yRule @@ -14,11 +13,9 @@ import java.util.UUID * * @param rules The rules to run. Build from [com.composea11yscanner.rules.ScannerRules.buildRules] * or supply a custom list. - * @param density Compose [Density] used by [A11yNodeExtractor] for pixel↔dp conversion. */ class A11yScanner( private val rules: List, - private val density: Density, ) { /** * Scans from a semantics node, typically in tests or manual tree walks. @@ -27,7 +24,7 @@ class A11yScanner( * @return Completed scan result. */ fun scan(rootNode: SemanticsNode): ScanResult = - buildResult(A11yNodeExtractor(density).extract(rootNode)) + buildResult(A11yNodeExtractor().extract(rootNode)) /** * Scans the live Compose semantics tree via [SemanticsOwner]. @@ -37,7 +34,7 @@ class A11yScanner( */ @OptIn(InternalComposeUiApi::class) fun scan(owner: SemanticsOwner): ScanResult = - buildResult(A11yNodeExtractor(density).extract(owner)) + buildResult(A11yNodeExtractor().extract(owner)) private fun buildResult(nodes: List): ScanResult { val issues = rules diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt index 4f74b7f..6444c5e 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerController.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext * Typical usage: * ``` * val controller = A11yScannerController( - * nodeProvider = { A11yNodeExtractor(density).extract(semanticsOwner) }, + * nodeProvider = { A11yNodeExtractor().extract(semanticsOwner) }, * screenDensity = resources.displayMetrics.density, * ) * .configure(ScannerConfig(enabledRules = ScannerRules.allRuleIds().toSet())) diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt index 6544080..0c49a90 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/A11yScannerScaffold.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult import com.composea11yscanner.core.model.ScannerConfig @@ -194,7 +193,6 @@ private fun previewScanResult() = ScanResult( nodeId = "node-1", composableName = "Button", bounds = Rect(0, 0, 300, 120), contentDescription = null, isTouchTarget = true, - touchTargetSize = DpSize(100f, 40f), textColor = null, backgroundColors = emptyList(), isFocusable = true, isMergedDescendant = false, depth = 1, ), diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt index 78d54ad..daf76ea 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueDetailPanel.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import java.util.Locale @@ -396,13 +395,6 @@ private fun IssueDetailPanelMultipleIssuesPreview() { howToFix = "Apply Modifier.clickable(role = Role.Button) for action controls.", wcagReference = "WCAG 4.1.2 Name, Role, Value (Level A)", ), - previewIssue( - severity = A11ySeverity.Error, - ruleName = "Touch Target Size", - message = "Touch target is 28x28dp. Minimum required is 48x48dp.", - howToFix = "Apply Modifier.minimumInteractiveComponentSize() or add padding.", - wcagReference = "WCAG 2.5.5 Target Size (Level AA)", - ), previewIssue( severity = A11ySeverity.Warning, ruleName = "Missing Content Description", @@ -433,7 +425,6 @@ private fun previewIssue( bounds = Rect(0, 0, 300, 120), contentDescription = null, isTouchTarget = true, - touchTargetSize = DpSize(100f, 40f), textColor = null, backgroundColors = emptyList(), isFocusable = true, diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt index 10c03c6..8d9225d 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect private val ErrorBorderColor = Color(0xFFD32F2F) @@ -148,7 +147,6 @@ private fun previewIssue(severity: A11ySeverity) = A11yIssue( bounds = Rect(0, 0, 300, 150), contentDescription = null, isTouchTarget = true, - touchTargetSize = DpSize(100f, 50f), textColor = null, backgroundColors = emptyList(), isFocusable = true, diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt index a7c6664..2d3b47a 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanReportSheet.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult import com.composea11yscanner.export.ScanResultExporter @@ -443,7 +442,7 @@ private fun previewResult(errors: Int, warnings: Int, info: Int): ScanResult { val node = A11yNode( nodeId = "node-1", composableName = "Button", bounds = Rect(0, 0, 300, 120), contentDescription = null, - isTouchTarget = true, touchTargetSize = DpSize(100f, 40f), + isTouchTarget = true, textColor = null, backgroundColors = emptyList(), isFocusable = true, isMergedDescendant = false, depth = 1, ) diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt index 0f58fd6..3b5c964 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult import com.composea11yscanner.core.model.ScannerState @@ -339,7 +338,7 @@ private fun previewResult( val node = A11yNode( nodeId = "node-1", composableName = "Button", bounds = Rect(0, 0, 300, 120), contentDescription = null, - isTouchTarget = true, touchTargetSize = DpSize(100f, 40f), + isTouchTarget = true, textColor = null, backgroundColors = emptyList(), isFocusable = true, isMergedDescendant = false, depth = 1, ) diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt index df02bd7..0092f5b 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScannerUi.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.tooling.preview.Preview import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult @@ -83,7 +82,6 @@ private fun A11yIssueOverlayPreview() { bounds = Rect(0, 0, 120, 48), contentDescription = null, isTouchTarget = true, - touchTargetSize = DpSize(120f, 48f), textColor = null, backgroundColors = emptyList(), isFocusable = true, diff --git a/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt b/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt index 7b8e2f3..6223aab 100644 --- a/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt +++ b/scanner-ui/src/test/java/com/composea11yscanner/ui/IssueDetailPanelSnapshotTest.kt @@ -82,14 +82,6 @@ class IssueDetailPanelSnapshotTest { howToFix = "Apply Modifier.clickable(role = Role.Button) for action controls.", wcagReference = "WCAG 4.1.2 Name, Role, Value (Level A)", ), - issueFixture( - severity = A11ySeverity.Error, - issueId = "touch-target-size", - ruleName = "Touch Target Size", - message = "Touch target is 28x28dp. Minimum required is 48x48dp.", - howToFix = "Apply Modifier.minimumInteractiveComponentSize() or add padding.", - wcagReference = "WCAG 2.5.5 Target Size (Level AA)", - ), issueFixture( severity = A11ySeverity.Warning, issueId = "missing-content-description", diff --git a/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt b/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt index 5218ac8..18719c1 100644 --- a/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt +++ b/scanner-ui/src/test/java/com/composea11yscanner/ui/SnapshotFixtures.kt @@ -3,7 +3,6 @@ package com.composea11yscanner.ui import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.A11ySeverity -import com.composea11yscanner.core.model.DpSize import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult @@ -31,7 +30,6 @@ internal fun nodeFixture() = A11yNode( bounds = Rect(left = 0, top = 0, right = 300, bottom = 120), contentDescription = null, isTouchTarget = true, - touchTargetSize = DpSize(width = 100f, height = 40f), textColor = null, backgroundColors = emptyList(), isFocusable = true,