Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverProvider
import dev.appoutlet.some.core.StrategyProvider
import kotlin.random.Random
import dev.appoutlet.some.android.resolver.compose.ColorResolver as ComposeColorResolver

/**
* Contributes Android-specific resolvers discovered through `ServiceLoader`.
*
* The returned resolvers extend the shared `some-core` chain with Android-specific type support.
* The returned resolvers extend the shared `some-core` chain with Android framework and Compose type support.
*/
class AndroidResolverProvider : ResolverProvider {
/**
Expand All @@ -45,7 +46,8 @@ class AndroidResolverProvider : ResolverProvider {
PairResolver(random),
UserHandleResolver(random),
ColorResolver(random),
AnnotatedStringResolver(random)
AnnotatedStringResolver(random),
ComposeColorResolver(strategyProvider, random),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dev.appoutlet.some.android.resolver.compose

import androidx.compose.ui.graphics.Color
import dev.appoutlet.some.android.strategy.ColorStrategy
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.get
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

private const val HUE_MAX = 360f

/**
* Resolves Compose [Color] values using the active [ColorStrategy].
*
* @param strategyProvider Provider of all configured generation strategies.
* @param random Random source used when generating channel values.
*/
class ColorResolver(
strategyProvider: StrategyProvider,
private val random: Random
) : Resolver {
private val colorStrategy = strategyProvider.get<ColorStrategy>() ?: ColorStrategy.default

override fun canResolve(type: KType): Boolean = type == typeOf<Color>()

override fun resolve(type: KType, chain: ResolverChain): Any {
return when (colorStrategy) {
is ColorStrategy.RandomArgb -> resolveRandomArgb()
is ColorStrategy.RandomHsl -> resolveRandomHsl()
is ColorStrategy.Palette -> colorStrategy.colors.random(random)
}
}

private fun resolveRandomArgb(): Color {
return Color(
red = random.nextFloat(),
green = random.nextFloat(),
blue = random.nextFloat(),
alpha = random.nextFloat()
)
}

private fun resolveRandomHsl(): Color {
val hue = random.nextFloat() * HUE_MAX
val saturation = random.nextFloat()
val lightness = random.nextFloat()
val alpha = random.nextFloat()

return Color.hsl(
hue = hue,
saturation = saturation,
lightness = lightness,
alpha = alpha
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.appoutlet.some.android.strategy

import androidx.compose.ui.graphics.Color
import dev.appoutlet.some.config.Strategy

/**
* Strategy for generating Compose [Color] values.
*
* Available strategies:
* - [RandomArgb] - generates fully random red, green, blue, and alpha channels (default)
* - [RandomHsl] - generates colors from random hue, saturation, and lightness values
* - [Palette] - chooses from a fixed list of colors
*
* Example usage:
* ```kotlin
* someSetup {
* strategy(ColorStrategy.RandomHsl)
* }
* ```
*/
sealed interface ColorStrategy : Strategy {
override val key get() = ColorStrategy::class

/**
* Generates Compose colors with fully random ARGB channels.
*/
data object RandomArgb : ColorStrategy

/**
* Generates Compose colors from random HSL values.
*/
data object RandomHsl : ColorStrategy

/**
* Chooses colors from a fixed palette.
*
* @param colors Palette values eligible for generation.
*/
data class Palette(val colors: List<Color>) : ColorStrategy {
init {
require(colors.isNotEmpty()) { "Palette must contain at least one color" }
}
}

companion object {
/**
* The default Compose color strategy.
*/
val default: ColorStrategy get() = RandomArgb
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dev.appoutlet.some.config.DefaultStrategyProvider
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.random.Random
import dev.appoutlet.some.android.resolver.compose.ColorResolver as ComposeColorResolver

class AndroidResolverProviderTest {
@Test
Expand All @@ -35,7 +36,8 @@ class AndroidResolverProviderTest {
PairResolver::class,
UserHandleResolver::class,
ColorResolver::class,
AnnotatedStringResolver::class
AnnotatedStringResolver::class,
ComposeColorResolver::class,
),
resolvers.map { it::class }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import androidx.compose.ui.text.AnnotatedString
import dev.appoutlet.some.some
import org.junit.Assert.assertTrue
import org.junit.Test
import androidx.compose.ui.graphics.Color as ComposeColor

class AndroidServiceLoaderTest {
@Test
fun `top-level some also works with AndroidTypeResolverProvider`() {
fun `top-level some also works with AndroidResolverProvider`() {
val result: String = some()
assertTrue("Generated string should not be empty", result.isNotEmpty())
}
Expand All @@ -24,4 +25,10 @@ class AndroidServiceLoaderTest {
val result: AnnotatedString = some()
assertTrue("Generated AnnotatedString text should not be blank", result.text.isNotBlank())
}

@Test
fun `top-level some resolves Compose Color through AndroidResolverProvider`() {
val result: ComposeColor = some()
assertTrue(result.alpha in 0f..1f)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dev.appoutlet.some.android.resolver.compose

import androidx.compose.ui.graphics.Color
import dev.appoutlet.some.android.strategy.ColorStrategy
import dev.appoutlet.some.config.DefaultStrategyProvider
import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.core.ResolverChain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.random.Random
import kotlin.reflect.typeOf
import kotlin.test.assertNotNull

class ColorResolverTest {
@Test
fun `ColorResolver generates Compose Color values in default RandomArgb mode`() {
val resolver = ColorResolver(DefaultStrategyProvider(), FloatSequenceRandom(0.1f, 0.2f, 0.3f, 0.4f))

val result = resolver.resolve(typeOf<Color>(), testChain) as Color

assertEquals(Color(0.1f, 0.2f, 0.3f, 0.4f), result)
}

@Test
fun `ColorResolver RandomHsl produces HSL converted colors`() {
val resolver = ColorResolver(
DefaultStrategyProvider(mapOf(ColorStrategy::class to ColorStrategy.RandomHsl)),
Random.Default
)

val result = resolver.resolve(typeOf<Color>(), testChain) as Color

assertNotNull(result)
Comment thread
MessiasLima marked this conversation as resolved.
}

@Test
fun `ColorResolver Palette only returns configured colors`() {
val expected = Color(0.3f, 0.4f, 0.5f, 0.6f)
val resolver = ColorResolver(
DefaultStrategyProvider(mapOf(ColorStrategy::class to ColorStrategy.Palette(listOf(expected)))),
Random.Default
)

repeat(10) {
val result = resolver.resolve(typeOf<Color>(), testChain) as Color
assertEquals(expected, result)
}
}

@Test
fun `ColorResolver canResolve detects Compose Color type`() {
val resolver = ColorResolver(DefaultStrategyProvider(), Random.Default)
assertTrue(resolver.canResolve(typeOf<Color>()))
}

@Test
fun `ColorResolver rejects non-Compose Color types`() {
val resolver = ColorResolver(DefaultStrategyProvider(), Random.Default)
assertFalse(resolver.canResolve(typeOf<String>()))
assertFalse(resolver.canResolve(typeOf<Int>()))
assertFalse(resolver.canResolve(typeOf<android.graphics.Color>()))
}

private class FloatSequenceRandom(vararg values: Float) : Random() {
private val values = values.toList()
private var index = 0

override fun nextBits(bitCount: Int): Int = 0

override fun nextFloat(): Float {
return values.getOrElse(index++) { error("No more float values configured for test random") }
}
}

companion object {
private val testChain = ResolverChain(emptyList(), NullableStrategy.NullOnCircularReference)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.appoutlet.some.android.strategy

import androidx.compose.ui.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test

class ColorStrategyTest {
@Test
fun `ColorStrategy default is RandomArgb`() {
assertEquals(ColorStrategy.RandomArgb, ColorStrategy.default)
}

@Test(expected = IllegalArgumentException::class)
fun `ColorStrategy Palette rejects empty color lists`() {
ColorStrategy.Palette(emptyList<Color>())
}
}
114 changes: 114 additions & 0 deletions docs/android/color.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
icon: lucide/palette
---

# Compose Color

`some-android` includes a resolver for `androidx.compose.ui.graphics.Color`.

Once `some-android` is on your test classpath, `some<ComposeColor>()` works without custom factories or custom resolvers.

!!! note "Android-only feature"

`ColorStrategy` is part of `some-android`. It is not available from `some-core`.

## Basic usage

```kotlin
import androidx.compose.ui.graphics.Color as ComposeColor
import dev.appoutlet.some.some

val color: ComposeColor = some()
```

Use an import alias like `ComposeColor` when the same file also references `android.graphics.Color`.

## Color strategy

Use `ColorStrategy` when the distribution of generated Compose colors matters for the code you are testing.

| Strategy | Default | What it generates | Good for |
|----------|---------|-------------------|----------|
| `ColorStrategy.RandomArgb` | Yes | Fully random red, green, blue, and alpha channels | General UI tests that only need any valid Compose color |
| `ColorStrategy.RandomHsl` | No | Random hue, saturation, and lightness converted to a Compose color | Tests that want a more color-model-aware distribution |
| `ColorStrategy.Palette` | No | A random value chosen from a fixed list of Compose colors | Design-system palettes, brand colors, and snapshot stability |

### Configure it per call

```kotlin
import androidx.compose.ui.graphics.Color as ComposeColor
import dev.appoutlet.some.android.strategy.ColorStrategy
import dev.appoutlet.some.some

val accent = some<ComposeColor> {
strategy(ColorStrategy.RandomHsl)
}
```

### Configure it once and reuse it

```kotlin
import androidx.compose.ui.graphics.Color as ComposeColor
import dev.appoutlet.some.android.strategy.ColorStrategy
import dev.appoutlet.some.someSetup

val some = someSetup {
strategy(
ColorStrategy.Palette(
listOf(
ComposeColor(0.40f, 0.23f, 0.72f, 1f),
ComposeColor(0.15f, 0.61f, 0.84f, 1f),
ComposeColor(0.96f, 0.57f, 0.17f, 1f)
)
)
)
}

val primary: ComposeColor = some()
val secondary: ComposeColor = some()
```

## What the resolvers generate

- `ColorStrategy.RandomArgb` randomizes red, green, blue, and alpha independently
- `ColorStrategy.RandomHsl` converts randomized hue, saturation, and lightness values into RGB channels
- `ColorStrategy.Palette` only returns colors from the configured palette

## Picking the right strategy

- Use `ColorStrategy.RandomArgb` when your test only needs any valid Compose color
- Use `ColorStrategy.RandomHsl` when hue or tonal distribution matters more than raw channel randomness
- Use `ColorStrategy.Palette` when your UI should stay inside a known set of allowed colors

## Examples

=== "RandomArgb (default)"

```kotlin
val color: ComposeColor = some()
```

=== "RandomHsl"

```kotlin
val color = some<ComposeColor> {
strategy(ColorStrategy.RandomHsl)
}
```

=== "Palette"

```kotlin
val color = some<ComposeColor> {
strategy(
ColorStrategy.Palette(
listOf(
ComposeColor(0.12f, 0.13f, 0.18f, 1f),
ComposeColor(0.87f, 0.33f, 0.29f, 1f)
)
)
)
}
```

See [Android](index.md) for installation and the list of Android-specific supported types.
Loading