Merge "Added IconTest and IconButtonTest." into androidx-main
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 0d1dc6f..c0b208a 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -37,11 +37,14 @@
          * corresponding block below
          */
         implementation(libs.kotlinStdlibCommon)
+        implementation(project(":compose:foundation:foundation-layout"))
 
-        api("androidx.compose.foundation:foundation:1.0.1")
+        api(project(":compose:foundation:foundation"))
+        api("androidx.compose.material:material-icons-core:1.0.2")
         api("androidx.compose.material:material-ripple:1.0.0")
         api("androidx.compose.runtime:runtime:1.0.1")
         api("androidx.compose.ui:ui-graphics:1.0.1")
+        api(project(":compose:ui:ui"))
         api("androidx.compose.ui:ui-text:1.0.1")
 
         testImplementation(libs.testRules)
@@ -49,8 +52,7 @@
         testImplementation(libs.junit)
         testImplementation(libs.truth)
 
-        // TODO: Enable when Material 3 samples are in place.
-        // implementation(project(":compose:material3:material3:material3-samples"))
+        androidTestImplementation(project(":compose:material3:material3:material3-samples"))
         androidTestImplementation(project(":compose:test-utils"))
         androidTestImplementation(project(":test:screenshot:screenshot"))
         androidTestImplementation(libs.testRules)
@@ -77,6 +79,7 @@
                 implementation(libs.kotlinStdlibCommon)
 
                 api(project(":compose:foundation:foundation"))
+                api(project(":compose:material:material-icons-core"))
                 api(project(":compose:material:material-ripple"))
                 api(project(":compose:runtime:runtime"))
                 api(project(":compose:ui:ui-graphics"))
@@ -99,8 +102,7 @@
             }
 
             androidAndroidTest.dependencies {
-                // TODO: Enable when Material 3 samples are in place.
-                // implementation(project(":compose:material3:material3:material3-samples"))
+                implementation(project(":compose:material3:material3:material3-samples"))
                 implementation(project(":compose:test-utils"))
                 implementation(project(":test:screenshot:screenshot"))
 
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
new file mode 100644
index 0000000..5a724ca
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconToggleButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+
+@Sampled
+@Composable
+fun IconButtonSample() {
+    IconButton(onClick = { /* doSomething() */ }) {
+        Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+    }
+}
+
+@Sampled
+@Composable
+fun IconToggleButtonSample() {
+    var checked by remember { mutableStateOf(false) }
+
+    IconToggleButton(checked = checked, onCheckedChange = { checked = it }) {
+        val tint by animateColorAsState(if (checked) Color(0xFFEC407A) else Color(0xFFB0BEC5))
+        Icon(Icons.Filled.Favorite, contentDescription = "Localized description", tint = tint)
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
new file mode 100644
index 0000000..4b15249
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2021 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.compose.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.samples.IconButtonSample
+import androidx.compose.material3.samples.IconToggleButtonSample
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTouchHeightIsEqualTo
+import androidx.compose.ui.test.assertTouchWidthIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+/**
+ * Test for [IconButton] and [IconToggleButton].
+ */
+class IconButtonTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun iconButton_size() {
+        val width = 48.dp
+        val height = 48.dp
+        rule
+            .setMaterialContentForSizeAssertions {
+                IconButtonSample()
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun iconButton_defaultSemantics() {
+        rule.setMaterialContent {
+            IconButtonSample()
+        }
+        rule.onNode(hasClickAction()).apply {
+            assertIsEnabled()
+        }
+    }
+
+    @Test
+    fun iconButton_disabledSemantics() {
+        rule.setMaterialContent {
+            IconButton(onClick = {}, enabled = false) {}
+        }
+        rule.onNode(hasClickAction()).apply {
+            assertIsNotEnabled()
+        }
+    }
+
+    @Test
+    fun iconButton_materialIconSize_iconPositioning() {
+        val diameter = 24.dp
+        rule.setMaterialContent {
+            Box {
+                IconButton(onClick = {}) {
+                    Box(Modifier.size(diameter).testTag("icon"))
+                }
+            }
+        }
+
+        // Icon should be centered inside the IconButton
+        rule.onNodeWithTag("icon", useUnmergedTree = true)
+            .assertLeftPositionInRootIsEqualTo(24.dp / 2)
+            .assertTopPositionInRootIsEqualTo(24.dp / 2)
+    }
+
+    @Test
+    fun iconButton_customIconSize_iconPositioning() {
+        val width = 36.dp
+        val height = 14.dp
+        rule.setMaterialContent {
+            Box {
+                IconButton(onClick = {}) {
+                    Box(Modifier.size(width, height).testTag("icon"))
+                }
+            }
+        }
+
+        // Icon should be centered inside the IconButton
+        rule.onNodeWithTag("icon", useUnmergedTree = true)
+            .assertLeftPositionInRootIsEqualTo((48.dp - width) / 2)
+            .assertTopPositionInRootIsEqualTo((48.dp - height) / 2)
+    }
+
+    @Test
+    fun iconToggleButton_size() {
+        val width = 48.dp
+        val height = 48.dp
+        rule
+            .setMaterialContentForSizeAssertions {
+                IconToggleButtonSample()
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun iconToggleButton_defaultSemantics() {
+        rule.setMaterialContent {
+            IconToggleButtonSample()
+        }
+        rule.onNode(isToggleable()).apply {
+            assertIsEnabled()
+            assertIsOff()
+            performClick()
+            assertIsOn()
+        }
+    }
+
+    @Test
+    fun iconToggleButton_disabledSemantics() {
+        rule.setMaterialContent {
+            IconToggleButton(checked = false, onCheckedChange = {}, enabled = false) {}
+        }
+        rule.onNode(isToggleable()).apply {
+            assertIsNotEnabled()
+            assertIsOff()
+        }
+    }
+
+    @Test
+    fun iconToggleButton_materialIconSize_iconPositioning() {
+        val diameter = 24.dp
+        rule.setMaterialContent {
+            Box {
+                IconToggleButton(checked = false, onCheckedChange = {}) {
+                    Box(Modifier.size(diameter).testTag("icon"))
+                }
+            }
+        }
+
+        // Icon should be centered inside the IconButton
+        rule.onNodeWithTag("icon", useUnmergedTree = true)
+            .assertLeftPositionInRootIsEqualTo(24.dp / 2)
+            .assertTopPositionInRootIsEqualTo(24.dp / 2)
+    }
+
+    @Test
+    fun iconToggleButton_customIconSize_iconPositioning() {
+        val width = 36.dp
+        val height = 14.dp
+        rule.setMaterialContent {
+            Box {
+                IconToggleButton(checked = false, onCheckedChange = {}) {
+                    Box(Modifier.size(width, height).testTag("icon"))
+                }
+            }
+        }
+
+        // Icon should be centered inside the IconButton
+        rule.onNodeWithTag("icon", useUnmergedTree = true)
+            .assertLeftPositionInRootIsEqualTo((48.dp - width) / 2)
+            .assertTopPositionInRootIsEqualTo((48.dp - height) / 2)
+    }
+
+    @Test
+    fun iconToggleButton_clickInMinimumTouchTarget(): Unit = with(rule.density) {
+        val tag = "iconToggleButton"
+        var checked by mutableStateOf(false)
+        rule.setMaterialContent {
+            // Box is needed because otherwise the control will be expanded to fill its parent
+            Box(Modifier.fillMaxSize()) {
+                IconToggleButton(
+                    checked = checked,
+                    onCheckedChange = { checked = it },
+                    modifier = Modifier.align(Alignment.Center).requiredSize(2.dp).testTag(tag)
+                ) {
+                    Box(Modifier.size(2.dp))
+                }
+            }
+        }
+        rule.onNodeWithTag(tag)
+            .assertIsOff()
+            .assertWidthIsEqualTo(2.dp)
+            .assertHeightIsEqualTo(2.dp)
+            .assertTouchWidthIsEqualTo(48.dp)
+            .assertTouchHeightIsEqualTo(48.dp)
+            .performTouchInput {
+                click(position = Offset(-1f, -1f))
+            }.assertIsOn()
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
new file mode 100644
index 0000000..ef8ae3e
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2020 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.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class IconTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun vector_materialIconSize_dimensions() {
+        val width = 24.dp
+        val height = 24.dp
+        val vector = Icons.Filled.Menu
+        rule
+            .setMaterialContentForSizeAssertions {
+                Icon(vector, null)
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun vector_customIconSize_dimensions() {
+        val width = 35.dp
+        val height = 83.dp
+        val vector = ImageVector.Builder(
+            defaultWidth = width, defaultHeight = height,
+            viewportWidth = width.value, viewportHeight = height.value
+        ).build()
+        rule
+            .setMaterialContentForSizeAssertions {
+                Icon(vector, null)
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun image_noIntrinsicSize_dimensions() {
+        val width = 24.dp
+        val height = 24.dp
+        rule
+            .setMaterialContentForSizeAssertions {
+                val image = with(LocalDensity.current) {
+                    ImageBitmap(width.roundToPx(), height.roundToPx())
+                }
+
+                Icon(image, null)
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun image_withIntrinsicSize_dimensions() {
+        val width = 35.dp
+        val height = 83.dp
+
+        rule
+            .setMaterialContentForSizeAssertions {
+                val image = with(LocalDensity.current) {
+                    ImageBitmap(width.roundToPx(), height.roundToPx())
+                }
+
+                Icon(image, null)
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun painter_noIntrinsicSize_dimensions() {
+        val width = 24.dp
+        val height = 24.dp
+        val painter = ColorPainter(Color.Red)
+        rule
+            .setMaterialContentForSizeAssertions {
+                Icon(painter, null)
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @Test
+    fun painter_withIntrinsicSize_dimensions() {
+        val width = 35.dp
+        val height = 83.dp
+
+        rule
+            .setMaterialContentForSizeAssertions {
+                val image = with(LocalDensity.current) {
+                    ImageBitmap(width.roundToPx(), height.roundToPx())
+                }
+
+                val bitmapPainter = BitmapPainter(image)
+                Icon(bitmapPainter, null)
+            }
+            .assertWidthIsEqualTo(width)
+            .assertHeightIsEqualTo(height)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun iconScalesToFitSize() {
+        // Image with intrinsic size of 24dp
+        val width = 24.dp
+        val height = 24.dp
+        val testTag = "testTag"
+        var expectedIntSize: IntSize? = null
+        rule.setMaterialContent {
+            val image: ImageBitmap
+            with(LocalDensity.current) {
+                image = createBitmapWithColor(
+                    this,
+                    width.roundToPx(),
+                    height.roundToPx(),
+                    Color.Red
+                )
+            }
+            Icon(
+                image,
+                null,
+                // Force Icon to be 50dp
+                modifier = Modifier.requiredSize(50.dp).testTag(testTag),
+                tint = Color.Unspecified
+            )
+            with(LocalDensity.current) {
+                val dimension = 50.dp.roundToPx()
+                expectedIntSize = IntSize(dimension, dimension)
+            }
+        }
+
+        rule.onNodeWithTag(testTag)
+            .captureToImage()
+            // The icon should be 50x50 and fill the whole size with red pixels
+            .assertPixels(expectedSize = expectedIntSize!!) {
+                Color.Red
+            }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun iconUnspecifiedTintColorIgnored() {
+        val width = 35.dp
+        val height = 83.dp
+        val testTag = "testTag"
+        rule.setMaterialContent {
+            val image: ImageBitmap
+            with(LocalDensity.current) {
+                image = createBitmapWithColor(
+                    this,
+                    width.roundToPx(),
+                    height.roundToPx(),
+                    Color.Red
+                )
+            }
+            Icon(image, null, modifier = Modifier.testTag(testTag), tint = Color.Unspecified)
+        }
+
+        // With no color provided for a tint, the icon should render the original pixels
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Red }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun iconSpecifiedTintColorApplied() {
+        val width = 35.dp
+        val height = 83.dp
+        val testTag = "testTag"
+        rule.setMaterialContent {
+            val image: ImageBitmap
+            with(LocalDensity.current) {
+                image = createBitmapWithColor(
+                    this,
+                    width.roundToPx(),
+                    height.roundToPx(),
+                    Color.Red
+                )
+            }
+            Icon(image, null, modifier = Modifier.testTag(testTag), tint = Color.Blue)
+        }
+
+        // With a tint color provided, all pixels should be blue
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun defaultSemanticsWhenContentDescriptionProvided() {
+        val testTag = "TestTag"
+        rule.setContent {
+            Icon(
+                bitmap = ImageBitmap(100, 100),
+                contentDescription = "qwerty",
+                modifier = Modifier.testTag(testTag)
+            )
+        }
+
+        rule.onNodeWithTag(testTag)
+            .assertContentDescriptionEquals("qwerty")
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Image))
+    }
+
+    private fun createBitmapWithColor(
+        density: Density,
+        width: Int,
+        height: Int,
+        color: Color
+    ): ImageBitmap {
+        val size = Size(width.toFloat(), height.toFloat())
+        val image = ImageBitmap(width, height)
+        CanvasDrawScope().draw(
+            density,
+            LayoutDirection.Ltr,
+            Canvas(image),
+            size
+        ) {
+            drawRect(color)
+        }
+        return image
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index 00fd482..59534d3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -43,6 +43,8 @@
  * [androidx.compose.material.icons.Icons]. If using a custom icon, note that the typical size for
  * the internal icon is 24 x 24 dp.
  *
+ * @sample androidx.compose.material3.samples.IconButtonSample
+ *
  * @param onClick the lambda to be invoked when this icon is pressed
  * @param modifier optional [Modifier] for this IconButton
  * @param enabled whether or not this IconButton will handle input events and appear enabled for
@@ -88,6 +90,8 @@
  * An [IconButton] with two states, for icons that can be toggled 'on' and 'off', such as a bookmark
  * icon, or a navigation icon that opens a drawer.
  *
+ * @sample androidx.compose.material3.samples.IconToggleButtonSample
+ *
  * @param checked whether this IconToggleButton is currently checked
  * @param onCheckedChange callback to be invoked when this icon is selected
  * @param modifier optional [Modifier] for this IconToggleButton
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index c9c8637..ffecff2 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -58,6 +58,7 @@
     samples(project(":compose:foundation:foundation-layout:foundation-layout-samples"))
     samples(project(":compose:foundation:foundation:foundation-samples"))
     docs(project(":compose:material3:material3"))
+    samples(project(":compose:material3:material3:material3-samples"))
     docs(project(":compose:material:material"))
     docs(project(":compose:material:material-icons-core"))
     samples(project(":compose:material:material-icons-core:material-icons-core-samples"))