diff --git a/.gitignore b/.gitignore index 3ef7bc950..4e25c2edf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !/.idea/codeStyleSettings.xml !/.idea/codeStyles !/.idea/dictionaries +!/.idea/inspectionProfiles *.iml .gradle build diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..e1ca550c0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,47 @@ + + + + diff --git a/.run/catalog.run.xml b/.run/catalog.run.xml new file mode 100644 index 000000000..dca53c83d --- /dev/null +++ b/.run/catalog.run.xml @@ -0,0 +1,68 @@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md index f5a9bef72..c896da0e3 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,11 @@

QuackQuack

- + ```kotlin -QuackText( + + import java.awt.ColorQuackText( modifier = Modifier .background(color = Color.White) .span( @@ -14,7 +15,7 @@ QuackText( style = SpanStyle(color = QuackColor.DuckieOrange), ) .padding(30.dp), - text = "QuackQuack is an awesome ui kit created by the Duckie team.", + text = "QuackQuack is an awesome design system created by the Duckie team.", typography = QuackTypography.Body1, ) ``` diff --git a/animation/build.gradle.kts b/animation/build.gradle.kts index 3a737e23d..0117934f3 100644 --- a/animation/build.gradle.kts +++ b/animation/build.gradle.kts @@ -5,13 +5,13 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -@file:Suppress("INLINE_FROM_HIGHER_PLATFORM") - plugins { quackquack("android-library") quackquack("android-compose") quackquack("android-compose-metrics") quackquack("kotlin-explicit-api") + quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") } @@ -25,4 +25,5 @@ dependencies { libs.compose.animation, projects.util.orArtifact(), ) + testImplementation(libs.compose.foundation) } diff --git a/animation/report/compose-metrics/animation_debug-module.json b/animation/report/compose-metrics/animation_debug-module.json index 814d99280..4d2c0dbf8 100644 --- a/animation/report/compose-metrics/animation_debug-module.json +++ b/animation/report/compose-metrics/animation_debug-module.json @@ -1,25 +1,25 @@ { - "skippableComposables": 2, - "restartableComposables": 2, + "skippableComposables": 0, + "restartableComposables": 0, "readonlyComposables": 0, - "totalComposables": 5, - "restartGroups": 2, - "totalGroups": 5, - "staticArguments": 1, - "certainArguments": 25, - "knownStableArguments": 42, - "knownUnstableArguments": 9, - "unknownStableArguments": 1, - "totalArguments": 52, + "totalComposables": 3, + "restartGroups": 0, + "totalGroups": 3, + "staticArguments": 0, + "certainArguments": 18, + "knownStableArguments": 30, + "knownUnstableArguments": 11, + "unknownStableArguments": 0, + "totalArguments": 41, "markedStableClasses": 0, "inferredStableClasses": 0, - "inferredUnstableClasses": 1, + "inferredUnstableClasses": 0, "inferredUncertainClasses": 0, "effectivelyStableClasses": 0, - "totalClasses": 1, - "memoizedLambdas": 1, - "singletonLambdas": 1, + "totalClasses": 0, + "memoizedLambdas": 0, + "singletonLambdas": 0, "singletonComposableLambdas": 0, "composableLambdas": 0, - "totalLambdas": 1 + "totalLambdas": 0 } \ No newline at end of file diff --git a/animation/report/compose-metrics/animation_debugUnitTest-module.json b/animation/report/compose-metrics/animation_debugUnitTest-module.json new file mode 100644 index 000000000..939b96db0 --- /dev/null +++ b/animation/report/compose-metrics/animation_debugUnitTest-module.json @@ -0,0 +1,25 @@ +{ + "skippableComposables": 3, + "restartableComposables": 3, + "readonlyComposables": 0, + "totalComposables": 3, + "restartGroups": 3, + "totalGroups": 3, + "staticArguments": 7, + "certainArguments": 0, + "knownStableArguments": 40, + "knownUnstableArguments": 3, + "unknownStableArguments": 0, + "totalArguments": 43, + "markedStableClasses": 0, + "inferredStableClasses": 0, + "inferredUnstableClasses": 0, + "inferredUncertainClasses": 3, + "effectivelyStableClasses": 0, + "totalClasses": 3, + "memoizedLambdas": 6, + "singletonLambdas": 0, + "singletonComposableLambdas": 3, + "composableLambdas": 3, + "totalLambdas": 6 +} \ No newline at end of file diff --git a/animation/report/compose-metrics/animation_release-module.json b/animation/report/compose-metrics/animation_release-module.json index 814d99280..4d2c0dbf8 100644 --- a/animation/report/compose-metrics/animation_release-module.json +++ b/animation/report/compose-metrics/animation_release-module.json @@ -1,25 +1,25 @@ { - "skippableComposables": 2, - "restartableComposables": 2, + "skippableComposables": 0, + "restartableComposables": 0, "readonlyComposables": 0, - "totalComposables": 5, - "restartGroups": 2, - "totalGroups": 5, - "staticArguments": 1, - "certainArguments": 25, - "knownStableArguments": 42, - "knownUnstableArguments": 9, - "unknownStableArguments": 1, - "totalArguments": 52, + "totalComposables": 3, + "restartGroups": 0, + "totalGroups": 3, + "staticArguments": 0, + "certainArguments": 18, + "knownStableArguments": 30, + "knownUnstableArguments": 11, + "unknownStableArguments": 0, + "totalArguments": 41, "markedStableClasses": 0, "inferredStableClasses": 0, - "inferredUnstableClasses": 1, + "inferredUnstableClasses": 0, "inferredUncertainClasses": 0, "effectivelyStableClasses": 0, - "totalClasses": 1, - "memoizedLambdas": 1, - "singletonLambdas": 1, + "totalClasses": 0, + "memoizedLambdas": 0, + "singletonLambdas": 0, "singletonComposableLambdas": 0, "composableLambdas": 0, - "totalLambdas": 1 + "totalLambdas": 0 } \ No newline at end of file diff --git a/animation/report/compose-metrics/animation_releaseUnitTest-module.json b/animation/report/compose-metrics/animation_releaseUnitTest-module.json new file mode 100644 index 000000000..939b96db0 --- /dev/null +++ b/animation/report/compose-metrics/animation_releaseUnitTest-module.json @@ -0,0 +1,25 @@ +{ + "skippableComposables": 3, + "restartableComposables": 3, + "readonlyComposables": 0, + "totalComposables": 3, + "restartGroups": 3, + "totalGroups": 3, + "staticArguments": 7, + "certainArguments": 0, + "knownStableArguments": 40, + "knownUnstableArguments": 3, + "unknownStableArguments": 0, + "totalArguments": 43, + "markedStableClasses": 0, + "inferredStableClasses": 0, + "inferredUnstableClasses": 0, + "inferredUncertainClasses": 3, + "effectivelyStableClasses": 0, + "totalClasses": 3, + "memoizedLambdas": 6, + "singletonLambdas": 0, + "singletonComposableLambdas": 3, + "composableLambdas": 3, + "totalLambdas": 6 +} \ No newline at end of file diff --git a/animation/report/compose-reports/animation_debug-classes.txt b/animation/report/compose-reports/animation_debug-classes.txt index c9a7251f4..e69de29bb 100644 --- a/animation/report/compose-reports/animation_debug-classes.txt +++ b/animation/report/compose-reports/animation_debug-classes.txt @@ -1,4 +0,0 @@ -unstable class QuackAnimationSpec { - stable var snapMode: Boolean - = Unstable -} diff --git a/animation/report/compose-reports/animation_debug-composables.csv b/animation/report/compose-reports/animation_debug-composables.csv index 488e36fe4..4cf6c74ce 100644 --- a/animation/report/compose-reports/animation_debug-composables.csv +++ b/animation/report/compose-reports/animation_debug-composables.csv @@ -1,6 +1,4 @@ package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, -team.duckie.quackquack.animation.QuackAnimatedContent,QuackAnimatedContent,1,1,1,0,0,0,0,0,1,1, -team.duckie.quackquack.animation.QuackAnimatedVisibility,QuackAnimatedVisibility,1,1,1,0,0,0,0,0,1,1, team.duckie.quackquack.animation.animatedQuackBorderAsState,animatedQuackBorderAsState,1,0,0,0,0,0,0,0,1,2, team.duckie.quackquack.animation.animateQuackColorAsState,animateQuackColorAsState,1,0,0,0,0,0,0,0,1,2, -team.duckie.quackquack.animation.animatedQuackTextStyleAsState,animatedQuackTextStyleAsState,1,0,0,0,0,0,0,0,1,5, +team.duckie.quackquack.animation.animatedQuackTypographyAsState,animatedQuackTypographyAsState,1,0,0,0,0,0,0,0,1,5, diff --git a/animation/report/compose-reports/animation_debug-composables.txt b/animation/report/compose-reports/animation_debug-composables.txt index d4d21b3c9..47fc8ab6c 100644 --- a/animation/report/compose-reports/animation_debug-composables.txt +++ b/animation/report/compose-reports/animation_debug-composables.txt @@ -1,31 +1,20 @@ -restartable skippable fun QuackAnimatedContent( - stable modifier: Modifier? = @static Companion - targetState: T - stable label: String? = @static "AnimatedContent" - stable content: @[ExtensionFunctionType] Function4 -) -restartable skippable scheme("[0, [0]]") fun QuackAnimatedVisibility( - stable modifier: Modifier? = @static Companion - stable visible: Boolean - stable label: String? = @static "AnimatedVisibility" - stable otherEnterAnimation: EnterTransition? = @static null - stable otherExitAnimation: ExitTransition? = @static null - stable content: @[ExtensionFunctionType] Function3 -) fun animatedQuackBorderAsState( stable targetValue: QuackBorder - stable label: String? = @static "QuackBorderAnimation" + unstable animationSpec: AnimationSpec? = @static quackTween() + stable label: String? = @static "QuackBorder" stable widthAnimationfinishedListener: Function1<@[ParameterName(name = 'dp')] Dp, Unit>? = @static null stable colorAnimationFinishedListener: Function1<@[ParameterName(name = 'color')] QuackColor, Unit>? = @static null ): QuackBorder fun animateQuackColorAsState( stable targetValue: QuackColor - stable label: String? = @static "animateQuackColorAsState" + unstable animationSpec: AnimationSpec? = @static quackTween() + stable label: String? = @static "QuackColor" stable finishedListener: Function1<@[ParameterName(name = 'color')] QuackColor, Unit>? = @static null ): State -fun animatedQuackTextStyleAsState( +fun animatedQuackTypographyAsState( stable targetValue: QuackTypography - stable label: String? = @static "animatedQuackTextStyleAsState" + unstable animationSpec: AnimationSpec? = @static quackTween() + stable label: String? = @static "QuackTypography" stable colorAnimationFinishedListener: Function1<@[ParameterName(name = 'color')] QuackColor, Unit>? = @static null stable sizeAnimationFinishedListener: Function1<@[ParameterName(name = 'size')] Float, Unit>? = @static null stable letterSpacingAnimationFinishedListener: Function1<@[ParameterName(name = 'letterSpacing')] Float, Unit>? = @static null diff --git a/animation/report/compose-reports/animation_debugUnitTest-classes.txt b/animation/report/compose-reports/animation_debugUnitTest-classes.txt new file mode 100644 index 000000000..41465a9f6 --- /dev/null +++ b/animation/report/compose-reports/animation_debugUnitTest-classes.txt @@ -0,0 +1,12 @@ +runtime class QuackBorderSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) +} +runtime class QuackColorSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) +} +runtime class QuackTypographySnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) +} diff --git a/animation/report/compose-reports/animation_debugUnitTest-composables.csv b/animation/report/compose-reports/animation_debugUnitTest-composables.csv new file mode 100644 index 000000000..975e679ba --- /dev/null +++ b/animation/report/compose-reports/animation_debugUnitTest-composables.csv @@ -0,0 +1 @@ +package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, diff --git a/animation/report/compose-reports/animation_debugUnitTest-composables.txt b/animation/report/compose-reports/animation_debugUnitTest-composables.txt new file mode 100644 index 000000000..e69de29bb diff --git a/animation/report/compose-reports/animation_release-classes.txt b/animation/report/compose-reports/animation_release-classes.txt index c9a7251f4..e69de29bb 100644 --- a/animation/report/compose-reports/animation_release-classes.txt +++ b/animation/report/compose-reports/animation_release-classes.txt @@ -1,4 +0,0 @@ -unstable class QuackAnimationSpec { - stable var snapMode: Boolean - = Unstable -} diff --git a/animation/report/compose-reports/animation_release-composables.csv b/animation/report/compose-reports/animation_release-composables.csv index 488e36fe4..4cf6c74ce 100644 --- a/animation/report/compose-reports/animation_release-composables.csv +++ b/animation/report/compose-reports/animation_release-composables.csv @@ -1,6 +1,4 @@ package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, -team.duckie.quackquack.animation.QuackAnimatedContent,QuackAnimatedContent,1,1,1,0,0,0,0,0,1,1, -team.duckie.quackquack.animation.QuackAnimatedVisibility,QuackAnimatedVisibility,1,1,1,0,0,0,0,0,1,1, team.duckie.quackquack.animation.animatedQuackBorderAsState,animatedQuackBorderAsState,1,0,0,0,0,0,0,0,1,2, team.duckie.quackquack.animation.animateQuackColorAsState,animateQuackColorAsState,1,0,0,0,0,0,0,0,1,2, -team.duckie.quackquack.animation.animatedQuackTextStyleAsState,animatedQuackTextStyleAsState,1,0,0,0,0,0,0,0,1,5, +team.duckie.quackquack.animation.animatedQuackTypographyAsState,animatedQuackTypographyAsState,1,0,0,0,0,0,0,0,1,5, diff --git a/animation/report/compose-reports/animation_release-composables.txt b/animation/report/compose-reports/animation_release-composables.txt index d4d21b3c9..47fc8ab6c 100644 --- a/animation/report/compose-reports/animation_release-composables.txt +++ b/animation/report/compose-reports/animation_release-composables.txt @@ -1,31 +1,20 @@ -restartable skippable fun QuackAnimatedContent( - stable modifier: Modifier? = @static Companion - targetState: T - stable label: String? = @static "AnimatedContent" - stable content: @[ExtensionFunctionType] Function4 -) -restartable skippable scheme("[0, [0]]") fun QuackAnimatedVisibility( - stable modifier: Modifier? = @static Companion - stable visible: Boolean - stable label: String? = @static "AnimatedVisibility" - stable otherEnterAnimation: EnterTransition? = @static null - stable otherExitAnimation: ExitTransition? = @static null - stable content: @[ExtensionFunctionType] Function3 -) fun animatedQuackBorderAsState( stable targetValue: QuackBorder - stable label: String? = @static "QuackBorderAnimation" + unstable animationSpec: AnimationSpec? = @static quackTween() + stable label: String? = @static "QuackBorder" stable widthAnimationfinishedListener: Function1<@[ParameterName(name = 'dp')] Dp, Unit>? = @static null stable colorAnimationFinishedListener: Function1<@[ParameterName(name = 'color')] QuackColor, Unit>? = @static null ): QuackBorder fun animateQuackColorAsState( stable targetValue: QuackColor - stable label: String? = @static "animateQuackColorAsState" + unstable animationSpec: AnimationSpec? = @static quackTween() + stable label: String? = @static "QuackColor" stable finishedListener: Function1<@[ParameterName(name = 'color')] QuackColor, Unit>? = @static null ): State -fun animatedQuackTextStyleAsState( +fun animatedQuackTypographyAsState( stable targetValue: QuackTypography - stable label: String? = @static "animatedQuackTextStyleAsState" + unstable animationSpec: AnimationSpec? = @static quackTween() + stable label: String? = @static "QuackTypography" stable colorAnimationFinishedListener: Function1<@[ParameterName(name = 'color')] QuackColor, Unit>? = @static null stable sizeAnimationFinishedListener: Function1<@[ParameterName(name = 'size')] Float, Unit>? = @static null stable letterSpacingAnimationFinishedListener: Function1<@[ParameterName(name = 'letterSpacing')] Float, Unit>? = @static null diff --git a/animation/report/compose-reports/animation_releaseUnitTest-classes.txt b/animation/report/compose-reports/animation_releaseUnitTest-classes.txt new file mode 100644 index 000000000..41465a9f6 --- /dev/null +++ b/animation/report/compose-reports/animation_releaseUnitTest-classes.txt @@ -0,0 +1,12 @@ +runtime class QuackBorderSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) +} +runtime class QuackColorSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) +} +runtime class QuackTypographySnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) +} diff --git a/animation/report/compose-reports/animation_releaseUnitTest-composables.csv b/animation/report/compose-reports/animation_releaseUnitTest-composables.csv new file mode 100644 index 000000000..975e679ba --- /dev/null +++ b/animation/report/compose-reports/animation_releaseUnitTest-composables.csv @@ -0,0 +1 @@ +package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, diff --git a/animation/report/compose-reports/animation_releaseUnitTest-composables.txt b/animation/report/compose-reports/animation_releaseUnitTest-composables.txt new file mode 100644 index 000000000..e69de29bb diff --git a/animation/src/main/kotlin/team/duckie/quackquack/animation/AnimatedContent.kt b/animation/src/main/kotlin/team/duckie/quackquack/animation/AnimatedContent.kt deleted file mode 100644 index 472939fa3..000000000 --- a/animation/src/main/kotlin/team/duckie/quackquack/animation/AnimatedContent.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Designed and developed by Duckie Team, 2022~2023 - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE - */ - -package team.duckie.quackquack.animation - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.SizeTransform -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.with -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -/** - * 컴포저블의 상태에 변화가 있을 때 자동으로 해당 상태에 맞춰 애니메이션을 적용합니다. - * - * @param targetState 변화를 감지할 컴포저블의 상태값 - * @param label Animation Inspector에서 이 애니메이션을 구분할 별칭 - * @param content 상태 변화 애니메이션을 적용할 컴포저블 컨텐츠. 인자로는 애니메이션이 - * 적용되고 있는 [targetState]의 값이 들어옵니다. **애니메이션이 적용되기 위해선 필수로 - * 이 값을 상태로 적용해야 합니다.** - */ -@OptIn(ExperimentalAnimationApi::class) -@Composable -public fun QuackAnimatedContent( - modifier: Modifier = Modifier, - targetState: T, - label: String = "AnimatedContent", - content: @Composable AnimatedVisibilityScope.(animatedTargetState: T) -> Unit, -) { - AnimatedContent( - modifier = modifier, - targetState = targetState, - transitionSpec = { - fadeIn( - animationSpec = QuackAnimationSpec(), - ) with fadeOut( - animationSpec = QuackAnimationSpec(), - ) using SizeTransform( - clip = false, - sizeAnimationSpec = { _, _ -> - QuackAnimationSpec() - }, - ) - }, - label = label, - content = content, - ) -} diff --git a/animation/src/main/kotlin/team/duckie/quackquack/animation/AnimatedVisibility.kt b/animation/src/main/kotlin/team/duckie/quackquack/animation/AnimatedVisibility.kt deleted file mode 100644 index 3ff05c44d..000000000 --- a/animation/src/main/kotlin/team/duckie/quackquack/animation/AnimatedVisibility.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.animation - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -/** - * 컴포저블의 visiblilty 변화에 애니메이션을 적용합니다. - * - * @param visible visibility 여부 - * @param label Animation Inspector에서 이 애니메이션을 구분할 별칭 - * @param otherEnterAnimation 추가로 더할 enter 애니메이션 - * @param otherExitAnimation 추가로 더할 exit 애니메이션 - * @param content visiblilty 애니메이션이 적용될 컴포저블 컨텐츠 - */ -@Composable -public fun QuackAnimatedVisibility( - modifier: Modifier = Modifier, - visible: Boolean, - label: String = "AnimatedVisibility", - otherEnterAnimation: EnterTransition? = null, - otherExitAnimation: ExitTransition? = null, - content: @Composable AnimatedVisibilityScope.() -> Unit, -) { - AnimatedVisibility( - modifier = modifier, - visible = visible, - enter = fadeIn(QuackAnimationSpec()).optionalAdd(otherEnterAnimation), - exit = fadeOut(QuackAnimationSpec()).optionalAdd(otherExitAnimation), - label = label, - content = content, - ) -} - -private fun EnterTransition.optionalAdd(additional: EnterTransition?): EnterTransition { - return if (additional != null) plus(additional) else this -} - -private fun ExitTransition.optionalAdd(additional: ExitTransition?): ExitTransition { - return if (additional != null) plus(additional) else this -} diff --git a/animation/src/main/kotlin/team/duckie/quackquack/animation/border.kt b/animation/src/main/kotlin/team/duckie/quackquack/animation/border.kt index a9fda1f9c..9d66525ae 100644 --- a/animation/src/main/kotlin/team/duckie/quackquack/animation/border.kt +++ b/animation/src/main/kotlin/team/duckie/quackquack/animation/border.kt @@ -5,8 +5,11 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:Suppress("UNCHECKED_CAST") + package team.duckie.quackquack.animation +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateDpAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -19,6 +22,7 @@ import team.duckie.quackquack.material.QuackColor * [QuackBorder.color]의 변화에 애니메이션이 적용됩니다. * * @param targetValue 애니메이션을 적용할 [QuackBorder] + * @param animationSpec 애니메이션에 적용할 [애니메이션 스펙][AnimationSpec] * @param label Animation Inspector에서 이 애니메이션을 구분할 별칭 * @param widthAnimationfinishedListener [QuackBorder.thickness]의 애니메이션이 * 끝나면 호출될 콜백 @@ -30,18 +34,20 @@ import team.duckie.quackquack.material.QuackColor @Composable public fun animatedQuackBorderAsState( targetValue: QuackBorder, - label: String = "QuackBorderAnimation", + animationSpec: AnimationSpec = quackTween(), + label: String = "QuackBorder", widthAnimationfinishedListener: ((dp: Dp) -> Unit)? = null, colorAnimationFinishedListener: ((color: QuackColor) -> Unit)? = null, ): QuackBorder { val widthAnimationState by animateDpAsState( targetValue = targetValue.thickness, - animationSpec = QuackAnimationSpec(), - finishedListener = widthAnimationfinishedListener, + animationSpec = animationSpec as AnimationSpec, label = label, + finishedListener = widthAnimationfinishedListener, ) val colorAnimationState by animateQuackColorAsState( targetValue = targetValue.color, + animationSpec = animationSpec as AnimationSpec, label = label, finishedListener = colorAnimationFinishedListener, ) diff --git a/animation/src/main/kotlin/team/duckie/quackquack/animation/color.kt b/animation/src/main/kotlin/team/duckie/quackquack/animation/color.kt index 44f0031a9..373ab64c2 100644 --- a/animation/src/main/kotlin/team/duckie/quackquack/animation/color.kt +++ b/animation/src/main/kotlin/team/duckie/quackquack/animation/color.kt @@ -9,6 +9,7 @@ package team.duckie.quackquack.animation +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector4D import androidx.compose.animation.core.TwoWayConverter import androidx.compose.animation.core.animateValueAsState @@ -155,6 +156,7 @@ private val QuackColorVectorConverter: (colorSpace: ColorSpace) -> TwoWayConvert * [QuackColor]에 변경이 있을 때 애니메이션을 적용합니다. * * @param targetValue 색상 변경을 감지할 [QuackColor] + * @param animationSpec 애니메이션에 적용할 [애니메이션 스펙][AnimationSpec] * @param label Animation Inspector에서 이 애니메이션을 구분할 별칭 * @param finishedListener 애니메이션이 끝났을 때 실행될 콜백 * @@ -163,7 +165,8 @@ private val QuackColorVectorConverter: (colorSpace: ColorSpace) -> TwoWayConvert @Composable public fun animateQuackColorAsState( targetValue: QuackColor, - label: String = "animateQuackColorAsState", + animationSpec: AnimationSpec = quackTween(), + label: String = "QuackColor", finishedListener: ((color: QuackColor) -> Unit)? = null, ): State { val converter = remember(targetValue.value.colorSpace) { @@ -172,8 +175,8 @@ public fun animateQuackColorAsState( return animateValueAsState( targetValue = targetValue, typeConverter = converter, - animationSpec = QuackAnimationSpec(), - finishedListener = finishedListener, + animationSpec = animationSpec, label = label, + finishedListener = finishedListener, ) } diff --git a/animation/src/main/kotlin/team/duckie/quackquack/animation/spec.kt b/animation/src/main/kotlin/team/duckie/quackquack/animation/spec.kt index 144949e3a..fb8c10097 100644 --- a/animation/src/main/kotlin/team/duckie/quackquack/animation/spec.kt +++ b/animation/src/main/kotlin/team/duckie/quackquack/animation/spec.kt @@ -8,76 +8,22 @@ package team.duckie.quackquack.animation import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.DurationBasedAnimationSpec +import androidx.compose.animation.core.Easing import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.SnapSpec import androidx.compose.animation.core.TweenSpec -import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.runtime.Stable -import team.duckie.quackquack.util.DelicateQuackQuackApi -/** - * 꽥꽥에서 기본적으로 사용할 애니메이션의 기본 지속 시간 - * - * Playground에서 [QuackAnimationMillis] 편집 후 기본값으로 되돌리고 싶을 때 - * 기본값으로 사용할 수 있습니다. - */ +/** 꽥꽥에서 기본으로 사용할 애니메이션의 지속 시간 */ public const val QuackDefaultAnimationMillis: Int = 150 -/** - * 꽥꽥에서 사용할 애니메이션의 지속 시간 - * - * 애니메이션 디버깅 용도로 수정을 허용합니다. Transition API를 사용해 애니메이션을 - * 디버깅하는 방법도 있지만 컴포즈에서 Preview가 최적하게 돌아가지 않아 수동 애니메이션 - * 디버깅도 고려합니다. - * - * Playground에서 자유로운 지속 시간 편집으로 쉬운 디버깅이 가능합니다. - */ -public var QuackAnimationMillis: Int = QuackDefaultAnimationMillis - -/** 꽥꽥에서 사용할 [AnimationSpec]의 정보 */ -public object QuackAnimationSpec { - /** - * 일부 환경에서는 애니메이션이 없이 진행돼야 할 때도 있습니다. 이 값을 true로 - * 설정하면 모든 애니메이션을 무시합니다. - * - * **이 값의 변경은 모든 애니메이션에 영향을 미치므로 신중하게 사용해야 합니다.** - */ - @DelicateQuackQuackApi - public var snapMode: Boolean = false - - /** - * 꽥꽥에서 사용할 [애니메이션의 기본 스펙][AnimationSpec] - * - * @return 덕키에서 사용할 [AnimationSpec]. [snapMode] 에 따라 반환값이 달라집니다. - * false라면 덕키에서 사용하는 애니메이션 스펙인 [TweenSpec]이 반환되고, true라면 - * [SnapSpec]이 반환됩니다. - * - * @see snapMode - */ - @OptIn(DelicateQuackQuackApi::class) - public operator fun invoke(): DurationBasedAnimationSpec = when (snapMode) { - true -> snap() - else -> tween( - durationMillis = QuackAnimationMillis, - easing = LinearEasing, - ) - } -} +/** 꽥꽥에서 기본으로 사용할 애니메이션의 [Easing] */ +public val QuackDefaultAnimationEasing: Easing = LinearEasing -/** - * [QuackAnimationSpec.snapMode] 대신에 한 번만 선택적으로 애니메이션 여부를 결정하기 - * 위해 사용할 수 있습니다. - * - * @param useAnimation 애니메이션을 사용할지 여부 - * - * @return [useAnimation] 여부에 따른 [DurationBasedAnimationSpec] - */ +/** 꽥꽥에서 기본으로 사용할 애니메이션의 [AnimationSpec] */ @Stable -public fun quackOptionalAnimationSpec(useAnimation: Boolean): DurationBasedAnimationSpec { - return when (useAnimation) { - true -> QuackAnimationSpec() - else -> snap() - } -} +public fun quackTween(): TweenSpec = + tween( + durationMillis = QuackDefaultAnimationMillis, + easing = QuackDefaultAnimationEasing, + ) diff --git a/animation/src/main/kotlin/team/duckie/quackquack/animation/typography.kt b/animation/src/main/kotlin/team/duckie/quackquack/animation/typography.kt index 747b5a04c..93e315cee 100644 --- a/animation/src/main/kotlin/team/duckie/quackquack/animation/typography.kt +++ b/animation/src/main/kotlin/team/duckie/quackquack/animation/typography.kt @@ -5,8 +5,11 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:Suppress("UNCHECKED_CAST") + package team.duckie.quackquack.animation +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.TwoWayConverter import androidx.compose.animation.core.animateFloatAsState @@ -18,30 +21,28 @@ import kotlin.math.roundToInt import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.QuackTypography import team.duckie.quackquack.material.toSp -import team.duckie.quackquack.util.AllowMagicNumber /** * [TextAlign]의 [TwoWayConverter]를 구현합니다. * - * [TextAlign]의 생성자와 필드가 다 internal 이기에 [TextAlign]의 인자 값으로 될 수 있는 - * 1~6 까지의 [Int] 값을 애니메이션하여 각각 값에 맞는 [TextAlign]을 찾아서 생성하는 식으로 + * [TextAlign]의 생성자와 필드가 다 `internal`이기에 [TextAlign]의 인자 값으로 될 수 있는 + * 1~6까지의 [Int] 값을 애니메이션하여 각각 값에 맞는 [TextAlign]을 찾아서 생성하는 식으로 * 구현됩니다. */ -public val TextAlignVectorConverter: TwoWayConverter = - object : TwoWayConverter { - val values = TextAlign.values() +public val TextAlign.Companion.VectorConverter: TwoWayConverter + get() = object : TwoWayConverter { + val values = values() + override val convertFromVector: (vector: AnimationVector1D) -> TextAlign get() = { vector -> - @AllowMagicNumber(because = "TextAlign 의 range 를 나타냅니다.") - val index = vector.value.roundToInt().coerceIn( - // TextAlign 의 값이 1 부터 시작 - range = 1..6, - ) + // TextAlign의 값이 1 부터 시작 + val index = vector.value.roundToInt().coerceIn(1, 6) values[index] } + override val convertToVector: (value: TextAlign) -> AnimationVector1D get() = { value -> - // TextAlign 의 값이 1 부터 시작해서 +1 + // TextAlign의 값이 1 부터 시작해서 +1 AnimationVector1D(values.indexOf(value) + 1f) } } @@ -57,6 +58,7 @@ public val TextAlignVectorConverter: TwoWayConverter = quackTween(), + label: String = "QuackTypography", colorAnimationFinishedListener: ((color: QuackColor) -> Unit)? = null, sizeAnimationFinishedListener: ((size: Float) -> Unit)? = null, letterSpacingAnimationFinishedListener: ((letterSpacing: Float) -> Unit)? = null, @@ -83,31 +86,32 @@ public fun animatedQuackTextStyleAsState( ): QuackTypography { val targetColorAnimationState by animateQuackColorAsState( targetValue = targetValue.color, + animationSpec = animationSpec as AnimationSpec, label = label, finishedListener = colorAnimationFinishedListener, ) val targetSizeAnimationState by animateFloatAsState( targetValue = targetValue.size.value, - animationSpec = QuackAnimationSpec(), + animationSpec = animationSpec as AnimationSpec, label = label, finishedListener = sizeAnimationFinishedListener, ) val targetLetterSpacingAnimationState by animateFloatAsState( targetValue = targetValue.letterSpacing.value, - animationSpec = QuackAnimationSpec(), + animationSpec = animationSpec as AnimationSpec, label = label, finishedListener = letterSpacingAnimationFinishedListener, ) val targetLineHeightAnimationState by animateFloatAsState( targetValue = targetValue.lineHeight.value, - animationSpec = QuackAnimationSpec(), + animationSpec = animationSpec as AnimationSpec, label = label, finishedListener = lineHeightAnimationFinishedListener, ) val targetTextAlignAnimationState by animateValueAsState( targetValue = targetValue.textAlign, - typeConverter = TextAlignVectorConverter, - animationSpec = QuackAnimationSpec(), + typeConverter = TextAlign.VectorConverter, + animationSpec = animationSpec as AnimationSpec, label = label, finishedListener = textAlignAnimationFinishedListener, ) diff --git a/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackBorderSnapshot.kt b/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackBorderSnapshot.kt new file mode 100644 index 000000000..ce64e114f --- /dev/null +++ b/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackBorderSnapshot.kt @@ -0,0 +1,56 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.animation.snapshot + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.animation.animatedQuackBorderAsState +import team.duckie.quackquack.material.QuackBorder +import team.duckie.quackquack.material.QuackColor +import team.duckie.quackquack.material.quackBorder +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule + +@RunWith(AndroidJUnit4::class) +class QuackBorderSnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("border") + + @Test + fun animated_without_exception() { + captureRoboImage(snapshotPath()) { + var state by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + state = true + } + + Box( + Modifier + .size(30.dp) + .quackBorder( + border = animatedQuackBorderAsState( + if (state) QuackBorder(thickness = 5.dp, color = QuackColor.Success) + else QuackBorder(thickness = 1.dp, color = QuackColor.Alert), + ), + ), + ) + } + } +} diff --git a/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackColorSnapshot.kt b/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackColorSnapshot.kt new file mode 100644 index 000000000..f7006228d --- /dev/null +++ b/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackColorSnapshot.kt @@ -0,0 +1,54 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.animation.snapshot + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.animation.animateQuackColorAsState +import team.duckie.quackquack.material.QuackColor +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule + +@RunWith(AndroidJUnit4::class) +class QuackColorSnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("color") + + @Test + fun animated_without_exception() { + captureRoboImage(snapshotPath()) { + var state by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + state = true + } + + Box( + Modifier + .size(30.dp) + .background( + color = animateQuackColorAsState( + if (state) QuackColor.Success else QuackColor.Alert, + ).value.value, + ), + ) + } + } +} diff --git a/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackTypographySnapshot.kt b/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackTypographySnapshot.kt new file mode 100644 index 000000000..22a6436ad --- /dev/null +++ b/animation/src/test/kotlin/team/duckie/quackquack/animation/snapshot/QuackTypographySnapshot.kt @@ -0,0 +1,49 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.animation.snapshot + +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.animation.animatedQuackTypographyAsState +import team.duckie.quackquack.material.QuackColor +import team.duckie.quackquack.material.QuackTypography +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule + +@RunWith(AndroidJUnit4::class) +class QuackTypographySnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("typography") + + @Test + fun animated_without_exception() { + captureRoboImage(snapshotPath()) { + var state by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + state = true + } + + BasicText( + text = "Hello, World!", + style = animatedQuackTypographyAsState( + if (state) QuackTypography.Large1.change(color = QuackColor.Success) + else QuackTypography.Body3.change(color = QuackColor.Alert), + ).asComposeStyle(), + ) + } + } +} diff --git a/animation/src/test/resources/robolectric.properties b/animation/src/test/resources/robolectric.properties new file mode 100644 index 000000000..17e6de451 --- /dev/null +++ b/animation/src/test/resources/robolectric.properties @@ -0,0 +1,8 @@ +# +# Designed and developed by Duckie Team 2023. +# +# Licensed under the MIT. +# Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE +# + +sdk=33 \ No newline at end of file diff --git a/animation/src/test/snapshots/border/animated_without_exception.png b/animation/src/test/snapshots/border/animated_without_exception.png new file mode 100644 index 000000000..a0a541551 Binary files /dev/null and b/animation/src/test/snapshots/border/animated_without_exception.png differ diff --git a/animation/src/test/snapshots/color/animated_without_exception.png b/animation/src/test/snapshots/color/animated_without_exception.png new file mode 100644 index 000000000..17efab7da Binary files /dev/null and b/animation/src/test/snapshots/color/animated_without_exception.png differ diff --git a/animation/src/test/snapshots/typography/animated_without_exception.png b/animation/src/test/snapshots/typography/animated_without_exception.png new file mode 100644 index 000000000..36b96bd76 Binary files /dev/null and b/animation/src/test/snapshots/typography/animated_without_exception.png differ diff --git a/assets/QuackTextSnapshot_ModifierSpan.png b/assets/QuackTextSnapshot_ModifierSpan.png deleted file mode 100644 index 23949273c..000000000 Binary files a/assets/QuackTextSnapshot_ModifierSpan.png and /dev/null differ diff --git a/assets/awesome-quackquack.png b/assets/awesome-quackquack.png new file mode 100644 index 000000000..966a234dd Binary files /dev/null and b/assets/awesome-quackquack.png differ diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index 0ebecdc1d..46b8bf398 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { api( ArtifactConfig.of(project).toString() .also { artifact -> - println("BOM publishing: $artifact") + println("BOM loading: $artifact") }, ) } diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index a62337fa8..a51010ddc 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -18,12 +18,12 @@ gradlePlugin { "AndroidApplicationPlugin" to "android-application", "AndroidLibraryPlugin" to "android-library", "AndroidGmdPlugin" to "android-gmd", - "AndroidLintPlugin" to "android-lint", "AndroidComposePlugin" to "android-compose", "AndroidComposeMetricsPlugin" to "android-compose-metrics", "JvmKotlinPlugin" to "jvm-kotlin", "TestJUnitPlugin" to "test-junit", "TestKotestPlugin" to "test-kotest", + "TestRoborazziPlugin" to "test-roborazzi", "KotlinExplicitApiPlugin" to "kotlin-explicit-api", "QuackMavenPublishingPlugin" to "quack-publishing", ) diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties index 8a837eacf..4297b6044 100644 --- a/build-logic/gradle.properties +++ b/build-logic/gradle.properties @@ -8,7 +8,9 @@ org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC org.gradle.configureondemand=true org.gradle.parallel=true org.gradle.caching=true + # https://blog.gradle.org/introducing-file-system-watching org.gradle.vfs.watch=true + # https://docs.gradle.org/7.6/userguide/configuration_cache.html -org.gradle.unsafe.configuration-cache=true +org.gradle.configuration-cache=true diff --git a/build-logic/src/main/kotlin/ProjectOrArtifact.kt b/build-logic/src/main/kotlin/ProjectOrArtifact.kt index d5d27e634..1a4f10980 100644 --- a/build-logic/src/main/kotlin/ProjectOrArtifact.kt +++ b/build-logic/src/main/kotlin/ProjectOrArtifact.kt @@ -47,14 +47,9 @@ private const val useArtifact = false * * @see useArtifact */ -fun ProjectDependency.orArtifact(): Any { - return if (useArtifact) { - val artifact = ArtifactConfig.of(dependencyProject) - artifact.toString() - } else { - this - } -} +fun ProjectDependency.orArtifact(): Any = + if (useArtifact) ArtifactConfig.of(dependencyProject).toString() + else this /** * [useArtifact]가 false일 때만 주어진 [block]을 실행합니다. diff --git a/build-logic/src/main/kotlin/QuackMavenPublishingPlugin.kt b/build-logic/src/main/kotlin/QuackMavenPublishingPlugin.kt index aa0aa7c78..1f8039b7a 100644 --- a/build-logic/src/main/kotlin/QuackMavenPublishingPlugin.kt +++ b/build-logic/src/main/kotlin/QuackMavenPublishingPlugin.kt @@ -35,7 +35,7 @@ class QuackMavenPublishingPlugin : Plugin { } val (group, module, version) = ArtifactConfig.of(this).also { artifact -> - logger.warn("Publishing $artifact...") + logger.warn("Loading $artifact...") } extensions.configure { diff --git a/build-logic/src/main/kotlin/build-logics.kt b/build-logic/src/main/kotlin/build-logics.kt index 5434852fd..e392100dc 100644 --- a/build-logic/src/main/kotlin/build-logics.kt +++ b/build-logic/src/main/kotlin/build-logics.kt @@ -20,6 +20,8 @@ import internal.libs import internal.androidExtensions import internal.isAndroidProject import internal.setupJunit +import java.io.File +import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension @@ -74,15 +76,6 @@ internal class AndroidGmdPlugin : BuildLogicPlugin({ configureGmd(androidExtensions) }) -internal class AndroidLintPlugin : BuildLogicPlugin({ - applyPlugins(Plugins.AndroidLint) - - dependencies { - add("compileOnly", libs.findBundle("android-lint").get()) - add("testImplementation", libs.findBundle("test-android-lint").get()) - } -}) - internal class AndroidComposePlugin : BuildLogicPlugin({ configureCompose(androidExtensions) }) @@ -122,8 +115,6 @@ internal class JvmKotlinPlugin : BuildLogicPlugin({ dependencies.add("detektPlugins", libs.findLibrary("detekt-plugin-formatting").get()) }) -// prefix가 `Jvm`이 아니라 `Test`인 이유: -// 적용 타켓(android or pure)에 따라 `useJUnitPlatform()` 방식이 달라짐 internal class TestJUnitPlugin : BuildLogicPlugin({ useTestPlatformForTarget() dependencies { @@ -133,16 +124,57 @@ internal class TestJUnitPlugin : BuildLogicPlugin({ ) } }) + internal class TestKotestPlugin : BuildLogicPlugin({ useTestPlatformForTarget() dependencies.add("testImplementation", libs.findLibrary("test-kotest-framework").get()) }) +internal class TestRoborazziPlugin : BuildLogicPlugin({ + if (!isAndroidProject) throw GradleException("roborazzi only supports Android projects.") + + androidExtensions.testOptions { + unitTests { + isIncludeAndroidResources = true + isReturnDefaultValues = true + all { test -> + test.systemProperty("robolectric.graphicsMode", "NATIVE") + } + } + } + + val properties = File(projectDir, "src/test/resources/robolectric.properties") + if (!properties.exists()) { + properties.parentFile.mkdirs() + properties.createNewFile() + properties.writeText( + """ + # + # Designed and developed by Duckie Team 2023. + # + # Licensed under the MIT. + # Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + # + + sdk=33 + """.trimIndent(), + ) + } + + applyPlugins(libs.findPlugin("test-roborazzi").get().get().pluginId) + + dependencies.add("testImplementation", libs.findLibrary("test-robolectric").get()) + dependencies.add("testImplementation", libs.findLibrary("test-junit-compose").get()) + dependencies.add("testImplementation", libs.findLibrary("test-kotlin-coroutines").get()) // needed for compose-ui-test + dependencies.add("testImplementation", libs.findBundle("test-roborazzi").get()) + dependencies.add("testImplementation", project(":util-compose-snapshot-test")) +}) + internal class KotlinExplicitApiPlugin : BuildLogicPlugin({ tasks .matching { task -> task is KotlinCompile && - !task.name.contains("test", ignoreCase = true) + !task.name.contains("test", ignoreCase = true) } .configureEach { if (!project.hasProperty("kotlin.optOutExplicitApi")) { @@ -190,6 +222,7 @@ private fun Project.useTestPlatformForTarget() { } } } + tasks.withType().configureEach { setTestConfiguration() } diff --git a/build-logic/src/main/kotlin/internal/extensions.kt b/build-logic/src/main/kotlin/internal/extensions.kt index 85f05cdc9..118d9eb9d 100644 --- a/build-logic/src/main/kotlin/internal/extensions.kt +++ b/build-logic/src/main/kotlin/internal/extensions.kt @@ -12,12 +12,13 @@ package internal import org.gradle.api.Action import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.kotlin.dsl.NamedDomainObjectContainerScope import org.gradle.kotlin.dsl.getByType -internal val Project.libs +internal val Project.libs: VersionCatalog get() = extensions.getByType().named("libs") internal fun Project.applyPlugins(vararg plugins: String) { @@ -26,11 +27,10 @@ internal fun Project.applyPlugins(vararg plugins: String) { internal inline operator fun > C.invoke( configuration: Action>, -): C { - return apply { +) = + apply { configuration.execute(NamedDomainObjectContainerScope.of(this)) } -} internal inline fun DependencyHandler.setupJunit(core: Any, engine: Any) { add("testImplementation", core) diff --git a/build-logic/src/main/kotlin/plugins.kt b/build-logic/src/main/kotlin/plugins.kt index 01884ed82..fb3b3cf0d 100644 --- a/build-logic/src/main/kotlin/plugins.kt +++ b/build-logic/src/main/kotlin/plugins.kt @@ -13,5 +13,4 @@ internal object Plugins { const val AndroidApplication = "com.android.application" const val AndroidLibrary = "com.android.library" - const val AndroidLint = "com.android.lint" } diff --git a/build.gradle.kts b/build.gradle.kts index 00328db07..c81ee906b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,7 @@ plugins { alias(libs.plugins.kotlin.dokka) alias(libs.plugins.gradle.dependency.graph) alias(libs.plugins.gradle.dependency.handler.extensions) + alias(libs.plugins.test.roborazzi) apply false idea } @@ -116,6 +117,6 @@ allprojects { } } -tasks.register(name = "cleanAll", type = Delete::class) { +tasks.register("cleanAll", type = Delete::class) { allprojects.map(Project::getBuildDir).forEach(::delete) } diff --git a/buildSrc/src/main/kotlin/plugin.kt b/buildSrc/src/main/kotlin/plugin.kt index 8409baf9c..f1332aee4 100644 --- a/buildSrc/src/main/kotlin/plugin.kt +++ b/buildSrc/src/main/kotlin/plugin.kt @@ -5,10 +5,13 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:Suppress("NOTHING_TO_INLINE") + import org.gradle.plugin.use.PluginDependenciesSpec import org.gradle.plugin.use.PluginDependencySpec -@Suppress("NOTHING_TO_INLINE") -inline fun PluginDependenciesSpec.quackquack(pluginName: String): PluginDependencySpec { - return id("quackquack.plugin.$pluginName") -} +inline fun PluginDependenciesSpec.quackquack(pluginName: String): PluginDependencySpec = + id("quackquack.plugin.$pluginName") + +inline fun PluginDependenciesSpec.android(pluginId: String): PluginDependencySpec = + id("com.android.$pluginId") diff --git a/material-icon/build.gradle.kts b/material-icon/build.gradle.kts index e4be5184e..cbefbcf94 100644 --- a/material-icon/build.gradle.kts +++ b/material-icon/build.gradle.kts @@ -12,37 +12,19 @@ plugins { quackquack("android-compose") quackquack("kotlin-explicit-api") quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") alias(libs.plugins.test.roborazzi) } android { namespace = "team.duckie.quackquack.material.icon" - - defaultConfig { - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } - } - } } dependencies { implementation(libs.compose.ui.core) - testImplementations( libs.compose.foundation, - libs.test.robolectric, - libs.test.junit.compose, libs.test.kotest.assertion.core, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.bundles.test.roborazzi, ) } diff --git a/material-icon/src/test/kotlin/team/duckie/quackquack/material/icon/QuackIconSnapshot.kt b/material-icon/src/test/kotlin/team/duckie/quackquack/material/icon/snapshot/QuackIconSnapshot.kt similarity index 79% rename from material-icon/src/test/kotlin/team/duckie/quackquack/material/icon/QuackIconSnapshot.kt rename to material-icon/src/test/kotlin/team/duckie/quackquack/material/icon/snapshot/QuackIconSnapshot.kt index e5a53e95e..48dd54ed5 100644 --- a/material-icon/src/test/kotlin/team/duckie/quackquack/material/icon/QuackIconSnapshot.kt +++ b/material-icon/src/test/kotlin/team/duckie/quackquack/material/icon/snapshot/QuackIconSnapshot.kt @@ -7,7 +7,7 @@ @file:OptIn(ExperimentalLayoutApi::class) -package team.duckie.quackquack.material.icon +package team.duckie.quackquack.material.icon.snapshot import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,16 +24,23 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config +import team.duckie.quackquack.material.icon.AllIcons +import team.duckie.quackquack.material.icon.QuackIcon +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule +@Config(qualifiers = RobolectricDeviceQualifiers.NexusOne) @RunWith(AndroidJUnit4::class) class QuackIconSnapshot { - @Config(qualifiers = RobolectricDeviceQualifiers.NexusOne) + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("icon") + @Test fun icons() { - captureRoboImage("src/test/snapshots/icons.png") { + captureRoboImage(snapshotPath()) { FlowRow( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy( diff --git a/material-icon/src/test/snapshots/icons.png b/material-icon/src/test/snapshots/icon/icons.png similarity index 100% rename from material-icon/src/test/snapshots/icons.png rename to material-icon/src/test/snapshots/icon/icons.png diff --git a/material/build.gradle.kts b/material/build.gradle.kts index bfb39cfcf..3d9f775d0 100644 --- a/material/build.gradle.kts +++ b/material/build.gradle.kts @@ -5,31 +5,19 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -@file:Suppress("INLINE_FROM_HIGHER_PLATFORM", "UnstableApiUsage") - plugins { quackquack("android-library") quackquack("android-compose") quackquack("kotlin-explicit-api") quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") alias(libs.plugins.kotlin.dataclass.nocopy) - alias(libs.plugins.test.roborazzi) } android { namespace = "team.duckie.quackquack.material" resourcePrefix = "quack_" - - testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } - } - } } dependencies { @@ -43,12 +31,5 @@ dependencies { libs.androidx.core.ktx, // needed for androidx.core.graphics (used in SquircleShape) projects.util.orArtifact(), ) - - testImplementations( - libs.test.robolectric, - libs.test.junit.compose, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.test.kotest.assertion.core, - libs.bundles.test.roborazzi, - ) + testImplementation(libs.test.kotest.assertion.core) } diff --git a/material/src/main/kotlin/team/duckie/quackquack/material/border.kt b/material/src/main/kotlin/team/duckie/quackquack/material/border.kt index 02e47c32b..e99379c5f 100644 --- a/material/src/main/kotlin/team/duckie/quackquack/material/border.kt +++ b/material/src/main/kotlin/team/duckie/quackquack/material/border.kt @@ -42,9 +42,8 @@ public class QuackBorder( /** [QuackBorder] 를 [BorderStroke]로 변환합니다. */ @Stable - public fun asComposeBorder(): BorderStroke { - return BorderStroke(width = thickness, brush = brush) - } + public fun asComposeBorder(): BorderStroke = + BorderStroke(width = thickness, brush = brush) } /** @@ -59,17 +58,18 @@ public class QuackBorder( public fun Modifier.quackBorder( border: QuackBorder?, shape: Shape = RectangleShape, -): Modifier = applyIf(border != null) { - inspectable( - inspectorInfo = debugInspectorInfo { - name = "quackBorder" - properties["border"] = border - properties["shape"] = shape - }, - ) { - border( - border = border!!.asComposeBorder(), - shape = shape, - ) +): Modifier = + applyIf(border != null) { + inspectable( + inspectorInfo = debugInspectorInfo { + name = "quackBorder" + properties["border"] = border + properties["shape"] = shape + }, + ) { + border( + border = border!!.asComposeBorder(), + shape = shape, + ) + } } -} diff --git a/material/src/main/kotlin/team/duckie/quackquack/material/color.kt b/material/src/main/kotlin/team/duckie/quackquack/material/color.kt index 2799c2edd..22d00dff0 100644 --- a/material/src/main/kotlin/team/duckie/quackquack/material/color.kt +++ b/material/src/main/kotlin/team/duckie/quackquack/material/color.kt @@ -50,13 +50,9 @@ public value class QuackColor(public val value: Color) : ReadOnlyProperty Unit)? = null, -): Modifier = inspectable( - inspectorInfo = debugInspectorInfo { - name = "quackSurface" - properties["shape"] = shape - properties["backgroundColor"] = backgroundColor - properties["border"] = border - properties["elevation"] = elevation - properties["role"] = role - properties["rippleEnabled"] = rippleEnabled - properties["rippleColor"] = rippleColor - properties["onClick"] = onClick - }, -) { - this - .shadow( - elevation = elevation, - shape = shape, - clip = false, - ) - .clip(shape = shape) - .background( - color = backgroundColor.value, - shape = shape, - ) - .quackClickable( - role = role, - rippleEnabled = rippleEnabled, - rippleColor = rippleColor, - onClick = onClick, - ) - .quackBorder( - border = border, - shape = shape, - ) -} +): Modifier = + inspectable( + inspectorInfo = debugInspectorInfo { + name = "quackSurface" + properties["shape"] = shape + properties["backgroundColor"] = backgroundColor + properties["border"] = border + properties["elevation"] = elevation + properties["role"] = role + properties["rippleEnabled"] = rippleEnabled + properties["rippleColor"] = rippleColor + properties["onClick"] = onClick + }, + ) { + this + .shadow( + elevation = elevation, + shape = shape, + clip = false, + ) + .clip(shape = shape) + .background( + color = backgroundColor.value, + shape = shape, + ) + .quackClickable( + role = role, + rippleEnabled = rippleEnabled, + rippleColor = rippleColor, + onClick = onClick, + ) + .quackBorder( + border = border, + shape = shape, + ) + } diff --git a/material/src/main/kotlin/team/duckie/quackquack/material/padding.kt b/material/src/main/kotlin/team/duckie/quackquack/material/padding.kt index 2ad44bcd2..4a1ff2470 100644 --- a/material/src/main/kotlin/team/duckie/quackquack/material/padding.kt +++ b/material/src/main/kotlin/team/duckie/quackquack/material/padding.kt @@ -37,18 +37,16 @@ public value class QuackPadding internal constructor(@PublishedApi internal val /** [QuackPadding] 값을 [PaddingValues]로 변환합니다. */ @Stable - public fun asPaddingValues(): PaddingValues { - return PaddingValues(horizontal = horizontal, vertical = vertical) - } + public fun asPaddingValues(): PaddingValues = + PaddingValues(horizontal = horizontal, vertical = vertical) /** [QuackPadding]의 일부 값을 변경하여 반환합니다. */ @Stable public fun copy( horizontal: Dp = this.horizontal, vertical: Dp = this.vertical, - ): QuackPadding { - return QuackPadding(horizontal = horizontal, vertical = vertical) - } + ): QuackPadding = + QuackPadding(horizontal = horizontal, vertical = vertical) } /** @@ -58,6 +56,5 @@ public value class QuackPadding internal constructor(@PublishedApi internal val * @param vertical 위쪽과 아래쪽 가장자리에 적용될 패딩 값 */ @Stable -public fun QuackPadding(horizontal: Dp = 0.dp, vertical: Dp = 0.dp): QuackPadding { - return QuackPadding(packFloats(horizontal.value, vertical.value)) -} +public fun QuackPadding(horizontal: Dp = 0.dp, vertical: Dp = 0.dp): QuackPadding = + QuackPadding(packFloats(horizontal.value, vertical.value)) diff --git a/material/src/main/kotlin/team/duckie/quackquack/material/shape/SquircleShape.kt b/material/src/main/kotlin/team/duckie/quackquack/material/shape/SquircleShape.kt index fe3098851..447642926 100644 --- a/material/src/main/kotlin/team/duckie/quackquack/material/shape/SquircleShape.kt +++ b/material/src/main/kotlin/team/duckie/quackquack/material/shape/SquircleShape.kt @@ -35,20 +35,19 @@ public object SquircleShape : Shape { size: Size, layoutDirection: LayoutDirection, density: Density, - ): Outline.Generic { - return Outline.Generic( + ): Outline.Generic = + Outline.Generic( path = createSquirclePath( size = size, smoothing = SMOOTHING, ), ) - } private fun createSquirclePath( size: Size, smoothing: Double, - ): androidx.compose.ui.graphics.Path { - return Path().apply { + ): androidx.compose.ui.graphics.Path = + Path().apply { val oversize = size.width * OVERSAMPLING_MULTIPLIER val squircleRadius = (oversize / 2F).toInt() @@ -87,7 +86,6 @@ public object SquircleShape : Shape { ), ) }.asComposePath() - } private fun evalSquircleFun(x: Int, poweredRadius: Double, smoothing: Double) = (poweredRadius - abs(x.toDouble().pow(smoothing))).pow(1 / smoothing).toFloat() diff --git a/material/src/main/kotlin/team/duckie/quackquack/material/theme/theme.kt b/material/src/main/kotlin/team/duckie/quackquack/material/theme/theme.kt index f968057d4..b5ce9b265 100644 --- a/material/src/main/kotlin/team/duckie/quackquack/material/theme/theme.kt +++ b/material/src/main/kotlin/team/duckie/quackquack/material/theme/theme.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.staticCompositionLocalOf import team.duckie.quackquack.material.QuackColor +import team.duckie.quackquack.ui.plugin.EmptyQuackPlugins import team.duckie.quackquack.ui.plugin.LocalQuackPlugins import team.duckie.quackquack.ui.plugin.QuackPlugins import team.duckie.quackquack.ui.plugin.rememberQuackPlugins @@ -31,28 +32,6 @@ import team.duckie.quackquack.ui.plugin.rememberQuackPlugins public val LocalQuackTextFieldTheme: ProvidableCompositionLocal = staticCompositionLocalOf { DefaultTextFieldTheme } -/** - * 꽥꽥에서 사용하는 컴포저블 테마를 제공합니다. - * 이 테마에서는 다음과 같은 작업을 진행합니다. - * - * 1. OverscrollEffect 제거 - * 2. 꽥꽥 컴포넌트에서 사용할 커서(cursor) 테마 제공 - * - * @param content 꽥꽥 디자인에 맞게 표시할 컴포저블 컨텐츠 - */ -@Composable -public fun QuackTheme(content: @Composable () -> Unit) { - CompositionLocalProvider( - LocalOverscrollConfiguration provides null, - LocalTextSelectionColors provides TextSelectionColors( - handleColor = QuackColor.DuckieOrange.value, - backgroundColor = QuackColor.DuckieOrange.change(alpha = 0.2f).value, - ), - LocalQuackTextFieldTheme provides DefaultTextFieldTheme, - content = content, - ) -} - /** * 꽥꽥에서 사용하는 컴포저블 테마를 제공합니다. * 이 테마에서는 다음과 같은 작업을 진행합니다. @@ -65,7 +44,7 @@ public fun QuackTheme(content: @Composable () -> Unit) { * @param content 꽥꽥 디자인에 맞게 표시할 컴포저블 컨텐츠 */ @Composable -public fun QuackTheme(plugins: QuackPlugins, content: @Composable () -> Unit) { +public fun QuackTheme(plugins: QuackPlugins = EmptyQuackPlugins, content: @Composable () -> Unit) { CompositionLocalProvider( LocalOverscrollConfiguration provides null, LocalTextSelectionColors provides TextSelectionColors( diff --git a/material/src/main/kotlin/team/duckie/quackquack/material/typography.kt b/material/src/main/kotlin/team/duckie/quackquack/material/typography.kt index 7e6c0a423..7e4e5b8b7 100644 --- a/material/src/main/kotlin/team/duckie/quackquack/material/typography.kt +++ b/material/src/main/kotlin/team/duckie/quackquack/material/typography.kt @@ -56,8 +56,8 @@ public data class QuackTypography( ) { /** [QuackTypography]을 컴포즈 Text 컴포넌트에 사용하기 위해 [TextStyle]로 변환합니다. */ @Stable - public fun asComposeStyle(): TextStyle { - return TextStyle( + public fun asComposeStyle(): TextStyle = + TextStyle( color = color.value, fontSize = size, fontFamily = fontFamily, @@ -66,7 +66,6 @@ public data class QuackTypography( textAlign = textAlign, lineHeight = lineHeight, ) - } /** * 정해진 [QuackTypography]에서 일부 값만 변경이 필요할 때가 있습니다. 이를 대응하기 위해 @@ -86,10 +85,9 @@ public data class QuackTypography( public fun change( color: QuackColor = this.color, textAlign: TextAlign = this.textAlign, - ): QuackTypography { - return if (color == this.color && textAlign == this.textAlign) { - this - } else { + ): QuackTypography = + if (color == this.color && textAlign == this.textAlign) this + else QuackTypography( color = color, size = size, @@ -98,8 +96,6 @@ public data class QuackTypography( lineHeight = lineHeight, textAlign = textAlign, ) - } - } public companion object { @Stable @@ -206,7 +202,3 @@ public inline val FontWeight.Companion.Regular: FontWeight get() = Normal @Suppress("NOTHING_TO_INLINE") @Stable public inline fun Float.toSp(): TextUnit = TextUnit(value = this, type = TextUnitType.Sp) - -/** [List]의 component 정의가 5까지만 있어서 6번째 component를 추가로 정의합니다. */ -@Suppress("MagicNumber") -public operator fun List.component6(): T = get(index = 5) diff --git a/material/src/main/res/README.md b/material/src/main/res/README.md deleted file mode 100644 index 16e16f492..000000000 --- a/material/src/main/res/README.md +++ /dev/null @@ -1,4 +0,0 @@ -### 아이콘 주의 사항 - -- 모든 아이콘의 선 색은 `#000` 이 아닌, 디자인에 맞는 `#222` 를 사용합니다. -- 모든 아이콘은 `24dp x 24dp` 크기를 가집니다. 기본 스펙이 16dp인 아이콘도 다른 아이콘과 통일을 위해 24dp로 override 됩니다. diff --git a/material/src/test/kotlin/team/duckie/quackquack/material/TypographySnapshot.kt b/material/src/test/kotlin/team/duckie/quackquack/material/snapshot/TypographySnapshot.kt similarity index 52% rename from material/src/test/kotlin/team/duckie/quackquack/material/TypographySnapshot.kt rename to material/src/test/kotlin/team/duckie/quackquack/material/snapshot/TypographySnapshot.kt index 860202784..ef0fcd408 100644 --- a/material/src/test/kotlin/team/duckie/quackquack/material/TypographySnapshot.kt +++ b/material/src/test/kotlin/team/duckie/quackquack/material/snapshot/TypographySnapshot.kt @@ -5,39 +5,48 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.material +package team.duckie.quackquack.material.snapshot -import android.app.Activity import android.widget.TextView import androidx.compose.foundation.text.BasicText +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.res.ResourcesCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.robolectric.Robolectric -import org.robolectric.android.controller.ActivityController +import team.duckie.quackquack.material.QuackTypography +import team.duckie.quackquack.material.R +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule +import team.duckie.quackquack.util.compose.snapshot.test.getActivityViaRobolectric +private const val TestMessage = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?" + +// FIXME: Part of #761. @RunWith(AndroidJUnit4::class) class TypographySnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("typography") + @Test - fun NoBreakText_Compose() { - captureRoboImage("src/test/snapshots/NoBreakText_Compose.png") { + fun NoBreakText_Compose_BasicText() { + captureRoboImage(snapshotPath()) { BasicText( - text = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?", - style = QuackTypography.Body1.asComposeStyle(), + text = TestMessage, + style = TextStyle(fontFamily = QuackTypography.Body1.fontFamily), ) } } @Test - fun NoBreakText_AndroidView() { - captureRoboImage("src/test/snapshots/NoBreakText_AndroidView.png") { + fun NoBreakText_Compose_AndroidView() { + captureRoboImage(snapshotPath()) { AndroidView( factory = { context -> TextView(context).apply { - text = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?" + text = TestMessage val typeface = ResourcesCompat.getFont(context, R.font.quack_suit_medium)!! setTypeface(typeface) } @@ -47,18 +56,11 @@ class TypographySnapshot { } @Test - fun NoBreakText_TextView() { + fun NoBreakText_View_TextView() { TextView(getActivityViaRobolectric).apply { - text = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?" + text = TestMessage val typeface = ResourcesCompat.getFont(context, R.font.quack_suit_medium)!! setTypeface(typeface) - }.captureRoboImage("src/test/snapshots/NoBreakText_TextView.png") + }.captureRoboImage(snapshotPath()) } } - -private val getActivityViaRobolectric: Activity - inline get() { - val activityController = Robolectric.buildActivity(Activity::class.java) - .also(ActivityController::setup) - return activityController.get() - } diff --git a/material/src/test/kotlin/team/duckie/quackquack/material/PaddingTest.kt b/material/src/test/kotlin/team/duckie/quackquack/material/unittest/PaddingTest.kt similarity index 66% rename from material/src/test/kotlin/team/duckie/quackquack/material/PaddingTest.kt rename to material/src/test/kotlin/team/duckie/quackquack/material/unittest/PaddingTest.kt index db4920366..854490543 100644 --- a/material/src/test/kotlin/team/duckie/quackquack/material/PaddingTest.kt +++ b/material/src/test/kotlin/team/duckie/quackquack/material/unittest/PaddingTest.kt @@ -5,11 +5,19 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.material +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.material.unittest import androidx.compose.ui.unit.dp import io.kotest.matchers.shouldBe import org.junit.Test +import team.duckie.quackquack.material.QuackPadding class PaddingTest { @Test diff --git a/material/src/test/snapshots/NoBreakText_Compose.png b/material/src/test/snapshots/NoBreakText_Compose.png deleted file mode 100644 index 8d1de8be3..000000000 Binary files a/material/src/test/snapshots/NoBreakText_Compose.png and /dev/null differ diff --git a/material/src/test/snapshots/NoBreakText_AndroidView.png b/material/src/test/snapshots/typography/NoBreakText_Compose_AndroidView.png similarity index 100% rename from material/src/test/snapshots/NoBreakText_AndroidView.png rename to material/src/test/snapshots/typography/NoBreakText_Compose_AndroidView.png diff --git a/material/src/test/snapshots/typography/NoBreakText_Compose_BasicText.png b/material/src/test/snapshots/typography/NoBreakText_Compose_BasicText.png new file mode 100644 index 000000000..145051838 Binary files /dev/null and b/material/src/test/snapshots/typography/NoBreakText_Compose_BasicText.png differ diff --git a/material/src/test/snapshots/NoBreakText_TextView.png b/material/src/test/snapshots/typography/NoBreakText_View_TextView.png similarity index 100% rename from material/src/test/snapshots/NoBreakText_TextView.png rename to material/src/test/snapshots/typography/NoBreakText_View_TextView.png diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index c5798ca3c..2855de464 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -35,5 +35,7 @@ dependencies { libs.test.kotlin.coroutines, libs.test.mockito.core, libs.test.mockito.kotlin, + projects.utilModifier, + projects.utilComposeRuntimeTest, ) } diff --git a/runtime/src/main/kotlin/team/duckie/quackquack/runtime/ComposedModifier.kt b/runtime/src/main/kotlin/team/duckie/quackquack/runtime/ComposedModifier.kt index 8d8a72e0f..0bbb25a81 100644 --- a/runtime/src/main/kotlin/team/duckie/quackquack/runtime/ComposedModifier.kt +++ b/runtime/src/main/kotlin/team/duckie/quackquack/runtime/ComposedModifier.kt @@ -22,13 +22,18 @@ private class KeyedQuackComposedModifier1( factory: @Composable Modifier.() -> Modifier, ) : QuackComposedModifier(factory) { override fun equals(other: Any?): Boolean { - return other is KeyedQuackComposedModifier1 && - fqName == other.fqName && - key1 == other.key1 + if (this === other) return true + if (other !is KeyedQuackComposedModifier1) return false + + if (fqName != other.fqName) return false + + return key1 == other.key1 } override fun hashCode(): Int { - return 31 * fqName.hashCode() + key1.hashCode() + var result = fqName.hashCode() + result = 31 * result + (key1?.hashCode() ?: 0) + return result } } @@ -40,16 +45,19 @@ private class KeyedQuackComposedModifier2( factory: @Composable Modifier.() -> Modifier, ) : QuackComposedModifier(factory) { override fun equals(other: Any?): Boolean { - return other is KeyedQuackComposedModifier2 && - fqName == other.fqName && - key1 == other.key1 && - key2 == other.key2 + if (this === other) return true + if (other !is KeyedQuackComposedModifier2) return false + + if (fqName != other.fqName) return false + if (key1 != other.key1) return false + + return key2 == other.key2 } override fun hashCode(): Int { var result = fqName.hashCode() - result = 31 * result + key1.hashCode() - result = 31 * result + key2.hashCode() + result = 31 * result + (key1?.hashCode() ?: 0) + result = 31 * result + (key2?.hashCode() ?: 0) return result } } @@ -63,18 +71,21 @@ private class KeyedQuackComposedModifier3( factory: @Composable Modifier.() -> Modifier, ) : QuackComposedModifier(factory) { override fun equals(other: Any?): Boolean { - return other is KeyedQuackComposedModifier3 && - fqName == other.fqName && - key1 == other.key1 && - key2 == other.key2 && - key3 == other.key3 + if (this === other) return true + if (other !is KeyedQuackComposedModifier3) return false + + if (fqName != other.fqName) return false + if (key1 != other.key1) return false + if (key2 != other.key2) return false + + return key3 == other.key3 } override fun hashCode(): Int { var result = fqName.hashCode() - result = 31 * result + key1.hashCode() - result = 31 * result + key2.hashCode() - result = 31 * result + key3.hashCode() + result = 31 * result + (key1?.hashCode() ?: 0) + result = 31 * result + (key2?.hashCode() ?: 0) + result = 31 * result + (key3?.hashCode() ?: 0) return result } } @@ -86,13 +97,18 @@ private class KeyedQuackComposedModifierN( factory: @Composable Modifier.() -> Modifier, ) : QuackComposedModifier(factory) { override fun equals(other: Any?): Boolean { - return other is KeyedQuackComposedModifierN && - fqName == other.fqName && - keys.contentEquals(other.keys) + if (this === other) return true + if (other !is KeyedQuackComposedModifierN) return false + + if (fqName != other.fqName) return false + + return keys.contentEquals(other.keys) } override fun hashCode(): Int { - return 31 * fqName.hashCode() + keys.contentHashCode() + var result = fqName.hashCode() + result = 31 * result + keys.contentHashCode() + return result } } @@ -108,9 +124,8 @@ private class KeyedQuackComposedModifierN( * @see Composer.quackMaterializeOf */ @Stable -public fun Modifier.quackComposed(factory: @Composable Modifier.() -> Modifier): Modifier { - return then(QuackComposedModifier(factory)) -} +public fun Modifier.quackComposed(factory: @Composable Modifier.() -> Modifier): Modifier = + this then QuackComposedModifier(factory) /** * [Modifier.composed]의 꽥꽥 버전을 구현합니다. @@ -135,15 +150,13 @@ public fun Modifier.quackComposed( fullyQualifiedName: String, key1: Any?, factory: @Composable Modifier.() -> Modifier, -): Modifier { - return then( +): Modifier = + this then KeyedQuackComposedModifier1( fqName = fullyQualifiedName, key1 = key1, factory = factory, - ), - ) -} + ) /** * [Modifier.composed]의 꽥꽥 버전을 구현합니다. @@ -169,16 +182,14 @@ public fun Modifier.quackComposed( key1: Any?, key2: Any?, factory: @Composable Modifier.() -> Modifier, -): Modifier { - return then( +): Modifier = + this then KeyedQuackComposedModifier2( fqName = fullyQualifiedName, key1 = key1, key2 = key2, factory = factory, - ), - ) -} + ) /** * [Modifier.composed]의 꽥꽥 버전을 구현합니다. @@ -205,17 +216,15 @@ public fun Modifier.quackComposed( key2: Any?, key3: Any?, factory: @Composable Modifier.() -> Modifier, -): Modifier { - return then( +): Modifier = + this then KeyedQuackComposedModifier3( fqName = fullyQualifiedName, key1 = key1, key2 = key2, key3 = key3, factory = factory, - ), - ) -} + ) /** * [Modifier.composed]의 꽥꽥 버전을 구현합니다. @@ -240,12 +249,10 @@ public fun Modifier.quackComposed( fullyQualifiedName: String, vararg keys: Any?, factory: @Composable Modifier.() -> Modifier, -): Modifier { - return then( +): Modifier = + this then KeyedQuackComposedModifierN( fqName = fullyQualifiedName, keys = keys, factory = factory, - ), - ) -} + ) diff --git a/runtime/src/main/kotlin/team/duckie/quackquack/runtime/materializing.kt b/runtime/src/main/kotlin/team/duckie/quackquack/runtime/materializing.kt index d9a1719e7..a93bf3c9b 100644 --- a/runtime/src/main/kotlin/team/duckie/quackquack/runtime/materializing.kt +++ b/runtime/src/main/kotlin/team/duckie/quackquack/runtime/materializing.kt @@ -75,9 +75,8 @@ public fun Composer.quackMaterializeOf( is QuackComposedModifier -> { @Suppress("UNCHECKED_CAST") val factory = element.factory as Modifier.(Composer, Int) -> Modifier - val composed = factory.invoke(Modifier, this, 0) as? QuackDataModifierModel ?: error( - message = QuackMaterializingErrors.MustProducesQuackDataModel, - ) + val composed = factory.invoke(Modifier, this, 0) as? QuackDataModifierModel + ?: error(message = QuackMaterializingErrors.MustProducesQuackDataModel) quackDataModels += composed acc } diff --git a/runtime/src/main/kotlin/team/duckie/quackquack/runtime/model.kt b/runtime/src/main/kotlin/team/duckie/quackquack/runtime/model.kt index 093484a20..c249ec67f 100644 --- a/runtime/src/main/kotlin/team/duckie/quackquack/runtime/model.kt +++ b/runtime/src/main/kotlin/team/duckie/quackquack/runtime/model.kt @@ -28,5 +28,6 @@ import androidx.compose.ui.Modifier * * @see quackMaterializeOf */ +// TODO: Modifier.Node() 마이그레이션 (#636) @Stable public interface QuackDataModifierModel : Modifier.Element diff --git a/runtime/src/test/kotlin/team/duckie/quackquack/runtime/MaterializingTest.kt b/runtime/src/test/kotlin/team/duckie/quackquack/runtime/MaterializingTest.kt index 0c9106d3e..a8590ecaf 100644 --- a/runtime/src/test/kotlin/team/duckie/quackquack/runtime/MaterializingTest.kt +++ b/runtime/src/test/kotlin/team/duckie/quackquack/runtime/MaterializingTest.kt @@ -5,31 +5,27 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:Suppress("UnnecessaryComposedModifier") + package team.duckie.quackquack.runtime -import androidx.compose.runtime.Applier -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Composition -import androidx.compose.runtime.MonotonicFrameClock import androidx.compose.runtime.RecomposeScope -import androidx.compose.runtime.Recomposer import androidx.compose.runtime.currentComposer import androidx.compose.runtime.currentRecomposeScope -import androidx.compose.runtime.withRunningRecomposer import androidx.compose.ui.Modifier import androidx.compose.ui.composed import io.kotest.assertions.throwables.shouldThrowWithMessage import io.kotest.core.spec.style.StringSpec import io.kotest.core.test.Enabled -import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.collections.shouldHaveSingleElement import io.kotest.matchers.collections.shouldHaveSize -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.withContext import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify +import team.duckie.quackquack.runtime.QuackMaterializingErrors.MustProducesQuackDataModel +import team.duckie.quackquack.util.compose.runtime.test.TestMonotonicFrameClock +import team.duckie.quackquack.util.compose.runtime.test.composed +import team.duckie.quackquack.util.modifier.splitToList private object QuackElement : QuackDataModifierModel private object StdlibElement : Modifier.Element @@ -38,17 +34,6 @@ class MaterializingTest : StringSpec() { init { coroutineTestScope = true - "Modifier.Element 분리" { - val elements = listOf(StdlibElement, QuackElement) - var modifier: Modifier = Modifier - - elements.forEach { element -> - modifier = modifier.then(element) - } - - modifier.splitToList() shouldContainExactly elements - } - "stdlib-Modifier와 quack-Modifier가 분리돼야 함" { lateinit var stdlibModifiers: List lateinit var quackModifiers: List @@ -71,7 +56,6 @@ class MaterializingTest : StringSpec() { lateinit var stdlibModifiers: List lateinit var quackModifiers: List - @Suppress("UnnecessaryComposedModifier") val modifier = Modifier .composed { StdlibElement } .quackComposed { QuackElement } @@ -89,9 +73,7 @@ class MaterializingTest : StringSpec() { "quack-ComposedModifier가 nonquack한 값을 반환하면 ISE를 던짐" { val modifier = Modifier.quackComposed { this } - shouldThrowWithMessage( - message = QuackMaterializingErrors.MustProducesQuackDataModel, - ) { + shouldThrowWithMessage(MustProducesQuackDataModel) { composed { currentComposer.quackMaterializeOf(modifier) } @@ -140,11 +122,11 @@ class MaterializingTest : StringSpec() { enabledOrReasonIf = { Enabled.disabled( reason = """ - - asop에 있는 recomposingKeyedComposedModifierSkips 테스트를 로컬로 돌려보면 실패함 - [https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt;l=288-320;drc=5729d22fd521d7e83ec4eb8dedd34a0c2f491738] - - - 안정성 테스트 코드를 어떻게 작성해야 할지 모르겠음 - """, + - asop에 있는 recomposingKeyedComposedModifierSkips 테스트를 로컬로 돌려보면 실패함 + [https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt;l=288-320;drc=5729d22fd521d7e83ec4eb8dedd34a0c2f491738] + + - 안정성 테스트 코드를 어떻게 작성해야 할지 모르겠음 + """, ) }, ) { @@ -180,64 +162,3 @@ class MaterializingTest : StringSpec() { } } } - -private fun Modifier.splitToList(): List { - return foldIn(mutableListOf()) { acc, element -> - acc.apply { add(element) } - } -} - -private suspend fun composed( - coroutineContext: CoroutineContext = TestMonotonicFrameClock(), - withRecomposer: suspend (recomposer: Recomposer) -> Unit = {}, - withinCompositionContent: @Composable (composition: Composition) -> Unit, -) { - withContext(coroutineContext) { - withRunningRecomposer { recomposer -> - val compositon = Composition( - applier = EmptyApplier, - parent = recomposer, - ) - compositon.setContent { - withinCompositionContent(compositon) - } - withRecomposer(recomposer) - } - } -} - -@Suppress("EmptyFunctionBlock") -private object EmptyApplier : Applier { - override val current = Unit - override fun down(node: Unit) {} - override fun up() {} - override fun clear() {} - - override fun insertTopDown(index: Int, instance: Unit) { - error("Unexpected") - } - - override fun insertBottomUp(index: Int, instance: Unit) { - error("Unexpected") - } - - override fun remove(index: Int, count: Int) { - error("Unexpected") - } - - override fun move(from: Int, to: Int, count: Int) { - error("Unexpected") - } -} - -private class TestMonotonicFrameClock : MonotonicFrameClock { - private val frameChannel = Channel() - - suspend fun advance(frameTimeNanos: Long) { - frameChannel.send(frameTimeNanos) - } - - override suspend fun withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R { - return onFrame(frameChannel.receive()) - } -} diff --git a/ui-plugin/image/build.gradle.kts b/ui-plugin/image/build.gradle.kts index 1122393c8..2ec4dd993 100644 --- a/ui-plugin/image/build.gradle.kts +++ b/ui-plugin/image/build.gradle.kts @@ -14,6 +14,7 @@ plugins { quackquack("android-compose") quackquack("kotlin-explicit-api") quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") alias(libs.plugins.test.roborazzi) } @@ -24,16 +25,6 @@ tasks.withType { android { namespace = "team.duckie.quackquack.ui.plugin.image" - - testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } - } - } } dependencies { @@ -44,11 +35,7 @@ dependencies { ) testImplementations( libs.coil.test, - libs.test.robolectric, - libs.test.junit.compose, libs.test.kotest.assertion.core, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.bundles.test.roborazzi, projects.ui, projects.utilComposeRuntimeTest, ) diff --git a/ui-plugin/image/gif/build.gradle.kts b/ui-plugin/image/gif/build.gradle.kts index b0dacb53c..99fd10032 100644 --- a/ui-plugin/image/gif/build.gradle.kts +++ b/ui-plugin/image/gif/build.gradle.kts @@ -12,6 +12,7 @@ plugins { quackquack("android-compose") quackquack("kotlin-explicit-api") quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") alias(libs.plugins.test.roborazzi) } @@ -29,16 +30,6 @@ android { sourceSets { getByName("test").resources.srcDir("src/test/assets") } - - testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } - } - } } dependencies { @@ -47,11 +38,5 @@ dependencies { libs.compose.runtime, libs.coil.gif, ) - testImplementations( - libs.test.robolectric, - libs.test.junit.compose, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.bundles.test.roborazzi, - projects.ui, - ) + testImplementation(projects.ui) } diff --git a/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginSnapshot.kt b/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/snapshot/QuackImageGifPluginSnapshot.kt similarity index 68% rename from ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginSnapshot.kt rename to ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/snapshot/QuackImageGifPluginSnapshot.kt index a82f809d9..5928adef0 100644 --- a/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginSnapshot.kt +++ b/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/snapshot/QuackImageGifPluginSnapshot.kt @@ -5,14 +5,13 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.ui.plugin.image.gif +package team.duckie.quackquack.ui.plugin.image.gif.snapshot import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.RoborazziRule -import java.io.File +import com.github.takahirom.roborazzi.captureRoboImage import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -20,39 +19,34 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config import team.duckie.quackquack.material.theme.QuackTheme import team.duckie.quackquack.ui.QuackImage +import team.duckie.quackquack.ui.plugin.image.gif.QuackImageGifPlugin import team.duckie.quackquack.ui.plugin.rememberQuackPlugins +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotName +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule -@Ignore("GIF 녹화 안됨") +@Ignore("캡처가 정상적으로 안됨") @RunWith(AndroidJUnit4::class) class QuackImageGifPluginSnapshot { @get:Rule - val compose = createAndroidComposeRule() + val snapshotPath = SnapshotPathGeneratorRule("QuackImageGifPlugin") @get:Rule - val roborazzi = RoborazziRule( - composeRule = compose, - captureRoot = compose.onRoot(), - options = RoborazziRule.Options( - captureType = RoborazziRule.CaptureType.Gif, - outputFileProvider = { description, _, fileExtension -> - val sdkLevel = description.methodName.split("sdk").last() - File("src/test/snapshots/QuackImageGifPlugin/rainbow-sdk$sdkLevel.$fileExtension").also { file -> - file.parentFile!!.mkdirs() - } - }, - ), - ) + val compose = createAndroidComposeRule() + @SnapshotName("sdk-28") @Config(sdk = [28]) @Test fun `QuackImageGifPlugin gif snapshot sdk28`() { `QuackImageGifPlugin gif snapshot`() + compose.onRoot().captureRoboImage(snapshotPath()) } + @SnapshotName("sdk-23") @Config(sdk = [23]) @Test fun `QuackImageGifPlugin gif snapshot sdk23`() { `QuackImageGifPlugin gif snapshot`() + compose.onRoot().captureRoboImage(snapshotPath()) } private fun `QuackImageGifPlugin gif snapshot`() { diff --git a/ui-plugin/image/gif/src/test/resources/robolectric.properties b/ui-plugin/image/gif/src/test/resources/robolectric.properties new file mode 100644 index 000000000..17e6de451 --- /dev/null +++ b/ui-plugin/image/gif/src/test/resources/robolectric.properties @@ -0,0 +1,8 @@ +# +# Designed and developed by Duckie Team 2023. +# +# Licensed under the MIT. +# Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE +# + +sdk=33 \ No newline at end of file diff --git a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt deleted file mode 100644 index 0a8b8ecf6..000000000 --- a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(ExperimentalCoilApi::class) - -package team.duckie.quackquack.ui.plugin.image - -import android.content.Context -import android.content.res.Resources -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.ColorFilter -import android.graphics.Paint -import android.graphics.Paint.Align -import android.graphics.drawable.Drawable -import android.util.TypedValue -import androidx.activity.ComponentActivity -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import coil.ImageLoader -import coil.annotation.ExperimentalCoilApi -import coil.decode.DataSource -import coil.request.ImageRequest -import coil.request.SuccessResult -import coil.size.Size -import coil.test.FakeImageLoaderEngine -import com.github.takahirom.roborazzi.captureRoboImage -import io.kotest.matchers.maps.shouldMatchExactly -import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.kotest.matchers.types.shouldBeSameInstanceAs -import kotlin.math.roundToInt -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import team.duckie.quackquack.material.theme.QuackTheme -import team.duckie.quackquack.ui.QuackImage -import team.duckie.quackquack.ui.plugin.QuackPluginLocal -import team.duckie.quackquack.ui.plugin.quackPluginLocal -import team.duckie.quackquack.ui.plugin.rememberQuackPlugins - -@Suppress("OVERRIDE_DEPRECATION") -private class TextDrawable(private val res: Resources, private val text: String) : Drawable() { - private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.parseColor("#FFA500") // orange - textAlign = Align.CENTER - textSize = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_SP, - 13f, - res.displayMetrics, - ) - } - - override fun draw(canvas: Canvas) { - val x = intrinsicWidth / 2f - val y = intrinsicHeight / 2 - (paint.descent() + paint.ascent()) / 2 - - canvas.drawText(text, x, y, paint) - } - - override fun getOpacity() = paint.alpha - - override fun setAlpha(alpha: Int) { - paint.alpha = alpha - } - - override fun setColorFilter(colorFilter: ColorFilter?) { - paint.colorFilter = colorFilter - } - - override fun getIntrinsicWidth() = (paint.measureText(text, 0, text.length) + .5).roundToInt() - - override fun getIntrinsicHeight() = paint.getFontMetricsInt(null) - - fun setFontSize(fontSize: Float) { - paint.textSize = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_SP, - fontSize, - res.displayMetrics, - ) - } -} - -private fun imageResultOf(drawable: Drawable, request: ImageRequest) = - SuccessResult( - drawable = drawable, - request = request, - dataSource = DataSource.MEMORY, - ) - -private class QuackImageCoilBuilderIntercepter( - private val map: MutableMap = mutableMapOf(), -) : QuackImagePlugin.CoilImageLoader { - override fun ImageLoader.Builder.quackBuild( - context: Context, - src: Any?, - contentDescription: String?, - quackPluginLocal: QuackPluginLocal?, - ): ImageLoader.Builder = - components { - val testEngine = FakeImageLoaderEngine - .Builder() - .default { chain -> - val textDrawable = TextDrawable(context.resources, src.toString()).apply { - (quackPluginLocal?.value as? Float)?.let { fontSize -> - setFontSize(fontSize) - } - } - imageResultOf( - drawable = textDrawable, - request = chain - .withSize(Size(textDrawable.intrinsicWidth, textDrawable.intrinsicHeight)) - .request, - ) - } - .build() - add(testEngine) - } - .also { - map["context"] = context - map["src"] = src - map["contentDescription"] = contentDescription - map["quackPluginLocal"] = quackPluginLocal - } -} - -@RunWith(AndroidJUnit4::class) -class QuackImagePluginTest { - @get:Rule - val compose = createAndroidComposeRule() - - @Test - fun `QuackImage plugin snapshot`() { - captureRoboImage("src/test/snapshots/QuackImagePlugin/orange-string.png") { - QuackTheme( - plugins = rememberQuackPlugins { - +QuackImageCoilBuilderIntercepter() - }, - ) { - QuackImage( - modifier = Modifier.quackPluginLocal(30f), - src = "Hello, World!", - contentDescription = "orange string", - ) - } - } - } - - @Test - fun `QuackImage plugin intercept test`() { - val map = mutableMapOf() - - var context: Context? = null - val src = "Hello, World!" - val contentDescription = "orange string" - val fontSize = 30f - - compose.setContent { - context = LocalContext.current - - QuackTheme( - plugins = rememberQuackPlugins { - +QuackImageCoilBuilderIntercepter(map) - }, - ) { - QuackImage( - modifier = Modifier.quackPluginLocal(fontSize), - src = src, - contentDescription = contentDescription, - ) - } - } - - map.shouldMatchExactly( - "context" to { it shouldBeSameInstanceAs context }, - "src" to { it shouldBe src }, - "contentDescription" to { it shouldBe contentDescription }, - "quackPluginLocal" to { - it.shouldBeInstanceOf().value shouldBe fontSize - }, - ) - } -} diff --git a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/common/QuackImageCoilBuilderIntercepter.kt b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/common/QuackImageCoilBuilderIntercepter.kt new file mode 100644 index 000000000..86f5680bc --- /dev/null +++ b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/common/QuackImageCoilBuilderIntercepter.kt @@ -0,0 +1,121 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalCoilApi::class) +@file:Suppress("OVERRIDE_DEPRECATION") + +package team.duckie.quackquack.ui.plugin.image.common + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.util.TypedValue +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.decode.DataSource +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.size.Size +import coil.test.FakeImageLoaderEngine +import kotlin.math.roundToInt +import team.duckie.quackquack.ui.plugin.QuackPluginLocal +import team.duckie.quackquack.ui.plugin.image.QuackImagePlugin + +private class TextDrawable(private val res: Resources, private val text: String) : Drawable() { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#FFA500") // orange + textAlign = Paint.Align.CENTER + textSize = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + 13f, + res.displayMetrics, + ) + } + + override fun draw(canvas: Canvas) { + val x = intrinsicWidth / 2f + val y = intrinsicHeight / 2 - (paint.descent() + paint.ascent()) / 2 + canvas.drawText(text, x, y, paint) + } + + override fun getOpacity() = paint.alpha + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + override fun getIntrinsicWidth() = (paint.measureText(text, 0, text.length) + .5).roundToInt() + + override fun getIntrinsicHeight() = paint.getFontMetricsInt(null) + + fun setFontSize(fontSize: Float) { + paint.textSize = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + fontSize, + res.displayMetrics, + ) + } +} + +private fun imageResultOf(drawable: Drawable, request: ImageRequest) = + SuccessResult( + drawable = drawable, + request = request, + dataSource = DataSource.MEMORY, + ) + +class QuackImageCoilBuilderIntercepter( + private val map: MutableMap = mutableMapOf(), +) : QuackImagePlugin.CoilImageLoader { + override fun ImageLoader.Builder.quackBuild( + context: Context, + src: Any?, + contentDescription: String?, + quackPluginLocal: QuackPluginLocal?, + ): ImageLoader.Builder = + components { + val testEngine = + FakeImageLoaderEngine + .Builder() + .default { chain -> + val textDrawable = + TextDrawable(res = context.resources, text = src.toString()).apply { + (quackPluginLocal?.value as? Float)?.let { fontSize -> + setFontSize(fontSize) + } + } + imageResultOf( + drawable = textDrawable, + request = chain + .withSize( + Size( + width = textDrawable.intrinsicWidth, + height = textDrawable.intrinsicHeight, + ), + ) + .request, + ) + } + .build() + add(testEngine) + } + .also { + map["context"] = context + map["src"] = src + map["contentDescription"] = contentDescription + map["quackPluginLocal"] = quackPluginLocal + } +} diff --git a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/snapshot/QuackImagePluginSnapshot.kt b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/snapshot/QuackImagePluginSnapshot.kt new file mode 100644 index 000000000..6c8eb2fd5 --- /dev/null +++ b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/snapshot/QuackImagePluginSnapshot.kt @@ -0,0 +1,44 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.ui.plugin.image.snapshot + +import androidx.compose.ui.Modifier +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.material.theme.QuackTheme +import team.duckie.quackquack.ui.QuackImage +import team.duckie.quackquack.ui.plugin.image.common.QuackImageCoilBuilderIntercepter +import team.duckie.quackquack.ui.plugin.quackPluginLocal +import team.duckie.quackquack.ui.plugin.rememberQuackPlugins +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule + +@RunWith(AndroidJUnit4::class) +class QuackImagePluginSnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("QuackImagePlugin") + + @Test + fun OrangeStringIntercepted() { + captureRoboImage(snapshotPath()) { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackImageCoilBuilderIntercepter() + }, + ) { + QuackImage( + modifier = Modifier.quackPluginLocal(30f), + src = "Hello, World!", + contentDescription = "orange string", + ) + } + } + } +} diff --git a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/uitest/QuackImagePluginTest.kt b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/uitest/QuackImagePluginTest.kt new file mode 100644 index 000000000..6924f8ab1 --- /dev/null +++ b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/uitest/QuackImagePluginTest.kt @@ -0,0 +1,69 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.ui.plugin.image.uitest + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.kotest.matchers.maps.shouldMatchExactly +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.matchers.types.shouldBeSameInstanceAs +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.material.theme.QuackTheme +import team.duckie.quackquack.ui.QuackImage +import team.duckie.quackquack.ui.plugin.QuackPluginLocal +import team.duckie.quackquack.ui.plugin.image.common.QuackImageCoilBuilderIntercepter +import team.duckie.quackquack.ui.plugin.quackPluginLocal +import team.duckie.quackquack.ui.plugin.rememberQuackPlugins + +@RunWith(AndroidJUnit4::class) +class QuackImagePluginTest { + @get:Rule + val compose = createAndroidComposeRule() + + @Test + fun `QuackImage plugin intercept test`() { + val map = mutableMapOf() + + var context: Context? = null + val src = "Hello, World!" + val contentDescription = "orange string" + val fontSize = 30f + + compose.setContent { + context = LocalContext.current + + QuackTheme( + plugins = rememberQuackPlugins { + +QuackImageCoilBuilderIntercepter(map) + }, + ) { + QuackImage( + modifier = Modifier.quackPluginLocal(fontSize), + src = src, + contentDescription = contentDescription, + ) + } + } + + map.shouldMatchExactly( + "context" to { it shouldBeSameInstanceAs context }, + "src" to { it shouldBe src }, + "contentDescription" to { it shouldBe contentDescription }, + "quackPluginLocal" to { + it.shouldBeInstanceOf().value shouldBe fontSize + }, + ) + } +} diff --git a/ui-plugin/image/src/test/snapshots/QuackImagePlugin/orange-string.png b/ui-plugin/image/src/test/snapshots/QuackImagePlugin/OrangeStringIntercepted.png similarity index 100% rename from ui-plugin/image/src/test/snapshots/QuackImagePlugin/orange-string.png rename to ui-plugin/image/src/test/snapshots/QuackImagePlugin/OrangeStringIntercepted.png diff --git a/ui-plugin/interceptor/build.gradle.kts b/ui-plugin/interceptor/build.gradle.kts index 5118130d8..f4956e89a 100644 --- a/ui-plugin/interceptor/build.gradle.kts +++ b/ui-plugin/interceptor/build.gradle.kts @@ -13,8 +13,9 @@ plugins { quackquack("android-library") quackquack("android-compose") quackquack("kotlin-explicit-api") + quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") - alias(libs.plugins.test.roborazzi) } tasks.withType { @@ -23,16 +24,6 @@ tasks.withType { android { namespace = "team.duckie.quackquack.ui.plugin.interceptor" - - testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } - } - } } dependencies { @@ -44,11 +35,7 @@ dependencies { projects.utilModifier, ) testImplementations( - libs.test.robolectric, - libs.test.junit.compose, libs.test.kotest.assertion.core, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.bundles.test.roborazzi, projects.ui, projects.utilComposeSnapshotTest, ) diff --git a/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt deleted file mode 100644 index b728416d8..000000000 --- a/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(ExperimentalQuackQuackApi::class) - -package team.duckie.quackquack.ui.plugin.interceptor - -import com.github.takahirom.roborazzi.RoborazziRule.Ignore as NoSnapshot -import androidx.activity.ComponentActivity -import androidx.compose.ui.Modifier -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.RoborazziRule -import io.kotest.assertions.throwables.shouldThrowWithMessage -import io.kotest.matchers.maps.shouldMatchExactly -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import team.duckie.quackquack.material.QuackColor -import team.duckie.quackquack.material.theme.QuackTheme -import team.duckie.quackquack.ui.QuackTag -import team.duckie.quackquack.ui.QuackTagStyle -import team.duckie.quackquack.ui.TagStyleMarker -import team.duckie.quackquack.ui.plugin.rememberQuackPlugins -import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi -import team.duckie.quackquack.util.compose.snapshot.test.SnapshotName -import team.duckie.quackquack.util.compose.snapshot.test.snapshotPath - -@RunWith(AndroidJUnit4::class) -class QuackInterceptorPluginTest { - @get:Rule - val compose = createAndroidComposeRule() - - @get:Rule - val roborazzi = RoborazziRule( - composeRule = compose, - captureRoot = compose.onRoot(), - options = RoborazziRule.Options( - outputFileProvider = { description, _, fileExtension -> - val snapshotName = description.getAnnotation(SnapshotName::class.java)?.name ?: description.methodName - snapshotPath( - domain = "QuackInterceptorPlugin", - snapshotName = snapshotName, - isGif = fileExtension == "gif", - ) - }, - ), - ) - - @SnapshotName("QuackTagRadiusStyleIntercepted") - @Test - fun `style intercept works fine`() { - val map = mutableMapOf() - - var interceptedStyle: QuackTagStyle? = null - val interceptedRadius = Int.MAX_VALUE.dp - - compose.setContent { - QuackTheme( - plugins = rememberQuackPlugins { - +QuackInterceptorPlugin.DesignToken { componentName, componentDesignToken, componentModifier, _ -> - map["componentName"] = componentName - map["componentDesignToken"] = componentDesignToken - map["componentModifier"] = componentModifier - - (if (componentName == "QuackTag") { - @Suppress("UNCHECKED_CAST") - componentDesignToken as QuackTagStyle - object : QuackTagStyle by componentDesignToken { - override val radius = interceptedRadius - override val colors = - componentDesignToken.colors.copy( - backgroundColor = QuackColor.Gray3, - contentColor = QuackColor.Black, - ) - } - } else { - componentDesignToken - }).also { intercepttedResult -> - @Suppress("UNCHECKED_CAST") - interceptedStyle = intercepttedResult as QuackTagStyle - } - } - }, - ) { - QuackTag( - text = "Intercepted Tag", - style = QuackTagStyle.Filled, - onClick = {}, - ) - } - } - - map.shouldMatchExactly( - "componentName" to { it shouldBe "QuackTag" }, - "componentDesignToken" to { it.toString() shouldBe QuackTagStyle.Filled.toString() }, - "componentModifier" to { it shouldBe Modifier }, - ) - interceptedStyle.shouldNotBeNull().radius shouldBe interceptedRadius - } - - @NoSnapshot - @Test - fun InterceptedStyleTypeExceptionMessage() { - shouldThrowWithMessage(InterceptedStyleTypeExceptionMessage) { - compose.setContent { - QuackTheme( - plugins = rememberQuackPlugins { - +QuackInterceptorPlugin.DesignToken { _, _, _, _ -> Unit } - }, - ) { - QuackTag( - text = "", - style = QuackTagStyle.Filled, - onClick = {}, - ) - } - } - } - } -} diff --git a/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/snapshot/QuackInterceptorPluginSnapshot.kt b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/snapshot/QuackInterceptorPluginSnapshot.kt new file mode 100644 index 000000000..a18ffb0c3 --- /dev/null +++ b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/snapshot/QuackInterceptorPluginSnapshot.kt @@ -0,0 +1,79 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalQuackQuackApi::class) +@file:Suppress("UNCHECKED_CAST") + +package team.duckie.quackquack.ui.plugin.interceptor.snapshot + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.material.QuackColor +import team.duckie.quackquack.material.theme.QuackTheme +import team.duckie.quackquack.ui.QuackTag +import team.duckie.quackquack.ui.QuackTagStyle +import team.duckie.quackquack.ui.TagStyleMarker +import team.duckie.quackquack.ui.plugin.interceptor.QuackInterceptorPlugin +import team.duckie.quackquack.ui.plugin.rememberQuackPlugins +import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule + +@RunWith(AndroidJUnit4::class) +class QuackInterceptorPluginSnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("QuackInterceptorPlugin") + + @get:Rule + val compose = createAndroidComposeRule() + + @Test + fun QuackTagBackgroundColorIntercepted() { + compose.setContent { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackInterceptorPlugin.DesignToken { componentName, componentDesignToken, componentModifier, _ -> + (if (componentName == "QuackTag") { + componentDesignToken as QuackTagStyle + object : QuackTagStyle by componentDesignToken { + override val radius = Int.MAX_VALUE.dp + override val colors = + componentDesignToken.colors.copy( + backgroundColor = QuackColor.Gray3, + contentColor = QuackColor.Black, + ) + } + } else { + componentDesignToken + }) + } + }, + ) { + QuackTag( + text = "Intercepted Tag", + style = QuackTagStyle.Filled, + onClick = {}, + ) + } + } + + compose.onRoot().captureRoboImage(snapshotPath()) + } +} diff --git a/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/uitest/QuackInterceptorPluginTest.kt b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/uitest/QuackInterceptorPluginTest.kt new file mode 100644 index 000000000..cb6cb210f --- /dev/null +++ b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/uitest/QuackInterceptorPluginTest.kt @@ -0,0 +1,76 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalQuackQuackApi::class) + +package team.duckie.quackquack.ui.plugin.interceptor.uitest + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.collections.shouldContainExactly +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.material.theme.QuackTheme +import team.duckie.quackquack.ui.QuackTag +import team.duckie.quackquack.ui.QuackTagStyle +import team.duckie.quackquack.ui.plugin.interceptor.InterceptedStyleTypeExceptionMessage +import team.duckie.quackquack.ui.plugin.interceptor.QuackInterceptorPlugin +import team.duckie.quackquack.ui.plugin.rememberQuackPlugins +import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi + +@RunWith(AndroidJUnit4::class) +class QuackInterceptorPluginTest { + @get:Rule + val compose = createAndroidComposeRule() + + @Test + fun InnerComponentsCanBeIntercepted() { + val componentNames = mutableListOf() + + compose.setContent { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackInterceptorPlugin.DesignToken { componentName, componentDesignToken, _, _ -> + componentNames += componentName + componentDesignToken + } + }, + ) { + QuackTag( + text = "", + style = QuackTagStyle.Filled, + onClick = {}, + ) + } + } + + // 맹글링 제거 후 assertion + componentNames.map { it.split("-").first() } shouldContainExactly listOf("QuackTag", "QuackText") + } + + @Test + fun InterceptedStyleTypeException() { + shouldThrowWithMessage(InterceptedStyleTypeExceptionMessage) { + compose.setContent { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackInterceptorPlugin.DesignToken { _, _, _, _ -> Unit } + }, + ) { + QuackTag( + text = "", + style = QuackTagStyle.Filled, + onClick = {}, + ) + } + } + } + } +} diff --git a/ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/style intercept works fine.png b/ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/QuackTagBackgroundColorIntercepted.png similarity index 100% rename from ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/style intercept works fine.png rename to ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/QuackTagBackgroundColorIntercepted.png diff --git a/ui-plugin/interceptor/textfield/build.gradle.kts b/ui-plugin/interceptor/textfield/build.gradle.kts index ba8123cb1..a2ac57a4c 100644 --- a/ui-plugin/interceptor/textfield/build.gradle.kts +++ b/ui-plugin/interceptor/textfield/build.gradle.kts @@ -14,6 +14,7 @@ plugins { quackquack("android-compose") quackquack("kotlin-explicit-api") quackquack("test-junit") + quackquack("test-roborazzi") quackquack("quack-publishing") alias(libs.plugins.test.roborazzi) } @@ -31,16 +32,6 @@ tasks.withType { android { namespace = "team.duckie.quackquack.ui.plugin.interceptor.textfield" - - testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } - } - } } dependencies { @@ -52,11 +43,7 @@ dependencies { ) testImplementations( libs.compose.foundation, - libs.test.robolectric, - libs.test.junit.compose, libs.test.kotest.assertion.core, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.bundles.test.roborazzi, projects.utilComposeSnapshotTest, ) } diff --git a/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt b/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/snapshot/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt similarity index 85% rename from ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt rename to ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/snapshot/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt index 6ff3556a2..760e0f7c8 100644 --- a/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt +++ b/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/snapshot/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt @@ -7,7 +7,7 @@ @file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class) -package team.duckie.quackquack.ui.plugin.interceptor.textfield +package team.duckie.quackquack.ui.plugin.interceptor.textfield.snapshot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage @@ -19,10 +19,13 @@ import team.duckie.quackquack.ui.QuackDefaultTextField import team.duckie.quackquack.ui.QuackFilledTextField import team.duckie.quackquack.ui.QuackTextFieldStyle import team.duckie.quackquack.ui.optin.ExperimentalDesignToken +import team.duckie.quackquack.ui.plugin.interceptor.textfield.QuackTextFieldFontFamilyRemovalPlugin import team.duckie.quackquack.ui.plugin.rememberQuackPlugins import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule +private const val TestMessage = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?" + @RunWith(AndroidJUnit4::class) class QuackTextFieldFontFamilyRemovalPluginSnapshot { @get:Rule @@ -37,7 +40,7 @@ class QuackTextFieldFontFamilyRemovalPluginSnapshot { }, ) { QuackDefaultTextField( - value = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?", + value = TestMessage, onValueChange = {}, style = QuackTextFieldStyle.Default, ) @@ -54,7 +57,7 @@ class QuackTextFieldFontFamilyRemovalPluginSnapshot { }, ) { QuackFilledTextField( - value = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?", + value = TestMessage, onValueChange = {}, style = QuackTextFieldStyle.FilledLarge, ) diff --git a/ui-plugin/src/test/kotlin/team/duckie/quackquack/ui/plugin/QuackPluginsTest.kt b/ui-plugin/src/test/kotlin/team/duckie/quackquack/ui/plugin/QuackPluginsTest.kt index f62b971c3..7e2efff21 100644 --- a/ui-plugin/src/test/kotlin/team/duckie/quackquack/ui/plugin/QuackPluginsTest.kt +++ b/ui-plugin/src/test/kotlin/team/duckie/quackquack/ui/plugin/QuackPluginsTest.kt @@ -5,6 +5,8 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:Suppress("UNCHECKED_CAST") + package team.duckie.quackquack.ui.plugin import androidx.compose.runtime.collection.MutableVector @@ -64,7 +66,6 @@ class QuackPluginsTest : StringSpec() { pluginOrNull.shouldBeNull() } - @Suppress("UNCHECKED_CAST") "filterByTypeOrNull으로 타입에 맞는 플러그인을 모두 조회할 수 있음" { open class LocalP : QuackPlugin open class LocalP2 : QuackPlugin diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 63fda769a..26197c211 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -18,6 +18,7 @@ plugins { quackquack("kotlin-explicit-api") quackquack("quack-publishing") quackquack("test-junit") + quackquack("test-roborazzi") alias(libs.plugins.kotlin.ksp) alias(libs.plugins.test.roborazzi) } @@ -45,13 +46,8 @@ android { } testOptions { - unitTests { - isIncludeAndroidResources = true - isReturnDefaultValues = true - all { test -> - test.maxHeapSize = "4G" - test.systemProperty("robolectric.graphicsMode", "NATIVE") - } + unitTests.all { test -> + test.maxHeapSize = "4G" } } } @@ -97,13 +93,7 @@ dependencies { projects.casaAnnotation.orArtifact(), ) - testImplementations( - libs.test.robolectric, - libs.test.junit.compose, - libs.test.kotest.assertion.core, - libs.test.kotlin.coroutines, // needed for compose-ui-test - libs.bundles.test.roborazzi, - ) + testImplementation(libs.test.kotest.assertion.core) androidTestImplementations( libs.test.junit.compose, libs.test.kotest.assertion.core, diff --git a/ui/report/compose-metrics/ui_debug-module.json b/ui/report/compose-metrics/ui_debug-module.json index 73b681230..495f09229 100644 --- a/ui/report/compose-metrics/ui_debug-module.json +++ b/ui/report/compose-metrics/ui_debug-module.json @@ -1,25 +1,25 @@ { - "skippableComposables": 8, - "restartableComposables": 14, - "readonlyComposables": 1, - "totalComposables": 52, - "restartGroups": 14, - "totalGroups": 66, - "staticArguments": 54, - "certainArguments": 264, - "knownStableArguments": 613, - "knownUnstableArguments": 32, + "skippableComposables": 2, + "restartableComposables": 5, + "readonlyComposables": 0, + "totalComposables": 7, + "restartGroups": 5, + "totalGroups": 8, + "staticArguments": 6, + "certainArguments": 16, + "knownStableArguments": 76, + "knownUnstableArguments": 6, "unknownStableArguments": 0, - "totalArguments": 645, - "markedStableClasses": 18, - "inferredStableClasses": 11, + "totalArguments": 82, + "markedStableClasses": 14, + "inferredStableClasses": 0, "inferredUnstableClasses": 0, "inferredUncertainClasses": 0, - "effectivelyStableClasses": 29, - "totalClasses": 29, - "memoizedLambdas": 24, - "singletonLambdas": 5, + "effectivelyStableClasses": 14, + "totalClasses": 14, + "memoizedLambdas": 0, + "singletonLambdas": 0, "singletonComposableLambdas": 0, - "composableLambdas": 3, - "totalLambdas": 36 + "composableLambdas": 0, + "totalLambdas": 2 } \ No newline at end of file diff --git a/ui/report/compose-metrics/ui_debugUnitTest-module.json b/ui/report/compose-metrics/ui_debugUnitTest-module.json index 75d3e0c58..bfec53e19 100644 --- a/ui/report/compose-metrics/ui_debugUnitTest-module.json +++ b/ui/report/compose-metrics/ui_debugUnitTest-module.json @@ -1,22 +1,22 @@ { - "skippableComposables": 142, - "restartableComposables": 145, + "skippableComposables": 139, + "restartableComposables": 140, "readonlyComposables": 0, - "totalComposables": 145, - "restartGroups": 145, - "totalGroups": 146, - "staticArguments": 451, - "certainArguments": 4, - "knownStableArguments": 1789, - "knownUnstableArguments": 1, + "totalComposables": 140, + "restartGroups": 140, + "totalGroups": 140, + "staticArguments": 454, + "certainArguments": 0, + "knownStableArguments": 1767, + "knownUnstableArguments": 0, "unknownStableArguments": 0, - "totalArguments": 1790, + "totalArguments": 1767, "markedStableClasses": 0, - "inferredStableClasses": 3, - "inferredUnstableClasses": 6, - "inferredUncertainClasses": 5, - "effectivelyStableClasses": 3, - "totalClasses": 14, + "inferredStableClasses": 2, + "inferredUnstableClasses": 1, + "inferredUncertainClasses": 10, + "effectivelyStableClasses": 2, + "totalClasses": 13, "memoizedLambdas": 221, "singletonLambdas": 78, "singletonComposableLambdas": 102, diff --git a/ui/report/compose-metrics/ui_release-module.json b/ui/report/compose-metrics/ui_release-module.json index 73b681230..d3920a71a 100644 --- a/ui/report/compose-metrics/ui_release-module.json +++ b/ui/report/compose-metrics/ui_release-module.json @@ -6,20 +6,20 @@ "restartGroups": 14, "totalGroups": 66, "staticArguments": 54, - "certainArguments": 264, - "knownStableArguments": 613, + "certainArguments": 268, + "knownStableArguments": 618, "knownUnstableArguments": 32, "unknownStableArguments": 0, - "totalArguments": 645, + "totalArguments": 650, "markedStableClasses": 18, "inferredStableClasses": 11, "inferredUnstableClasses": 0, "inferredUncertainClasses": 0, "effectivelyStableClasses": 29, "totalClasses": 29, - "memoizedLambdas": 24, + "memoizedLambdas": 23, "singletonLambdas": 5, "singletonComposableLambdas": 0, "composableLambdas": 3, - "totalLambdas": 36 + "totalLambdas": 35 } \ No newline at end of file diff --git a/ui/report/compose-metrics/ui_releaseUnitTest-module.json b/ui/report/compose-metrics/ui_releaseUnitTest-module.json index 75d3e0c58..bfec53e19 100644 --- a/ui/report/compose-metrics/ui_releaseUnitTest-module.json +++ b/ui/report/compose-metrics/ui_releaseUnitTest-module.json @@ -1,22 +1,22 @@ { - "skippableComposables": 142, - "restartableComposables": 145, + "skippableComposables": 139, + "restartableComposables": 140, "readonlyComposables": 0, - "totalComposables": 145, - "restartGroups": 145, - "totalGroups": 146, - "staticArguments": 451, - "certainArguments": 4, - "knownStableArguments": 1789, - "knownUnstableArguments": 1, + "totalComposables": 140, + "restartGroups": 140, + "totalGroups": 140, + "staticArguments": 454, + "certainArguments": 0, + "knownStableArguments": 1767, + "knownUnstableArguments": 0, "unknownStableArguments": 0, - "totalArguments": 1790, + "totalArguments": 1767, "markedStableClasses": 0, - "inferredStableClasses": 3, - "inferredUnstableClasses": 6, - "inferredUncertainClasses": 5, - "effectivelyStableClasses": 3, - "totalClasses": 14, + "inferredStableClasses": 2, + "inferredUnstableClasses": 1, + "inferredUncertainClasses": 10, + "effectivelyStableClasses": 2, + "totalClasses": 13, "memoizedLambdas": 221, "singletonLambdas": 78, "singletonComposableLambdas": 102, diff --git a/ui/report/compose-reports/ui_debug-classes.txt b/ui/report/compose-reports/ui_debug-classes.txt index f0c83eedd..788c6814a 100644 --- a/ui/report/compose-reports/ui_debug-classes.txt +++ b/ui/report/compose-reports/ui_debug-classes.txt @@ -80,21 +80,6 @@ stable class QuackSecondaryRoundSmallButtonDefaults { stable var typography: QuackTypography stable var disabledTypography: QuackTypography } -stable class QuackSwitchColors { - stable val track: QuackColor - stable val disableTrack: QuackColor - stable val thumb: QuackColor - stable val thumbStroke: QuackColor - stable val disableThumb: QuackColor - stable val disableThumbStroke: QuackColor -} -stable class QuackTabColors { - stable val background: QuackColor - stable val underline: QuackColor - stable val indicate: QuackColor - stable val contentColor: QuackColor - stable val disableContentColor: QuackColor -} stable class QuackTagColors { stable val backgroundColor: QuackColor stable val unselectedBackgroundColor: QuackColor @@ -142,75 +127,3 @@ stable class QuackGrayscaleOutlinedTagDefaults { stable var typography: QuackTypography stable var unselectedTypography: QuackTypography } -stable class Success { - stable val label: String? - = Stable -} -stable class Error { - stable val label: String? - = Stable -} -stable class Default { - = Stable -} -stable class TextFieldValidationState { -} -stable class Invisible { - stable val baselineLabel: String - stable val baselineTypography: QuackTypography? - = Stable -} -stable class Gone { - = Stable -} -stable class TextFieldValidationLabelVisibilityStrategy { -} -stable class TextFieldColors { - stable val backgroundColor: QuackColor - stable val contentColor: QuackColor - stable val placeholderColor: QuackColor - stable val errorColor: QuackColor - stable val successColor: QuackColor - = Stable -} -stable class TextFieldColors { - stable val backgroundColor: QuackColor? - stable val backgroundColorGetter: Function2<@[ParameterName(name = 'text')] String, @[ParameterName(name = 'focusInteraction')] FocusInteraction?, QuackColor>? - stable val contentColor: QuackColor - stable val placeholderColor: QuackColor - = Stable -} -stable class QuackDefaultTextFieldDefaults { - stable var colors: TextFieldColors - stable var contentPadding: PaddingValues - stable var contentSpacedBy: Dp - stable var validationLabelAndIndicatorSpacedBy: Dp - stable var typography: QuackTypography - stable val validationLabelTypography: QuackTypography - = Stable -} -stable class QuackDefaultLargeTextFieldDefaults { - stable var colors: TextFieldColors - stable var contentPadding: PaddingValues - stable var contentSpacedBy: Dp - stable var validationLabelAndIndicatorSpacedBy: Dp - stable var typography: QuackTypography - stable val validationLabelTypography: QuackTypography - = Stable -} -stable class QuackFilledLargeTextFieldDefaults { - stable var radius: Dp - stable var colors: TextFieldColors - stable var contentPadding: PaddingValues - stable var contentSpacedBy: Dp - stable var typography: QuackTypography - = Stable -} -stable class QuackFilledFlatTextFieldDefaults { - stable var radius: Dp - stable var colors: TextFieldColors - stable var contentPadding: PaddingValues - stable var contentSpacedBy: Dp - stable var typography: QuackTypography - = Stable -} diff --git a/ui/report/compose-reports/ui_debug-composables.csv b/ui/report/compose-reports/ui_debug-composables.csv index b110b6d30..c98d0f702 100644 --- a/ui/report/compose-reports/ui_debug-composables.csv +++ b/ui/report/compose-reports/ui_debug-composables.csv @@ -1,44 +1,5 @@ package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, team.duckie.quackquack.ui.QuackButton,QuackButton,1,0,0,0,0,0,0,0,1,6, team.duckie.quackquack.ui.QuackBaseButton,QuackBaseButton,1,1,1,0,0,0,0,0,1,2, -team.duckie.quackquack.ui.QuackIcon,QuackIcon,1,0,0,0,0,0,0,0,2,3, -team.duckie.quackquack.ui.QuackImage,QuackImage,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.QuackImage,QuackImage,1,0,0,0,0,0,0,0,2,3, -team.duckie.quackquack.ui.QuackImage,QuackImage,1,0,0,0,0,0,0,0,2,6, -team.duckie.quackquack.ui.sugar.QuackPrimaryLargeButton,QuackPrimaryLargeButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackSecondaryLargeButton,QuackSecondaryLargeButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackMediumButton,QuackMediumButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackPrimaryFilledSmallButton,QuackPrimaryFilledSmallButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackPrimaryOutlinedSmallButton,QuackPrimaryOutlinedSmallButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackPrimaryOutlinedRoundSmallButton,QuackPrimaryOutlinedRoundSmallButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackSecondarySmallButton,QuackSecondarySmallButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackSecondaryRoundSmallButton,QuackSecondaryRoundSmallButton,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackOutlinedTag,QuackOutlinedTag,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackFilledTag,QuackFilledTag,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackGrayscaleFlatTag,QuackGrayscaleFlatTag,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackGrayscaleOutlinedTag,QuackGrayscaleOutlinedTag,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackBody1,QuackBody1,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackBody2,QuackBody2,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackBody3,QuackBody3,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackHeadLine1,QuackHeadLine1,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackHeadLine2,QuackHeadLine2,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackLarge1,QuackLarge1,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackSubtitle,QuackSubtitle,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackSubtitle2,QuackSubtitle2,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackTitle1,QuackTitle1,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.sugar.QuackTitle2,QuackTitle2,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.QuackSwitch,QuackSwitch,1,1,1,0,0,0,0,0,1,7, -team.duckie.quackquack.ui.QuackTab,QuackTab,1,1,1,0,0,0,1,0,3,10, -team.duckie.quackquack.ui.QuackTag,QuackTag,1,0,0,0,0,0,0,0,1,7, +team.duckie.quackquack.ui.QuackTag,QuackTag,1,0,0,0,0,0,0,0,1,6, team.duckie.quackquack.ui.QuackBaseTag,QuackBaseTag,1,1,1,0,0,0,0,0,1,2, -team.duckie.quackquack.ui.QuackText,QuackText,1,1,1,0,0,0,0,0,4,10, -team.duckie.quackquack.ui.ClickableText,ClickableText,1,1,1,0,0,0,0,0,1,4, -team.duckie.quackquack.ui.rememberSpanAnnotatedString,rememberSpanAnnotatedString,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.QuackDefaultTextField,QuackDefaultTextField,1,0,0,0,0,0,0,0,1,7, -team.duckie.quackquack.ui.QuackDefaultTextField,QuackDefaultTextField,1,0,0,0,0,0,0,0,1,10, -team.duckie.quackquack.ui.QuackFilledTextField,QuackFilledTextField,1,0,0,0,0,0,0,0,1,7, -team.duckie.quackquack.ui.QuackFilledTextField,QuackFilledTextField,1,0,0,0,0,0,0,0,1,10, -team.duckie.quackquack.ui.QuackBaseDefaultTextField,QuackBaseDefaultTextField,1,1,1,0,0,0,0,0,1,27, -team.duckie.quackquack.ui.QuackOutlinedTextField,QuackOutlinedTextField,1,0,0,0,0,0,0,0,1,0, -team.duckie.quackquack.ui.util.rememberLtrTextMeasurer,rememberLtrTextMeasurer,1,0,0,0,0,0,0,0,1,3, -team.duckie.quackquack.ui.util.currentFontScale,currentFontScale,1,0,0,1,1,0,0,0,1,1, diff --git a/ui/report/compose-reports/ui_debug-composables.txt b/ui/report/compose-reports/ui_debug-composables.txt index 1afe56d80..75270c240 100644 --- a/ui/report/compose-reports/ui_debug-composables.txt +++ b/ui/report/compose-reports/ui_debug-composables.txt @@ -22,216 +22,6 @@ restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun QuackBase stable trailingIcon: ImageVector? stable onClick: Function0? ) -scheme("[androidx.compose.ui.UiComposable]") fun QuackIcon( - stable icon: ImageVector - stable modifier: Modifier? = @static Companion - stable size: Dp = @static 24.dp - stable tint: QuackColor = @static Companion.Unspecified - stable contentScale: ContentScale? = @static Companion.Fit - stable contentDescription: String? = @static null -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackImage( - stable src: ImageVector - stable modifier: Modifier? = @static Companion - stable tint: QuackColor = @static Companion.Unspecified - stable contentScale: ContentScale? = @static Companion.Fit -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackImage( - stable src: Int - stable modifier: Modifier? = @static Companion - stable tint: QuackColor = @static Companion.Unspecified - stable contentScale: ContentScale? = @static Companion.Fit - stable contentDescription: String? = @static null -) -fun QuackImage( - unstable src: Any? - stable modifier: Modifier? = @static Companion - stable tint: QuackColor = @static Companion.Unspecified - stable contentScale: ContentScale? = @static Companion.Fit - stable contentDescription: String? = @static null -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackPrimaryLargeButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackSecondaryLargeButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackMediumButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackPrimaryFilledSmallButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackPrimaryOutlinedSmallButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackPrimaryOutlinedRoundSmallButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackSecondarySmallButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackSecondaryRoundSmallButton( - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable text: String - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -fun QuackOutlinedTag( - stable text: String - stable modifier: Modifier? = @static Companion - stable selected: Boolean = @static true - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -fun QuackFilledTag( - stable text: String - stable modifier: Modifier? = @static Companion - stable selected: Boolean = @static true - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -fun QuackGrayscaleFlatTag( - stable text: String - stable modifier: Modifier? = @static Companion - stable selected: Boolean = @static true - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -fun QuackGrayscaleOutlinedTag( - stable text: String - stable modifier: Modifier? = @static Companion - stable selected: Boolean = @static true - stable rippleEnabled: Boolean = @static true - stable onClick: Function0 -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackBody1( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackBody2( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackBody3( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackHeadLine1( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackHeadLine2( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackLarge1( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackSubtitle( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackSubtitle2( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackTitle1( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackTitle2( - stable modifier: Modifier? = @static Companion - stable text: String - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun QuackSwitch( - stable enabled: Boolean - stable colors: QuackSwitchColors? = @static Companion.defaultSwitchColors - stable modifier: Modifier? = @static Companion - stable onClick: Function0 -) -restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun QuackTab( - stable index: Int - stable colors: QuackTabColors? = @static Companion.defaultTabColors() - indicatorStartXOffsetAnimatable: Animatable? = @dynamic remember({ - Animatable ( - initialValue = QuackTabIndicatorXOffsetInitialValue , - typeConverter = Companion . VectorConverter , - label = "QuackTabIndicatorStartXOffset" - ) -} -, $composer, 0) - indicatorEndXOffsetAnimatable: Animatable? = @dynamic remember({ - Animatable ( - initialValue = QuackTabIndicatorXOffsetInitialValue , - typeConverter = Companion . VectorConverter , - label = "QuackTabIndicatorEndXOffset" - ) -} -, $composer, 0) - stable modifier: Modifier? = @static Companion - stable content: @[ExtensionFunctionType] Function1 -) scheme("[androidx.compose.ui.UiComposable]") fun QuackTag( stable text: String stable style: QuackTagStyle @@ -257,201 +47,3 @@ restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun QuackBase stable trailingIconOnClick: Function0? stable onClick: Function0? ) -restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun QuackText( - stable text: String - stable typography: QuackTypography - stable modifier: Modifier? = @static Companion - stable singleLine: Boolean = @static false - stable softWrap: Boolean = @static true - stable overflow: TextOverflow = @static Companion.Ellipsis -) -restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ClickableText( - stable modifier: Modifier - stable text: String - stable highlightData: TextHighlightData - stable style: TextStyle - stable softWrap: Boolean - stable overflow: TextOverflow - stable maxLines: Int -) -fun rememberSpanAnnotatedString( - stable text: String - unstable spanTexts: List - stable spanStyle: SpanStyle - unstable annotationTexts: List -): AnnotatedString -fun QuackDefaultTextField( - stable value: String - stable onValueChange: Function1<@[ParameterName(name = 'value')] String, Unit> - stable style: QuackTextFieldStyle - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable readOnly: Boolean = @static false - stable placeholderText: String? = @static null - stable placeholderStrategy: TextFieldPlaceholderStrategy? = @static TextFieldPlaceholderStrategy.Hidable - stable keyboardOptions: KeyboardOptions? = @static Companion.Default - stable keyboardActions: KeyboardActions? = @static Companion.Default - stable singleLine: Boolean = @static false - stable minLines: Int = @static 1 - stable maxLines: Int = @dynamic if (singleLine) { - 1 -} else { - Companion . MAX_VALUE -} - - stable visualTransformation: VisualTransformation? = @static Companion.None - stable onTextLayout: Function1<@[ParameterName(name = 'layoutResult')] TextLayoutResult, Unit>? = @static { it: TextLayoutResult -> - -} - - stable validationState: TextFieldValidationState? = @static Default - stable validationLabelVisibilityStrategy: TextFieldValidationLabelVisibilityStrategy? = @static Gone - stable interactionSource: MutableInteractionSource? = @static remember({ - MutableInteractionSource ( ) -} -, $composer, 0) -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackDefaultTextField( - stable value: TextFieldValue - stable onValueChange: Function1<@[ParameterName(name = 'value')] TextFieldValue, Unit> - stable style: QuackTextFieldStyle - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable readOnly: Boolean = @static false - stable placeholderText: String? = @static null - stable placeholderStrategy: TextFieldPlaceholderStrategy? = @static TextFieldPlaceholderStrategy.Hidable - stable keyboardOptions: KeyboardOptions? = @static Companion.Default - stable keyboardActions: KeyboardActions? = @static Companion.Default - stable singleLine: Boolean = @static false - stable minLines: Int = @static 1 - stable maxLines: Int = @dynamic if (singleLine) { - 1 -} else { - Companion . MAX_VALUE -} - - stable visualTransformation: VisualTransformation? = @static Companion.None - stable onTextLayout: Function1<@[ParameterName(name = 'layoutResult')] TextLayoutResult, Unit>? = @static { it: TextLayoutResult -> - -} - - stable validationState: TextFieldValidationState? = @static Default - stable validationLabelVisibilityStrategy: TextFieldValidationLabelVisibilityStrategy? = @static Gone - stable interactionSource: MutableInteractionSource? = @static remember({ - MutableInteractionSource ( ) -} -, $composer, 0) -) -fun QuackFilledTextField( - stable value: String - stable onValueChange: Function1<@[ParameterName(name = 'value')] String, Unit> - stable style: QuackTextFieldStyle - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable readOnly: Boolean = @static false - stable placeholderText: String? = @static null - stable placeholderStrategy: TextFieldPlaceholderStrategy? = @static TextFieldPlaceholderStrategy.Hidable - stable keyboardOptions: KeyboardOptions? = @static Companion.Default - stable keyboardActions: KeyboardActions? = @static Companion.Default - stable singleLine: Boolean = @static false - stable minLines: Int = @static 1 - stable maxLines: Int = @dynamic if (singleLine) { - 1 -} else { - Companion . MAX_VALUE -} - - stable visualTransformation: VisualTransformation? = @static Companion.None - stable onTextLayout: Function1<@[ParameterName(name = 'layoutResult')] TextLayoutResult, Unit>? = @static { it: TextLayoutResult -> - -} - - stable interactionSource: MutableInteractionSource? = @static remember({ - MutableInteractionSource ( ) -} -, $composer, 0) -) -scheme("[androidx.compose.ui.UiComposable]") fun QuackFilledTextField( - stable value: TextFieldValue - stable onValueChange: Function1<@[ParameterName(name = 'value')] TextFieldValue, Unit> - stable style: QuackTextFieldStyle - stable modifier: Modifier? = @static Companion - stable enabled: Boolean = @static true - stable readOnly: Boolean = @static false - stable placeholderText: String? = @static null - stable placeholderStrategy: TextFieldPlaceholderStrategy? = @static TextFieldPlaceholderStrategy.Hidable - stable keyboardOptions: KeyboardOptions? = @static Companion.Default - stable keyboardActions: KeyboardActions? = @static Companion.Default - stable singleLine: Boolean = @static false - stable minLines: Int = @static 1 - stable maxLines: Int = @dynamic if (singleLine) { - 1 -} else { - Companion . MAX_VALUE -} - - stable visualTransformation: VisualTransformation? = @static Companion.None - stable onTextLayout: Function1<@[ParameterName(name = 'layoutResult')] TextLayoutResult, Unit>? = @static { it: TextLayoutResult -> - -} - - stable interactionSource: MutableInteractionSource? = @static remember({ - MutableInteractionSource ( ) -} -, $composer, 0) -) -restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun QuackBaseDefaultTextField( - stable value: TextFieldValue - stable onValueChange: Function1<@[ParameterName(name = 'value')] TextFieldValue, Unit> - stable modifier: Modifier - stable enabled: Boolean - stable readOnly: Boolean - stable placeholderText: String? - stable placeholderStrategy: TextFieldPlaceholderStrategy - stable keyboardOptions: KeyboardOptions - stable keyboardActions: KeyboardActions - stable singleLine: Boolean - stable minLines: Int - stable maxLines: Int - stable visualTransformation: VisualTransformation - stable onTextLayout: Function1<@[ParameterName(name = 'layoutResult')] TextLayoutResult, Unit> - stable validationState: TextFieldValidationState? - stable validationLabelVisibilityStrategy: TextFieldValidationLabelVisibilityStrategy? - stable interactionSource: MutableInteractionSource - stable backgroundColor: QuackColor - stable contentPadding: PaddingValues? - stable contentSpacedBy: Dp - stable validationLabelAndIndicatorSpacedBy: Dp? - stable typography: QuackTypography - stable placeholderTypography: QuackTypography - stable errorTypography: QuackTypography? - stable successTypography: QuackTypography? - stable textFieldType: QuackTextFieldType - stable leadingIcon: ImageVector? - stable leadingIconSize: Dp? - stable leadingIconTint: QuackColor? - stable leadingIconContentScale: ContentScale? - stable leadingIconContentDescription: String? - stable leadingIconOnClick: Function0? - stable trailingIcon: ImageVector? - stable trailingIconSize: Dp? - stable trailingIconTint: QuackColor? - stable trailingIconContentScale: ContentScale? - stable trailingIconContentDescription: String? - stable trailingIconOnClick: Function0? - stable indicatorThickness: Dp? - stable indicatorColor: QuackColor? - stable indicatorDirection: VerticalDirection? - stable counterBaseColor: QuackColor? - stable counterHighlightColor: QuackColor? - stable counterTypography: QuackTypography? - stable counterBaseAndHighlightGap: Dp? - stable counterMaxLength: Int? -) -fun QuackOutlinedTextField() -fun rememberLtrTextMeasurer( - stable cacheSize: Int = @static 8 -): TextMeasurer -readonly inline fun currentFontScale( - stable content: Function1<@[ParameterName(name = 'fontScale')] Float, T> -): T diff --git a/ui/report/compose-reports/ui_debugUnitTest-classes.txt b/ui/report/compose-reports/ui_debugUnitTest-classes.txt index 3a98ccbf7..72534dd46 100644 --- a/ui/report/compose-reports/ui_debugUnitTest-classes.txt +++ b/ui/report/compose-reports/ui_debugUnitTest-classes.txt @@ -1,13 +1,14 @@ -stable class ImageSnapshot { - = Stable +runtime class ImageSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } -unstable class SwitchSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable +runtime class SwitchSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } -unstable class TabSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable +runtime class TabSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } unstable class TagSnapshot { unstable val testNameToSelectState: MutableMap @@ -15,18 +16,13 @@ unstable class TagSnapshot { unstable val roborazzi: RoborazziRule = Unstable } -unstable class TextFieldSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable +runtime class TextFieldSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } -unstable class TextSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable -} -unstable class SnapshotPathGeneratorRule { - stable val domain: String - stable var realtimeTestMethodName: String? - = Unstable +runtime class TextSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } runtime class ButtonTest { runtime val compose: AndroidComposeTestRule, ComponentActivity> diff --git a/ui/report/compose-reports/ui_debugUnitTest-composables.csv b/ui/report/compose-reports/ui_debugUnitTest-composables.csv index aa562c9bb..975e679ba 100644 --- a/ui/report/compose-reports/ui_debugUnitTest-composables.csv +++ b/ui/report/compose-reports/ui_debugUnitTest-composables.csv @@ -1,4 +1 @@ package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, -team.duckie.quackquack.ui.snapshot.util.TestColumn,TestColumn,1,1,1,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.snapshot.util.WithLabel,WithLabel,1,1,1,0,0,0,1,0,2,1, -team.duckie.quackquack.ui.snapshot.util.withIncreaseFontScale,withIncreaseFontScale,1,1,1,0,0,0,0,0,1,2, diff --git a/ui/report/compose-reports/ui_debugUnitTest-composables.txt b/ui/report/compose-reports/ui_debugUnitTest-composables.txt index bbaa45a5e..e69de29bb 100644 --- a/ui/report/compose-reports/ui_debugUnitTest-composables.txt +++ b/ui/report/compose-reports/ui_debugUnitTest-composables.txt @@ -1,20 +0,0 @@ -restartable skippable scheme("[androidx.compose.ui.UiComposable, [androidx.compose.ui.UiComposable]]") fun TestColumn( - stable contentGap: Dp = @static 15.dp - stable content: Function2 -) -restartable skippable scheme("[androidx.compose.ui.UiComposable, [androidx.compose.ui.UiComposable]]") fun WithLabel( - stable label: String - stable isTitle: Boolean = @static false - stable labelGap: Dp = @static 4.dp - stable contentGap: Dp = @dynamic if (isTitle) { - 15 . dp -} else { - 4 . dp -} - - stable content: @[ExtensionFunctionType] Function3 -) -restartable skippable scheme("[0, [0]]") fun withIncreaseFontScale( - stable fontScale: Float - stable content: Function2 -) diff --git a/ui/report/compose-reports/ui_release-composables.csv b/ui/report/compose-reports/ui_release-composables.csv index b110b6d30..a8f435b64 100644 --- a/ui/report/compose-reports/ui_release-composables.csv +++ b/ui/report/compose-reports/ui_release-composables.csv @@ -27,11 +27,11 @@ team.duckie.quackquack.ui.sugar.QuackSubtitle,QuackSubtitle,1,0,0,0,0,0,0,0,1,1, team.duckie.quackquack.ui.sugar.QuackSubtitle2,QuackSubtitle2,1,0,0,0,0,0,0,0,1,1, team.duckie.quackquack.ui.sugar.QuackTitle1,QuackTitle1,1,0,0,0,0,0,0,0,1,1, team.duckie.quackquack.ui.sugar.QuackTitle2,QuackTitle2,1,0,0,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.QuackSwitch,QuackSwitch,1,1,1,0,0,0,0,0,1,7, -team.duckie.quackquack.ui.QuackTab,QuackTab,1,1,1,0,0,0,1,0,3,10, -team.duckie.quackquack.ui.QuackTag,QuackTag,1,0,0,0,0,0,0,0,1,7, +team.duckie.quackquack.ui.QuackSwitch,QuackSwitch,1,1,1,0,0,0,0,0,1,8, +team.duckie.quackquack.ui.QuackTab,QuackTab,1,1,1,0,0,0,1,0,3,11, +team.duckie.quackquack.ui.QuackTag,QuackTag,1,0,0,0,0,0,0,0,1,6, team.duckie.quackquack.ui.QuackBaseTag,QuackBaseTag,1,1,1,0,0,0,0,0,1,2, -team.duckie.quackquack.ui.QuackText,QuackText,1,1,1,0,0,0,0,0,4,10, +team.duckie.quackquack.ui.QuackText,QuackText,1,1,1,0,0,0,0,0,4,11, team.duckie.quackquack.ui.ClickableText,ClickableText,1,1,1,0,0,0,0,0,1,4, team.duckie.quackquack.ui.rememberSpanAnnotatedString,rememberSpanAnnotatedString,1,0,0,0,0,0,0,0,1,1, team.duckie.quackquack.ui.QuackDefaultTextField,QuackDefaultTextField,1,0,0,0,0,0,0,0,1,7, diff --git a/ui/report/compose-reports/ui_releaseUnitTest-classes.txt b/ui/report/compose-reports/ui_releaseUnitTest-classes.txt index 3a98ccbf7..72534dd46 100644 --- a/ui/report/compose-reports/ui_releaseUnitTest-classes.txt +++ b/ui/report/compose-reports/ui_releaseUnitTest-classes.txt @@ -1,13 +1,14 @@ -stable class ImageSnapshot { - = Stable +runtime class ImageSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } -unstable class SwitchSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable +runtime class SwitchSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } -unstable class TabSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable +runtime class TabSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } unstable class TagSnapshot { unstable val testNameToSelectState: MutableMap @@ -15,18 +16,13 @@ unstable class TagSnapshot { unstable val roborazzi: RoborazziRule = Unstable } -unstable class TextFieldSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable +runtime class TextFieldSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } -unstable class TextSnapshot { - unstable val snapshotPath: SnapshotPathGeneratorRule - = Unstable -} -unstable class SnapshotPathGeneratorRule { - stable val domain: String - stable var realtimeTestMethodName: String? - = Unstable +runtime class TextSnapshot { + runtime val snapshotPath: SnapshotPathGeneratorRule + = Runtime(SnapshotPathGeneratorRule) } runtime class ButtonTest { runtime val compose: AndroidComposeTestRule, ComponentActivity> diff --git a/ui/report/compose-reports/ui_releaseUnitTest-composables.csv b/ui/report/compose-reports/ui_releaseUnitTest-composables.csv index aa562c9bb..975e679ba 100644 --- a/ui/report/compose-reports/ui_releaseUnitTest-composables.csv +++ b/ui/report/compose-reports/ui_releaseUnitTest-composables.csv @@ -1,4 +1 @@ package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls, -team.duckie.quackquack.ui.snapshot.util.TestColumn,TestColumn,1,1,1,0,0,0,0,0,1,1, -team.duckie.quackquack.ui.snapshot.util.WithLabel,WithLabel,1,1,1,0,0,0,1,0,2,1, -team.duckie.quackquack.ui.snapshot.util.withIncreaseFontScale,withIncreaseFontScale,1,1,1,0,0,0,0,0,1,2, diff --git a/ui/report/compose-reports/ui_releaseUnitTest-composables.txt b/ui/report/compose-reports/ui_releaseUnitTest-composables.txt index bbaa45a5e..e69de29bb 100644 --- a/ui/report/compose-reports/ui_releaseUnitTest-composables.txt +++ b/ui/report/compose-reports/ui_releaseUnitTest-composables.txt @@ -1,20 +0,0 @@ -restartable skippable scheme("[androidx.compose.ui.UiComposable, [androidx.compose.ui.UiComposable]]") fun TestColumn( - stable contentGap: Dp = @static 15.dp - stable content: Function2 -) -restartable skippable scheme("[androidx.compose.ui.UiComposable, [androidx.compose.ui.UiComposable]]") fun WithLabel( - stable label: String - stable isTitle: Boolean = @static false - stable labelGap: Dp = @static 4.dp - stable contentGap: Dp = @dynamic if (isTitle) { - 15 . dp -} else { - 4 . dp -} - - stable content: @[ExtensionFunctionType] Function3 -) -restartable skippable scheme("[0, [0]]") fun withIncreaseFontScale( - stable fontScale: Float - stable content: Function2 -) diff --git a/ui/src/androidTest/kotlin/team/duckie/quackquack/ui/TextFieldTest.kt b/ui/src/androidTest/kotlin/team/duckie/quackquack/ui/TextFieldTest.kt index 015572139..1938c9443 100644 --- a/ui/src/androidTest/kotlin/team/duckie/quackquack/ui/TextFieldTest.kt +++ b/ui/src/androidTest/kotlin/team/duckie/quackquack/ui/TextFieldTest.kt @@ -15,15 +15,15 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import io.kotest.matchers.shouldBe +import org.junit.Ignore import org.junit.Rule import org.junit.Test import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.theme.QuackTheme import team.duckie.quackquack.ui.optin.ExperimentalDesignToken import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi -import team.duckie.quackquack.util.Empty -// wip +@Ignore("WIP") class TextFieldTest { @get:Rule val compose = createComposeRule() @@ -35,7 +35,7 @@ class TextFieldTest { compose.setContent { QuackTheme { QuackFilledTextField( - value = String.Empty, + value = "", onValueChange = {}, style = QuackTextFieldStyle.FilledLarge { colors = colors.copy( diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt index ecab93987..a515804b3 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset -import androidx.compose.ui.util.fastFirstOrNull import team.duckie.quackquack.casa.annotation.CasaValue import team.duckie.quackquack.material.QuackBorder import team.duckie.quackquack.material.QuackColor @@ -56,7 +55,9 @@ import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafe import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi import team.duckie.quackquack.ui.util.QuackDsl import team.duckie.quackquack.ui.util.asLoose +import team.duckie.quackquack.ui.util.fastGetByIdOrNull import team.duckie.quackquack.ui.util.fontScaleAwareIconSize +import team.duckie.quackquack.ui.util.get import team.duckie.quackquack.ui.util.wrappedDebugInspectable import team.duckie.quackquack.util.MustBeTested import team.duckie.quackquack.util.fastFirstIsInstanceOrNull @@ -770,7 +771,7 @@ public fun QuackButton( @CasaValue("{}") onClick: () -> Unit, ) { @Suppress("NAME_SHADOWING") - val style: QuackButtonStyle = rememberInterceptedStyleSafely(style = style, modifier = modifier) + val style = rememberInterceptedStyleSafely>(style = style, modifier = modifier) val isSmallButton = style is QuackSmallButtonStyle // TODO: 다른 경우로 사이즈를 지정하는 방법이 있을까? @@ -796,17 +797,13 @@ public fun QuackButton( val borderThickness = style.borderThickness val borderColor = style.colors.borderColor val disabledBorderColor = style.colors.disabledBorderColor - val currentBorder = remember( - enabled, - borderThickness, - borderColor, - disabledBorderColor, - ) { - QuackBorder( - thickness = borderThickness, - color = if (enabled) borderColor else disabledBorderColor, - ) - } + val currentBorder = + remember(enabled, borderThickness, borderColor, disabledBorderColor) { + QuackBorder( + thickness = borderThickness, + color = if (enabled) borderColor else disabledBorderColor, + ) + } val iconColor = style.colors.iconColor @@ -815,9 +812,7 @@ public fun QuackButton( val currentRippleEnabled = if (enabled) rippleEnabled else false val radius = style.radius - val shape = remember(radius) { - RoundedCornerShape(size = radius) - } + val shape = remember(radius) { RoundedCornerShape(size = radius) } val contentPadding = style.contentPadding val currentContentPadding = if (isSizeSpecified) null else contentPadding @@ -943,15 +938,9 @@ public fun QuackBaseButton( } }, ) { measurables, constraints -> - val leadingIconMeasurable = measurables.fastFirstOrNull { measurable -> - measurable.layoutId == LeadingIconLayoutId - } - val textMeasurable = measurables.fastFirstOrNull { measurable -> - measurable.layoutId == TextLayoutId - }!! - val trailingIconMeasurable = measurables.fastFirstOrNull { measurable -> - measurable.layoutId == TrailingIconLayoutId - } + val textMeasurable = measurables[TextLayoutId] + val leadingIconMeasurable = measurables.fastGetByIdOrNull(LeadingIconLayoutId) + val trailingIconMeasurable = measurables.fastGetByIdOrNull(TrailingIconLayoutId) val looseConstraints = constraints.asLoose(width = true, height = true) diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt index 18f7d0a3c..3f8b4a0e1 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.quackClickable import team.duckie.quackquack.sugar.material.NoSugar +import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.onDrawFront /** @@ -133,6 +134,9 @@ public fun QuackSwitch( modifier: Modifier = Modifier, onClick: () -> Unit, ) { + @Suppress("NAME_SHADOWING") + val colors = rememberInterceptedStyleSafely(style = colors, modifier = modifier) + val density = LocalDensity.current val enabledXCenter = remember(density, width, height) { diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt index 243b70723..1a05c0452 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt @@ -11,6 +11,7 @@ package team.duckie.quackquack.ui import androidx.annotation.VisibleForTesting import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.snap @@ -23,6 +24,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.collection.MutableVector import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset @@ -40,12 +42,14 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastMap +import kotlin.math.roundToInt import kotlinx.coroutines.launch -import team.duckie.quackquack.animation.animatedQuackTextStyleAsState +import team.duckie.quackquack.animation.animatedQuackTypographyAsState import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.QuackTypography import team.duckie.quackquack.material.quackClickable import team.duckie.quackquack.sugar.material.NoSugar +import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.fastFilterById import team.duckie.quackquack.ui.util.onDrawFront import team.duckie.quackquack.ui.util.rememberLtrTextMeasurer @@ -223,6 +227,9 @@ public fun QuackTab( modifier: Modifier = Modifier, content: QuackTabScope.() -> Unit, ) { + @Suppress("NAME_SHADOWING") + val colors = rememberInterceptedStyleSafely(style = colors, modifier = modifier) + fun Modifier.drawUnderline() = drawWithCache { val underline = object { @@ -303,9 +310,11 @@ public fun QuackTab( .drawUnderline(), content = { tabs.fastForEachIndexed { tabIndex, (label, onClick) -> + @Suppress("UNCHECKED_CAST") val labelTypography = - animatedQuackTextStyleAsState( + animatedQuackTypographyAsState( targetValue = if (index == tabIndex) tabTypography else disabledTabTypography, + animationSpec = tabTweenSpec as AnimationSpec, label = "QuackTabLabelTypography", ) val labelMeasureResult = remember(label, labelTypography) { @@ -334,7 +343,10 @@ public fun QuackTab( val labelPlacementTopLeftOffset = Offset( x = tabLabelSpacedBy.toPx(), - y = size.height / 2 - labelMeasureResult.size.height / 2, + y = Alignment + .CenterVertically + .align(size = labelMeasureResult.size.height, space = size.height.roundToInt()) + .toFloat(), ) onDrawFront { drawText( diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt index 62debea83..e92df7ce2 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt @@ -554,7 +554,7 @@ public fun QuackTag( @CasaValue("{}") onClick: () -> Unit, ) { @Suppress("NAME_SHADOWING") - val style: QuackTagStyle = rememberInterceptedStyleSafely(style = style, modifier = modifier) + val style = rememberInterceptedStyleSafely>(style = style, modifier = modifier) val isGrayscaleFlat = style is QuackGrayscaleFlatTagDefaults if (isGrayscaleFlat) { @@ -582,26 +582,20 @@ public fun QuackTag( val borderThickness = style.borderThickness val borderColor = style.colors.borderColor val unselectedBorderColor = style.colors.unselectedBorderColor - val currentBorder = remember( - selected, - borderThickness, - borderColor, - unselectedBorderColor, - ) { - QuackBorder( - thickness = borderThickness, - color = if (selected) borderColor else unselectedBorderColor, - ) - } + val currentBorder = + remember(selected, borderThickness, borderColor, unselectedBorderColor) { + QuackBorder( + thickness = borderThickness, + color = if (selected) borderColor else unselectedBorderColor, + ) + } val rippleColor = style.colors.rippleColor val currentRippleEnabled = if (selected) rippleEnabled else false val radius = style.radius - val shape = remember(radius) { - RoundedCornerShape(size = radius) - } + val shape = remember(radius) { RoundedCornerShape(size = radius) } val contentPadding = style.contentPadding val currentContentPadding = if (isSizeSpecified) null else contentPadding @@ -610,14 +604,10 @@ public fun QuackTag( val typography = style.typography val unselectedTypography = style.unselectedTypography - val currentTypography = remember( - selected, - typography, - unselectedTypography, - currentContentColor, - ) { - (if (selected) typography else unselectedTypography).change(color = currentContentColor) - } + val currentTypography = + remember(selected, typography, unselectedTypography, currentContentColor) { + (if (selected) typography else unselectedTypography).change(color = currentContentColor) + } val iconColor = style.colors.iconColor val unselectedIconColor = style.colors.unselectedIconColor @@ -627,25 +617,7 @@ public fun QuackTag( val trailingIconSize = trailingIconData?.size val trailingIconOnClick = trailingIconData?.onClick - val inspectableModifier = style - .wrappedDebugInspectable(composeModifier) - .wrappedDebugInspectable { - name = "QuackTag" - properties["text"] = text - properties["backgroundColor"] = currentBackgroundColor - properties["rippleColor"] = rippleColor - properties["rippleEnabled"] = currentRippleEnabled - properties["shape"] = shape - properties["border"] = currentBorder - properties["typography"] = currentTypography - properties["contentPadding"] = currentContentPadding - properties["iconSpacedBy"] = iconSpacedBy - properties["iconColor"] = currentIconColor - properties["trailingIcon"] = trailingIcon - properties["trailingIconSize"] = trailingIconSize - properties["trailingIconOnClick"] = trailingIconOnClick - properties["onClick"] = onClick - } + val inspectableModifier = style.wrappedDebugInspectable(composeModifier) QuackBaseTag( modifier = inspectableModifier, diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt index c33a60ccf..f96f198d3 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt @@ -39,6 +39,7 @@ import team.duckie.quackquack.runtime.quackComposed import team.duckie.quackquack.runtime.quackMaterializeOf import team.duckie.quackquack.sugar.material.SugarName import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.asLoose import team.duckie.quackquack.ui.util.rememberLtrTextMeasurer import team.duckie.quackquack.util.fastFirstIsInstanceOrNull @@ -74,15 +75,16 @@ private data class TextHighlightData( public fun Modifier.span( texts: List, style: SpanStyle, -): Modifier = inspectable( - inspectorInfo = debugInspectorInfo { - name = "span" - properties["texts"] = texts - properties["style"] = style - }, -) { - TextSpanData(texts = texts, style = style) -} +): Modifier = + inspectable( + inspectorInfo = debugInspectorInfo { + name = "span" + properties["texts"] = texts + properties["style"] = style + }, + ) { + TextSpanData(texts = texts, style = style) + } /** * 주어진 텍스트에 클릭 가능한 [SpanStyle]을 입힙니다. @@ -97,15 +99,16 @@ public fun Modifier.highlight( color = QuackColor.DuckieOrange.value, fontWeight = FontWeight.SemiBold, ), -): Modifier = inspectable( - inspectorInfo = debugInspectorInfo { - name = "highlight" - properties["highlights"] = highlights - properties["span"] = span - }, -) { - TextHighlightData(highlights = highlights, span = span) -} +): Modifier = + inspectable( + inspectorInfo = debugInspectorInfo { + name = "highlight" + properties["highlights"] = highlights + properties["span"] = span + }, + ) { + TextHighlightData(highlights = highlights, span = span) + } /** * 주어진 텍스트에 클릭 가능한 [SpanStyle]을 입힙니다. @@ -122,20 +125,21 @@ public fun Modifier.highlight( fontWeight = FontWeight.SemiBold, ), globalOnClick: (text: String) -> Unit, -): Modifier = inspectable( - inspectorInfo = debugInspectorInfo { - name = "highlight" - properties["texts"] = texts - properties["span"] = span - }, -) { - quackComposed { - val highlights = remember(texts, globalOnClick) { - texts.fastMap { text -> HighlightText(text, globalOnClick) } +): Modifier = + inspectable( + inspectorInfo = debugInspectorInfo { + name = "highlight" + properties["texts"] = texts + properties["span"] = span + }, + ) { + quackComposed { + val highlights = remember(texts, globalOnClick) { + texts.fastMap { text -> HighlightText(text, globalOnClick) } + } + TextHighlightData(highlights = highlights, span = span) } - TextHighlightData(highlights = highlights, span = span) } -} @VisibleForTesting internal object QuackTextErrors { @@ -184,6 +188,8 @@ public fun QuackText( error(QuackTextErrors.CannotUseSpanAndHighlightAtSameTime) } + @Suppress("NAME_SHADOWING") + val typography = rememberInterceptedStyleSafely(style = typography, modifier = modifier) val currentTypography = remember(typography, calculation = typography::asComposeStyle) val maxLines = if (singleLine) 1 else Int.MAX_VALUE diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/util/measurable.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/util/measurable.kt index f3c65e84f..dc26e60ab 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/util/measurable.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/util/measurable.kt @@ -8,20 +8,35 @@ package team.duckie.quackquack.ui.util import androidx.compose.runtime.Stable +import androidx.compose.ui.layout.LayoutIdParentData import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.util.fastFirstOrNull import team.duckie.quackquack.util.fastFilter +/** + * 주어진 [Measurable] 중 [LayoutIdParentData]가 [layoutId]와 일치하는 + * 첫 번째 [Measurable]을 반환합니다. 만약 일치하는 [Measurable]이 없다면 + * [IllegalStateException]이 발생합니다. + */ @Stable public operator fun List.get(layoutId: String): Measurable = fastFirstOrNull { measurable -> measurable.layoutId == layoutId } ?: error("No Measurable was found for the given layoutId($layoutId).") +/** + * 주어진 [Measurable] 중 [LayoutIdParentData]가 [layoutId]와 일치하는 + * 첫 번째 [Measurable]을 반환합니다. 만약 일치하는 [Measurable]이 없다면 + * `null`을 반환합니다. + */ @Stable public fun List.fastGetByIdOrNull(layoutId: String): Measurable? = fastFirstOrNull { measurable -> measurable.layoutId == layoutId } +/** + * 주어진 [Measurable] 중 [LayoutIdParentData]가 [layoutId]와 일치하는 + * [Measurable]들만 반환합니다. + */ @Stable public fun List.fastFilterById(layoutId: String): List = fastFilter { measurable -> measurable.layoutId == layoutId } diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/util/ux.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/util/ux.kt index c49db244c..55bf70468 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/util/ux.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/util/ux.kt @@ -30,7 +30,6 @@ import team.duckie.quackquack.util.DpSize @ReadOnlyComposable public inline fun currentFontScale(content: (fontScale: Float) -> T): T { contract { callsInPlace(content, InvocationKind.EXACTLY_ONCE) } - val configration = LocalConfiguration.current return content(configration.fontScale) } diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/commonutil/ComposeContentTestRule.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/common/ComposeContentTestRule.kt similarity index 82% rename from ui/src/test/kotlin/team/duckie/quackquack/ui/commonutil/ComposeContentTestRule.kt rename to ui/src/test/kotlin/team/duckie/quackquack/ui/common/ComposeContentTestRule.kt index 66fd9f901..a25cdcbc3 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/commonutil/ComposeContentTestRule.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/common/ComposeContentTestRule.kt @@ -5,12 +5,12 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.ui.commonutil +package team.duckie.quackquack.ui.common import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.ComposeContentTestRule import team.duckie.quackquack.material.theme.QuackTheme fun ComposeContentTestRule.setQuackContent(content: @Composable () -> Unit) { - setContent { QuackTheme(content) } + setContent { QuackTheme(content = content) } } diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/ImageSnapshot.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/ImageSnapshot.kt index 7688ca657..5c822d45c 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/ImageSnapshot.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/ImageSnapshot.kt @@ -10,17 +10,21 @@ package team.duckie.quackquack.ui.snapshot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import org.junit.Ignore +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import team.duckie.quackquack.ui.QuackImage -import team.duckie.quackquack.ui.snapshot.util.snapshotPath +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule @Ignore("인터넷 권한이 안되는 듯...?") @RunWith(AndroidJUnit4::class) class ImageSnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("image") + @Test fun QuackImage_url_png() { - captureRoboImage(snapshotPath("icon")) { + captureRoboImage(snapshotPath()) { val quackquackBanner = "https://github.com/duckie-team/quack-quack-android/blob/main/website/static/images/meta-banner.png?raw=true" QuackImage(quackquackBanner) diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/SwitchSnapshot.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/SwitchSnapshot.kt index 6418e39ae..592650039 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/SwitchSnapshot.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/SwitchSnapshot.kt @@ -15,15 +15,14 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import team.duckie.quackquack.ui.QuackSwitch -import team.duckie.quackquack.ui.snapshot.util.SnapshotPathGeneratorRule +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule +// FIXME: CompareOptions is a workaround for the "Verify components snapshot" failure in #788. @RunWith(AndroidJUnit4::class) class SwitchSnapshot { @get:Rule val snapshotPath = SnapshotPathGeneratorRule("switch") - // FIXME: CompareOptions is a workaround for the "Verify components snapshot" failure in #788. - @Test fun Enabled() { captureRoboImage( diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TabSnapshot.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TabSnapshot.kt index d8d5d8353..1fc91028b 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TabSnapshot.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TabSnapshot.kt @@ -18,7 +18,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import team.duckie.quackquack.ui.QuackTab -import team.duckie.quackquack.ui.snapshot.util.SnapshotPathGeneratorRule +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule @Config(qualifiers = "w400dp-h200dp") @RunWith(AndroidJUnit4::class) diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TagSnapshot.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TagSnapshot.kt index cce3a2dc4..f73b077ce 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TagSnapshot.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TagSnapshot.kt @@ -26,24 +26,20 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode import team.duckie.quackquack.material.icon.QuackIcon import team.duckie.quackquack.material.icon.quackicon.Outlined import team.duckie.quackquack.material.icon.quackicon.outlined.Heart -import team.duckie.quackquack.ui.commonutil.setQuackContent -import team.duckie.quackquack.ui.snapshot.util.BaseSnapshotPath -import team.duckie.quackquack.ui.snapshot.util.withIncreaseFontScale +import team.duckie.quackquack.ui.common.setQuackContent import team.duckie.quackquack.ui.sugar.QuackFilledTag import team.duckie.quackquack.ui.sugar.QuackGrayscaleFlatTag import team.duckie.quackquack.ui.sugar.QuackGrayscaleOutlinedTag import team.duckie.quackquack.ui.sugar.QuackOutlinedTag import team.duckie.quackquack.ui.trailingIcon import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi +import team.duckie.quackquack.util.compose.snapshot.test.BaseSnapshotPath +import team.duckie.quackquack.util.compose.snapshot.test.withIncreaseFontScale @RunWith(AndroidJUnit4::class) -@Config(manifest = Config.NONE) -@GraphicsMode(GraphicsMode.Mode.NATIVE) class TagSnapshot { private val testNameToSelectState = mutableMapOf() @@ -63,9 +59,9 @@ class TagSnapshot { options = RoborazziRule.Options( captureType = RoborazziRule.CaptureType.AllImage, outputFileProvider = { description, _, fileExtension -> - val testName = "$BaseSnapshotPath/tag/${description.methodName}" - val countedTestName = stateMarkedTestName(testName) - File("$countedTestName.$fileExtension") + val snapshotPath = "$BaseSnapshotPath/tag/${description.methodName}" + val countedSnapshotPath = stateMarkedTestName(snapshotPath) + File("$countedSnapshotPath.$fileExtension") }, ), ) diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextFieldSnapshot.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextFieldSnapshot.kt index 9fbdc529c..9b2c14045 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextFieldSnapshot.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextFieldSnapshot.kt @@ -33,14 +33,14 @@ import team.duckie.quackquack.ui.defaultTextFieldIcon import team.duckie.quackquack.ui.defaultTextFieldIndicator import team.duckie.quackquack.ui.filledTextFieldIcon import team.duckie.quackquack.ui.optin.ExperimentalDesignToken -import team.duckie.quackquack.ui.snapshot.util.LargestFontScale -import team.duckie.quackquack.ui.snapshot.util.MultilinesSnapshotQualifier -import team.duckie.quackquack.ui.snapshot.util.SnapshotPathGeneratorRule -import team.duckie.quackquack.ui.snapshot.util.TestColumn -import team.duckie.quackquack.ui.snapshot.util.WithLabel import team.duckie.quackquack.ui.token.HorizontalDirection import team.duckie.quackquack.ui.token.VerticalDirection import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi +import team.duckie.quackquack.util.compose.snapshot.test.LargestFontScale +import team.duckie.quackquack.util.compose.snapshot.test.MultilinesSnapshotQualifier +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule +import team.duckie.quackquack.util.compose.snapshot.test.TestColumn +import team.duckie.quackquack.util.compose.snapshot.test.WithLabel private const val MediumText = "가나다라마바사아자차카타파하" private const val LongText = "그러므로 주며, 없으면 우리 보라. 이것은 온갖 안고, 거선의 황금시대다." diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextSnapshot.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextSnapshot.kt index 64935efb0..9e46dfed8 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextSnapshot.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/TextSnapshot.kt @@ -23,9 +23,9 @@ import org.robolectric.annotation.Config import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.QuackTypography import team.duckie.quackquack.ui.QuackText -import team.duckie.quackquack.ui.snapshot.util.SnapshotPathGeneratorRule -import team.duckie.quackquack.ui.snapshot.util.TestColumn import team.duckie.quackquack.ui.span +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule +import team.duckie.quackquack.util.compose.snapshot.test.TestColumn @RunWith(AndroidJUnit4::class) class TextSnapshot { diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/SnapshotPathGenerator.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/SnapshotPathGenerator.kt deleted file mode 100644 index bd6910e14..000000000 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/SnapshotPathGenerator.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:Suppress("NOTHING_TO_INLINE") - -package team.duckie.quackquack.ui.snapshot.util - -import java.io.File -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -const val BaseSnapshotPath = "src/test/snapshots" - -inline fun snapshotPath(domain: String, isGif: Boolean = false): File { - return File("$BaseSnapshotPath/$domain/${getCurrentMethodName()}.${if (isGif) "gif" else "png"}") -} - -inline fun getCurrentMethodName(): String { - // https://stackoverflow.com/a/32329165/14299073 - return Thread.currentThread().stackTrace[1].methodName -} - -class SnapshotPathGeneratorRule(private val domain: String) : TestWatcher() { - init { - File("$BaseSnapshotPath/$domain").mkdirs() - } - - private var realtimeTestMethodName: String? = null - - override fun starting(description: Description) { - realtimeTestMethodName = description.methodName - } - - operator fun invoke(isGif: Boolean = false): File = - File("$BaseSnapshotPath/$domain/$realtimeTestMethodName.${if (isGif) "gif" else "png"}") -} diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/TestLayout.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/TestLayout.kt deleted file mode 100644 index d78ffc4bd..000000000 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/TestLayout.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.ui.snapshot.util - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import team.duckie.quackquack.material.theme.QuackTheme - -@Composable -fun TestColumn(contentGap: Dp = 15.dp, content: @Composable () -> Unit) { - Column( - modifier = Modifier.wrapContentSize(), - verticalArrangement = Arrangement.spacedBy(contentGap), - ) { - QuackTheme(content) - } -} - -@Composable -fun WithLabel( - label: String, - isTitle: Boolean = false, - labelGap: Dp = 4.dp, - contentGap: Dp = if (isTitle) 15.dp else 4.dp, - content: @Composable ColumnScope.() -> Unit, -) { - Column( - modifier = Modifier.wrapContentSize(), - verticalArrangement = Arrangement.spacedBy(labelGap), - ) { - BasicText( - label, - style = TextStyle.Default.let { style -> - if (isTitle) style.copy(fontWeight = FontWeight.Bold) else style - }, - ) - Column( - modifier = Modifier.wrapContentSize(), - verticalArrangement = Arrangement.spacedBy(contentGap), - content = content, - ) - } -} diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/UxTesting.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/UxTesting.kt deleted file mode 100644 index 9dcb13612..000000000 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/UxTesting.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.ui.snapshot.util - -import android.annotation.SuppressLint -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalConfiguration - -const val LargestFontScale = 2f // Maximum font scale based on Galaxy A31 - -@SuppressLint("ComposableNaming") -@Composable -fun withIncreaseFontScale(fontScale: Float, content: @Composable () -> Unit) { - val testConfiguration = LocalConfiguration.current.apply { - this.fontScale = fontScale - } - - CompositionLocalProvider( - LocalConfiguration provides testConfiguration, - content = content, - ) -} diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/qualifier.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/qualifier.kt deleted file mode 100644 index 8be005d20..000000000 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/snapshot/util/qualifier.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.ui.snapshot.util - -// Copied form Pixel7 qualifier -const val MultilinesSnapshotQualifier = "w400dp-h8000dp-normal-long-notround-any-420dpi-keyshidden-nonav" diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/ButtonTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/ButtonTest.kt index 37c5e146b..4de1aec96 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/ButtonTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/ButtonTest.kt @@ -20,7 +20,7 @@ import org.junit.Test import org.junit.runner.RunWith import team.duckie.quackquack.ui.QuackButton import team.duckie.quackquack.ui.QuackButtonStyle -import team.duckie.quackquack.ui.commonutil.setQuackContent +import team.duckie.quackquack.ui.common.setQuackContent import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi @RunWith(AndroidJUnit4::class) diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TabTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TabTest.kt index 44c18dd2e..cd2a1d37b 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TabTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TabTest.kt @@ -25,12 +25,11 @@ import org.junit.Test import org.junit.runner.RunWith import team.duckie.quackquack.ui.QuackTab import team.duckie.quackquack.ui.QuackTabScopeScope -import team.duckie.quackquack.ui.commonutil.setQuackContent +import team.duckie.quackquack.ui.common.setQuackContent import team.duckie.quackquack.ui.layoutIdWithTabIndexPairTextLayoutResult import team.duckie.quackquack.ui.tabIndex import team.duckie.quackquack.ui.textLayoutResult import team.duckie.quackquack.ui.util.rememberLtrTextMeasurer -import team.duckie.quackquack.util.Empty @RunWith(AndroidJUnit4::class) class TabTest { @@ -44,7 +43,7 @@ class TabTest { val layoutId = "AwesomeLayoutId" val tabIndex = 999 - val textLayoutResult = textMeasurer.measure(text = String.Empty) + val textLayoutResult = textMeasurer.measure(text = "") Box( Modifier @@ -78,7 +77,7 @@ class TabTest { tab( label = run { labelIndices[time] = candidateIndex - String.Empty + "" }, onClick = {}, ) diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TagTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TagTest.kt index d349963a8..14de09b6d 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TagTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TagTest.kt @@ -17,10 +17,9 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import team.duckie.quackquack.ui.QuackTagErrors.GrayscaleFlatStyleUnselectedState -import team.duckie.quackquack.ui.commonutil.setQuackContent +import team.duckie.quackquack.ui.common.setQuackContent import team.duckie.quackquack.ui.sugar.QuackGrayscaleFlatTag import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi -import team.duckie.quackquack.util.Empty @RunWith(AndroidJUnit4::class) class TagTest { @@ -31,7 +30,7 @@ class TagTest { fun GrayscaleFlatStyleDotAllowUnselectedState() { shouldThrowWithMessage(GrayscaleFlatStyleUnselectedState) { compose.setQuackContent { - QuackGrayscaleFlatTag(String.Empty, selected = false) {} + QuackGrayscaleFlatTag("", selected = false) {} } } } diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextFieldTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextFieldTest.kt index 200b34a5b..920e309ab 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextFieldTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextFieldTest.kt @@ -27,7 +27,7 @@ import team.duckie.quackquack.ui.TextFieldErrors.CannotAppliedCounterAndTrailing import team.duckie.quackquack.ui.TextFieldErrors.ValidationLabelProvidedButNoBottomDirectionIndicator import team.duckie.quackquack.ui.TextFieldErrors.sameDirectionIcon import team.duckie.quackquack.ui.TextFieldValidationState -import team.duckie.quackquack.ui.commonutil.setQuackContent +import team.duckie.quackquack.ui.common.setQuackContent import team.duckie.quackquack.ui.counter import team.duckie.quackquack.ui.defaultTextFieldIcon import team.duckie.quackquack.ui.defaultTextFieldIndicator @@ -35,7 +35,6 @@ import team.duckie.quackquack.ui.optin.ExperimentalDesignToken import team.duckie.quackquack.ui.token.HorizontalDirection import team.duckie.quackquack.ui.token.VerticalDirection import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi -import team.duckie.quackquack.util.Empty @RunWith(AndroidJUnit4::class) class TextFieldTest { @@ -56,7 +55,7 @@ class TextFieldTest { icon = QuackIcon.Outlined.Heart, direction = HorizontalDirection.Left, ), - value = String.Empty, + value = "", onValueChange = {}, style = QuackTextFieldStyle.Default, ) @@ -70,7 +69,7 @@ class TextFieldTest { compose.setQuackContent { QuackDefaultTextField( modifier = Modifier.defaultTextFieldIndicator(color = null, colorGetter = null), - value = String.Empty, + value = "", onValueChange = {}, style = QuackTextFieldStyle.Default, ) @@ -84,10 +83,10 @@ class TextFieldTest { compose.setQuackContent { QuackDefaultTextField( modifier = Modifier.defaultTextFieldIndicator(direction = VerticalDirection.Top), - value = String.Empty, + value = "", onValueChange = {}, style = QuackTextFieldStyle.Default, - validationState = TextFieldValidationState.Success(String.Empty), + validationState = TextFieldValidationState.Success(""), ) } } @@ -101,7 +100,7 @@ class TextFieldTest { modifier = Modifier .counter(10) .defaultTextFieldIcon(QuackIcon.Outlined.Heart), - value = String.Empty, + value = "", onValueChange = {}, style = QuackTextFieldStyle.Default, ) diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextTest.kt index 8f94b3182..712432a1b 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/uitest/TextTest.kt @@ -25,12 +25,11 @@ import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.QuackTypography import team.duckie.quackquack.ui.QuackText import team.duckie.quackquack.ui.QuackTextErrors -import team.duckie.quackquack.ui.commonutil.setQuackContent +import team.duckie.quackquack.ui.common.setQuackContent import team.duckie.quackquack.ui.highlight import team.duckie.quackquack.ui.span import team.duckie.quackquack.ui.uitest.util.markTest import team.duckie.quackquack.ui.uitest.util.onTest -import team.duckie.quackquack.util.Empty @RunWith(AndroidJUnit4::class) class TextTest { @@ -52,7 +51,7 @@ class TextTest { ), ) .highlight(highlights = emptyList()), - text = String.Empty, + text = "", typography = QuackTypography.Body1, ) } diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/utiltest/HashCodeTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/unittest/HashCodeTest.kt similarity index 98% rename from ui/src/test/kotlin/team/duckie/quackquack/ui/utiltest/HashCodeTest.kt rename to ui/src/test/kotlin/team/duckie/quackquack/ui/unittest/HashCodeTest.kt index 393b29248..600ae748f 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/utiltest/HashCodeTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/unittest/HashCodeTest.kt @@ -5,7 +5,7 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.ui.utiltest +package team.duckie.quackquack.ui.unittest import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize diff --git a/ui/src/test/kotlin/team/duckie/quackquack/ui/utiltest/NumberBuilderTest.kt b/ui/src/test/kotlin/team/duckie/quackquack/ui/unittest/NumberBuilderTest.kt similarity index 95% rename from ui/src/test/kotlin/team/duckie/quackquack/ui/utiltest/NumberBuilderTest.kt rename to ui/src/test/kotlin/team/duckie/quackquack/ui/unittest/NumberBuilderTest.kt index 232140456..bb319f2ce 100644 --- a/ui/src/test/kotlin/team/duckie/quackquack/ui/utiltest/NumberBuilderTest.kt +++ b/ui/src/test/kotlin/team/duckie/quackquack/ui/unittest/NumberBuilderTest.kt @@ -5,7 +5,7 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.ui.utiltest +package team.duckie.quackquack.ui.unittest import io.kotest.matchers.shouldBe import org.junit.Test diff --git a/util-compose-snapshot-test/build.gradle.kts b/util-compose-snapshot-test/build.gradle.kts index 49e9974c5..93857ddc8 100644 --- a/util-compose-snapshot-test/build.gradle.kts +++ b/util-compose-snapshot-test/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { libs.compose.runtime, libs.compose.ui.core, libs.compose.foundation, - libs.test.junit.core + libs.test.robolectric, + libs.test.junit.core, ) } diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPath.kt similarity index 60% rename from util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt rename to util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPath.kt index 41b1c4861..28cb18f0c 100644 --- a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPath.kt @@ -5,8 +5,6 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -@file:Suppress("NOTHING_TO_INLINE") - package team.duckie.quackquack.util.compose.snapshot.test import java.io.File @@ -15,16 +13,6 @@ import org.junit.runner.Description public const val BaseSnapshotPath: String = "src/test/snapshots" -public inline fun snapshotPath( - domain: String, - snapshotName: String = getCurrentMethodName(), - isGif: Boolean = false, -): File = - File("$BaseSnapshotPath/$domain/$snapshotName.${if (isGif) "gif" else "png"}") - -// https://stackoverflow.com/a/32329165/14299073 -public inline fun getCurrentMethodName(): String = Thread.currentThread().stackTrace[1].methodName - public class SnapshotPathGeneratorRule(private val domain: String) : TestWatcher() { init { File("$BaseSnapshotPath/$domain").mkdirs() @@ -33,11 +21,12 @@ public class SnapshotPathGeneratorRule(private val domain: String) : TestWatcher private var realtimeTestMethodName: String? = null override fun starting(description: Description) { - realtimeTestMethodName = description.methodName + val snapshotName = description.getAnnotation(SnapshotName::class.java)?.name ?: description.methodName + realtimeTestMethodName = snapshotName } public operator fun invoke(isGif: Boolean = false): File = - File("$BaseSnapshotPath/$domain/$realtimeTestMethodName.${if (isGif) "gif" else "png"}") + File("$BaseSnapshotPath/$domain/${requireNotNull(realtimeTestMethodName)}.${if (isGif) "gif" else "png"}") } @Target(AnnotationTarget.FUNCTION) diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt index 5c86cea1e..e8a0565aa 100644 --- a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt @@ -11,12 +11,15 @@ import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalConfiguration +import org.robolectric.RuntimeEnvironment public const val LargestFontScale: Float = 2f // Maximum font scale based on Galaxy A31 @SuppressLint("ComposableNaming") @Composable public fun withIncreaseFontScale(fontScale: Float, content: @Composable () -> Unit) { + RuntimeEnvironment.setFontScale(fontScale) + val testConfiguration = LocalConfiguration.current.apply { this.fontScale = fontScale } diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/robolectric.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/robolectric.kt new file mode 100644 index 000000000..c47e69705 --- /dev/null +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/robolectric.kt @@ -0,0 +1,21 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("unused") + +package team.duckie.quackquack.util.compose.snapshot.test + +import android.app.Activity +import org.robolectric.Robolectric +import org.robolectric.android.controller.ActivityController + +public val getActivityViaRobolectric: Activity + inline get() { + val activityController = Robolectric.buildActivity(Activity::class.java) + .also(ActivityController::setup) + return activityController.get() + } diff --git a/util/src/main/kotlin/team/duckie/quackquack/util/cast.kt b/util/src/main/kotlin/team/duckie/quackquack/util/cast.kt deleted file mode 100644 index 2fd7a8862..000000000 --- a/util/src/main/kotlin/team/duckie/quackquack/util/cast.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.util - -/** 주어진 값을 [T]로 unsafe casting 합니다. */ -public inline fun Any?.cast(): T = this as T diff --git a/util/src/main/kotlin/team/duckie/quackquack/util/convention.kt b/util/src/main/kotlin/team/duckie/quackquack/util/convention.kt index 4ebf6eb25..e8ec73cd2 100644 --- a/util/src/main/kotlin/team/duckie/quackquack/util/convention.kt +++ b/util/src/main/kotlin/team/duckie/quackquack/util/convention.kt @@ -9,14 +9,6 @@ package team.duckie.quackquack.util -/** - * MagicNumber을 예외적으로 허용할 때 사용하는 어노테이션입니다. - * - * @param because MagicNumber를 허용하는 이유 - */ -@Retention(AnnotationRetention.SOURCE) -public annotation class AllowMagicNumber(val because: String) - /** 주의깊게 사용해야 하는 API임을 나타냅니다. */ @MustBeDocumented @Retention(AnnotationRetention.BINARY) diff --git a/util/src/main/kotlin/team/duckie/quackquack/util/layout.kt b/util/src/main/kotlin/team/duckie/quackquack/util/layout.kt deleted file mode 100644 index 04d64c601..000000000 --- a/util/src/main/kotlin/team/duckie/quackquack/util/layout.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Designed and developed by Duckie Team, 2022~2023 - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE - */ - -@file:Suppress("unused") - -package team.duckie.quackquack.util - -import androidx.compose.runtime.Stable -import androidx.compose.ui.layout.Placeable -import androidx.compose.ui.unit.Constraints - -/** 0으로 고정된 [Constraints]을 반환합니다. */ -@Stable -public val Constraints.Companion.Zero: Constraints - get() = fixed(width = 0, height = 0) - -/** - * 주어진 사이즈로 고정된 [Constraints]를 반환합니다. - * - * 각각 주어진 사이즈로 min과 max를 모두 설정하는 [Constraints]의 유틸 함수입니다. - * 만약 인자로 주어진 값이 `null`이라면 min과 max에 해당하는 원래의 값을 그대로 사용합니다. - * - * @param width [Constraints]의 고정된 width 사이즈 - * @param height [Constraints]의 고정된 height 사이즈 - * - * @see Constraints - */ -@Stable -public fun Constraints.fixedCopy(width: Int? = null, height: Int? = null): Constraints = - Constraints( - minWidth = width ?: this.minWidth, - maxWidth = width ?: this.maxWidth, - minHeight = height ?: this.minHeight, - maxHeight = height ?: this.maxHeight, - ) - -/** 주어진 [Placeable]에서 width 값을 가져오거나, 유효하지 않을 경우 0을 반환합니다. */ -@Stable -public fun Placeable?.widthOrZero(): Int = this?.width ?: 0 - -/** 주어진 [Placeable]에서 height 값을 가져오거나, 유효하지 않을 경우 0을 반환합니다. */ -@Stable -public fun Placeable?.heightOrZero(): Int = this?.height ?: 0 diff --git a/util/src/main/kotlin/team/duckie/quackquack/util/padding.kt b/util/src/main/kotlin/team/duckie/quackquack/util/padding.kt deleted file mode 100644 index 35983a89e..000000000 --- a/util/src/main/kotlin/team/duckie/quackquack/util/padding.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Designed and developed by Duckie Team, 2022~2023 - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE - */ - -@file:Suppress("unused") - -package team.duckie.quackquack.util - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.runtime.Stable -import androidx.compose.ui.unit.dp - -/** 패딩이 없는 상태를 도메인적으로 나타내기 위해 사용됩니다. */ -@Stable -public val NoPadding: PaddingValues = PaddingValues(all = 0.dp) diff --git a/util/src/main/kotlin/team/duckie/quackquack/util/string.kt b/util/src/main/kotlin/team/duckie/quackquack/util/string.kt deleted file mode 100644 index abf31484c..000000000 --- a/util/src/main/kotlin/team/duckie/quackquack/util/string.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:Suppress("unused") - -package team.duckie.quackquack.util - -/** 공백 상태를 도메인적으로 나타내기 위해 사용됩니다. */ -public inline val String.Companion.Empty: String get() = ""