Feature: Bring-style emoji & category icons for items

Every shopping item now gets a visual like in Bring: a product emoji on
a softly tinted category tile.

- ItemVisuals.kt: pure client-side classifier. German dictionary
  (~180 entries) maps item names to emoji + one of 13 categories
  (produce, drinks, bakery, dairy, meat&fish, frozen, pantry, sweets,
  household, drugstore, baby, pet, other), each with an accent color.
  Longest-keyword-first matching so compounds resolve to their most
  specific entry (Wassermelone is produce, not a drink); juice
  compounds (Apfelsaft, Orangensaft, ...) are listed explicitly because
  their fruit prefix is longer than 'saft'. Unknown items fall back to
  the generic cart on neutral grey. Works fully offline, nothing leaves
  the device.
- ListDetailScreen: emoji tile in every item row, emoji in the
  autocomplete suggestions dropdown, and a live emoji preview as the
  text field's leading icon while typing.
- ItemVisualsTest: longest-match priority, case/whitespace
  insensitivity, fallbacks, category coverage.

Verified: assembleDebug + unit tests green (JDK 21).
This commit is contained in:
Tronax 2026-08-22 10:16:37 +02:00
parent 15b3f1071f
commit 37cbb816e5
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
5 changed files with 375 additions and 7 deletions

View file

@ -0,0 +1,254 @@
package com.example.mitbringsl.ui.detail
import androidx.compose.ui.graphics.Color
import com.example.mitbringsl.ui.detail.ItemCategory.*
/**
* Bring-style visual classification of shopping items: every item gets an emoji
* plus a category with an accent color, resolved purely client-side from a
* German keyword dictionary (works offline, no server round-trip).
*
* Matching is longest-keyword-first substring matching, so compound groceries
* resolve to their most specific entry ("Apfelsaft" -> drinks, not the apple
* entry it contains).
*/
enum class ItemCategory(val label: String, val colorHex: Long, val fallbackEmoji: String) {
PRODUCE("Obst & Gemüse", 0xFF43A047, "🥕"),
DRINKS("Getränke", 0xFF2563EB, "🧃"),
BAKERY("Backwaren", 0xFFD97706, "🥖"),
DAIRY("Milchprodukte & Eier", 0xFF60A5FA, "🥛"),
MEAT_FISH("Fleisch & Fisch", 0xFFE53935, "🥩"),
FROZEN("Tiefkühl", 0xFF06B6D4, "🧊"),
PANTRY("Vorrat", 0xFF8D6E63, "🥫"),
SWEETS("Süßes & Snacks", 0xFF9C27B0, "🍫"),
HOUSEHOLD("Haushalt", 0xFF78909C, "🧽"),
DRUGSTORE("Drogerie", 0xFF26A69A, "🧴"),
BABY("Baby", 0xFFF48FB1, "🍼"),
PET("Haustier", 0xFF795548, "🐾"),
OTHER("Sonstiges", 0xFF9E9E9E, "🛒"),
}
/** Accent color of a category, used as a soft tint behind the emoji tile. */
val ItemCategory.color: Color get() = Color(colorHex)
data class ItemVisual(val emoji: String, val category: ItemCategory)
private fun v(emoji: String, category: ItemCategory) = ItemVisual(emoji, category)
// German grocery dictionary. Order within a category is irrelevant (matching
// sorts by keyword length); compound-specific entries must simply exist.
private val dictionary: List<Pair<String, ItemVisual>> = listOf(
// --- Obst & Gemüse ---
"obst" to v("🍎", PRODUCE), "gemüse" to v("🥬", PRODUCE),
"apfel" to v("🍎", PRODUCE), "granatapfel" to v("🍎", PRODUCE),
"banane" to v("🍌", PRODUCE), "birne" to v("🍐", PRODUCE),
"orange" to v("🍊", PRODUCE), "mandarine" to v("🍊", PRODUCE),
"clementine" to v("🍊", PRODUCE), "zitron" to v("🍋", PRODUCE),
"limette" to v("🍋", PRODUCE), "grapefruit" to v("🍊", PRODUCE),
"pfirsich" to v("🍑", PRODUCE), "aprikose" to v("🍑", PRODUCE),
"nectarine" to v("🍑", PRODUCE), "pflaume" to v("🍑", PRODUCE),
"zwetschge" to v("🍑", PRODUCE), "kirsche" to v("🍒", PRODUCE),
"trauben" to v("🍇", PRODUCE), "erdbeer" to v("🍓", PRODUCE),
"himbeer" to v("🍓", PRODUCE), "brombeer" to v("🍓", PRODUCE),
"blaubeer" to v("🫐", PRODUCE), "heidelbeer" to v("🫐", PRODUCE),
"johannisbeer" to v("🫐", PRODUCE), "kiwi" to v("🥝", PRODUCE),
"ananas" to v("🍍", PRODUCE), "mango" to v("🥭", PRODUCE),
"melone" to v("🍈", PRODUCE), "wassermelone" to v("🍉", PRODUCE),
"honigmelone" to v("🍈", PRODUCE), "avocado" to v("🥑", PRODUCE),
"avokado" to v("🥑", PRODUCE), "tomat" to v("🍅", PRODUCE),
"eisbergsalat" to v("🥬", PRODUCE), "gurke" to v("🥒", PRODUCE),
"paprika" to v("🫑", PRODUCE), "chili" to v("🌶️", PRODUCE),
"peperoni" to v("🌶️", PRODUCE), "zwiebel" to v("🧅", PRODUCE),
"knoblauch" to v("🧄", PRODUCE), "lauch" to v("🧅", PRODUCE),
"porree" to v("🧅", PRODUCE), "schnittlauch" to v("🌿", PRODUCE),
"karotte" to v("🥕", PRODUCE), "möhre" to v("🥕", PRODUCE),
"moehre" to v("🥕", PRODUCE), "mohrrübe" to v("🥕", PRODUCE),
"kartoffel" to v("🥔", PRODUCE), "süßkartoffel" to v("🥔", PRODUCE),
"süsskartoffel" to v("🥔", PRODUCE), "salat" to v("🥬", PRODUCE),
"rucola" to v("🥬", PRODUCE), "spinat" to v("🥬", PRODUCE),
"mangold" to v("🥬", PRODUCE), "brokkoli" to v("🥦", PRODUCE),
"blumenkohl" to v("🥦", PRODUCE), "kohlrabi" to v("🥦", PRODUCE),
"rosenkohl" to v("🥦", PRODUCE), "zucchini" to v("🥒", PRODUCE),
"kürbis" to v("🎃", PRODUCE), "aubergine" to v("🍆", PRODUCE),
"pilz" to v("🍄", PRODUCE), "champignon" to v("🍄", PRODUCE),
"mais" to v("🌽", PRODUCE), "erbsen" to v("🥬", PRODUCE),
"bohnen" to v("🥬", PRODUCE), "ingwer" to v("🥕", PRODUCE),
"radieschen" to v("🥬", PRODUCE), "sellerie" to v("🥬", PRODUCE),
"kräuter" to v("🌿", PRODUCE), "petersilie" to v("🌿", PRODUCE),
"basilikum" to v("🌿", PRODUCE), "minze" to v("🌿", PRODUCE),
"rosmarin" to v("🌿", PRODUCE), "thymian" to v("🌿", PRODUCE),
"oregano" to v("🌿", PRODUCE), "dill" to v("🌿", PRODUCE),
// --- Getränke ---
// Juice compounds listed explicitly: longest-match alone would resolve
// "apfelsaft" to the apple entry ("apfel" is longer than "saft").
"apfelsaft" to v("🧃", DRINKS), "apfelschorle" to v("🧃", DRINKS),
"orangensaft" to v("🧃", DRINKS), "traubensaft" to v("🧃", DRINKS),
"kirschsaft" to v("🧃", DRINKS), "bananensaft" to v("🧃", DRINKS),
"tomatensaft" to v("🧃", DRINKS), "granatapfelsaft" to v("🧃", DRINKS),
"johannisbeersaft" to v("🧃", DRINKS), "maracujasaft" to v("🧃", DRINKS),
"wasser" to v("💧", DRINKS), "saft" to v("🧃", DRINKS),
"obstsaft" to v("🧃", DRINKS), "fruchtsaft" to v("🧃", DRINKS),
"limonade" to v("🥤", DRINKS), "cola" to v("🥤", DRINKS),
"bier" to v("🍺", DRINKS), "radler" to v("🍺", DRINKS),
"wein" to v("🍷", DRINKS), "prosecco" to v("🍾", DRINKS),
"sekt" to v("🍾", DRINKS), "champagner" to v("🍾", DRINKS),
"kaffee" to v("", DRINKS), "espresso" to v("", DRINKS),
"cappuccino" to v("", DRINKS), "tee" to v("🍵", DRINKS),
"eistee" to v("🧃", DRINKS), "smoothie" to v("🥤", DRINKS),
"kakao" to v("", DRINKS), "energy" to v("🥤", DRINKS),
"sprudel" to v("💧", DRINKS), "schnaps" to v("🥃", DRINKS),
"wodka" to v("🥃", DRINKS), "gin" to v("🥃", DRINKS),
"whisky" to v("🥃", DRINKS), "whiskey" to v("🥃", DRINKS),
"rum" to v("🥃", DRINKS), "flasche" to v("🍾", DRINKS),
// --- Backwaren ---
"knoblauchbrot" to v("🥖", BAKERY), "brot" to v("🍞", BAKERY),
"toast" to v("🍞", BAKERY), "brötchen" to v("🥖", BAKERY),
"broetchen" to v("🥖", BAKERY), "semmel" to v("🥖", BAKERY),
"baguette" to v("🥖", BAKERY), "ciabatta" to v("🥖", BAKERY),
"croissant" to v("🥐", BAKERY), "bagel" to v("🥯", BAKERY),
"brezel" to v("🥨", BAKERY), "kuchen" to v("🍰", BAKERY),
"torte" to v("🍰", BAKERY), "donut" to v("🍩", BAKERY),
"berliner" to v("🍩", BAKERY), "krapfen" to v("🍩", BAKERY),
"muffin" to v("🧁", BAKERY), "cupcake" to v("🧁", BAKERY),
"stollen" to v("🍰", BAKERY), "pfannkuchen" to v("🥞", BAKERY),
"crepe" to v("🥞", BAKERY), "waffel" to v("🥞", BAKERY),
"zwieback" to v("🍞", BAKERY),
// --- Milchprodukte & Eier ---
"milch" to v("🥛", DAIRY), "butter" to v("🧈", DAIRY),
"käse" to v("🧀", DAIRY), "camembert" to v("🧀", DAIRY),
"gouda" to v("🧀", DAIRY), "mozzarella" to v("🧀", DAIRY),
"feta" to v("🧀", DAIRY), "halloumi" to v("🧀", DAIRY),
"parmesan" to v("🧀", DAIRY), "mascarpone" to v("🧀", DAIRY),
"ricotta" to v("🧀", DAIRY), "quark" to v("🥛", DAIRY),
"joghurt" to v("🥛", DAIRY), "sahne" to v("🥛", DAIRY),
"schmand" to v("🥛", DAIRY), "fraiche" to v("🥛", DAIRY),
"fraîche" to v("🥛", DAIRY), "eier" to v("🥚", DAIRY),
"eigelb" to v("🥚", DAIRY), "eiweiß" to v("🥚", DAIRY),
"ei" to v("🥚", DAIRY), "pudding" to v("🍮", DAIRY),
// --- Fleisch & Fisch ---
"hackfleisch" to v("🥩", MEAT_FISH), "gehacktes" to v("🥩", MEAT_FISH),
"fleisch" to v("🥩", MEAT_FISH), "steak" to v("🥩", MEAT_FISH),
"filet" to v("🥩", MEAT_FISH), "schwein" to v("🥩", MEAT_FISH),
"hähnchen" to v("🍗", MEAT_FISH), "haehnchen" to v("🍗", MEAT_FISH),
"huhn" to v("🍗", MEAT_FISH), "pute" to v("🍗", MEAT_FISH),
"truthahn" to v("🦃", MEAT_FISH), "ente" to v("🦆", MEAT_FISH),
"schinken" to v("🥓", MEAT_FISH), "speck" to v("🥓", MEAT_FISH),
"bacon" to v("🥓", MEAT_FISH), "wurst" to v("🌭", MEAT_FISH),
"salami" to v("🌭", MEAT_FISH), "wiener" to v("🌭", MEAT_FISH),
"frankfurter" to v("🌭", MEAT_FISH), "mett" to v("🥩", MEAT_FISH),
"gyros" to v("🥙", MEAT_FISH), "döner" to v("🥙", MEAT_FISH),
"kebab" to v("🥙", MEAT_FISH), "kebap" to v("🥙", MEAT_FISH),
"schnitzel" to v("🍖", MEAT_FISH), "fisch" to v("🐟", MEAT_FISH),
"lachs" to v("🐟", MEAT_FISH), "forelle" to v("🐟", MEAT_FISH),
"karpfen" to v("🐟", MEAT_FISH), "hering" to v("🐟", MEAT_FISH),
"makrele" to v("🐟", MEAT_FISH), "garnelen" to v("🦐", MEAT_FISH),
"krabben" to v("🦐", MEAT_FISH), "scampi" to v("🦐", MEAT_FISH),
"shrimp" to v("🦐", MEAT_FISH), "muscheln" to v("🦪", MEAT_FISH),
"austern" to v("🦪", MEAT_FISH),
// --- Tiefkühl ---
"tiefkühl" to v("🧊", FROZEN), "gefroren" to v("🧊", FROZEN),
"eiscreme" to v("🍦", FROZEN), "eis" to v("🍦", FROZEN),
"pommes" to v("🍟", FROZEN), "pizza" to v("🍕", FROZEN),
// --- Vorrat ---
"nudeln" to v("🍝", PANTRY), "pasta" to v("🍝", PANTRY),
"spaghetti" to v("🍝", PANTRY), "penne" to v("🍝", PANTRY),
"lasagne" to v("🍝", PANTRY), "reis" to v("🍚", PANTRY),
"risotto" to v("🍚", PANTRY), "couscous" to v("🍚", PANTRY),
"bulgur" to v("🍚", PANTRY), "quinoa" to v("🍚", PANTRY),
"mehl" to v("🌾", PANTRY), "zucker" to v("🍚", PANTRY),
"salz" to v("🧂", PANTRY), "pfeffer" to v("🧂", PANTRY),
"gewürz" to v("🧂", PANTRY), "curry" to v("🧂", PANTRY),
"paprikapulver" to v("🧂", PANTRY), "öl" to v("🫒", PANTRY),
"olive" to v("🫒", PANTRY), "essig" to v("🍶", PANTRY),
"balsamico" to v("🍶", PANTRY), "sojasauce" to v("🍶", PANTRY),
"sauce" to v("🍶", PANTRY), "soße" to v("🍶", PANTRY),
"sosse" to v("🍶", PANTRY), "senf" to v("🥫", PANTRY),
"ketchup" to v("🍅", PANTRY), "mayo" to v("🥫", PANTRY),
"mayonnaise" to v("🥫", PANTRY), "marmelade" to v("🍯", PANTRY),
"honig" to v("🍯", PANTRY), "müsli" to v("🥣", PANTRY),
"haferflocken" to v("🥣", PANTRY), "cornflakes" to v("🥣", PANTRY),
"erdnuss" to v("🥜", PANTRY), "nuss" to v("🥜", PANTRY),
"mandel" to v("🥜", PANTRY), "pistazie" to v("🥜", PANTRY),
"cashew" to v("🥜", PANTRY), "linsen" to v("🥣", PANTRY),
"kichererbsen" to v("🥣", PANTRY), "passierte" to v("🥫", PANTRY),
"tomatenmark" to v("🥫", PANTRY), "tomatensauce" to v("🥫", PANTRY),
"tomatensoße" to v("🥫", PANTRY), "tomatensose" to v("🥫", PANTRY),
"dose" to v("🥫", PANTRY), "kokosmilch" to v("🥥", PANTRY),
"kokos" to v("🥥", PANTRY), "suppe" to v("🥫", PANTRY),
"brühe" to v("🥫", PANTRY), "bruehe" to v("🥫", PANTRY),
"backpulver" to v("🥣", PANTRY),
// --- Süßes & Snacks ---
"schokolade" to v("🍫", SWEETS), "riegel" to v("🍫", SWEETS),
"gummibärchen" to v("🍬", SWEETS), "gummi" to v("🍬", SWEETS),
"bonbon" to v("🍬", SWEETS), "lutscher" to v("🍭", SWEETS),
"karamell" to v("🍬", SWEETS), "marzipan" to v("🍬", SWEETS),
"pralinen" to v("🍫", SWEETS), "keks" to v("🍪", SWEETS),
"plätzchen" to v("🍪", SWEETS), "biscuit" to v("🍪", SWEETS),
"chips" to v("🍿", SWEETS), "popcorn" to v("🍿", SWEETS),
"salzstangen" to v("🍿", SWEETS), "studentenfutter" to v("🥜", SWEETS),
// --- Haushalt ---
"spülmittel" to v("🧽", HOUSEHOLD), "spüli" to v("🧽", HOUSEHOLD),
"waschmittel" to v("🧼", HOUSEHOLD), "weichspüler" to v("🧺", HOUSEHOLD),
"reiniger" to v("🧽", HOUSEHOLD), "putzmittel" to v("🧽", HOUSEHOLD),
"geschirr" to v("🍽️", HOUSEHOLD), "toilettenpapier" to v("🧻", HOUSEHOLD),
"klopapier" to v("🧻", HOUSEHOLD), "backpapier" to v("🧻", HOUSEHOLD),
"papier" to v("🧻", HOUSEHOLD), "küchenrolle" to v("🧻", HOUSEHOLD),
"müll" to v("🗑️", HOUSEHOLD), "abfall" to v("🗑️", HOUSEHOLD),
"besen" to v("🧹", HOUSEHOLD), "schwamm" to v("🧽", HOUSEHOLD),
"servietten" to v("🍽️", HOUSEHOLD), "folie" to v("🥡", HOUSEHOLD),
"batterie" to v("🔋", HOUSEHOLD), "glühbirne" to v("💡", HOUSEHOLD),
"kerze" to v("🕯️", HOUSEHOLD), "schneidebrett" to v("🍽️", HOUSEHOLD),
"eimer" to v("🧽", HOUSEHOLD),
// --- Drogerie ---
"zahnpasta" to v("🪥", DRUGSTORE), "zahncreme" to v("🪥", DRUGSTORE),
"zahnbürste" to v("🪥", DRUGSTORE), "zahn" to v("🪥", DRUGSTORE),
"shampoo" to v("🧴", DRUGSTORE), "duschgel" to v("🧴", DRUGSTORE),
"duschbad" to v("🧴", DRUGSTORE), "seife" to v("🧼", DRUGSTORE),
"deo" to v("🧴", DRUGSTORE), "rasier" to v("🪒", DRUGSTORE),
"pflaster" to v("🩹", DRUGSTORE), "salbe" to v("🩹", DRUGSTORE),
"tabletten" to v("💊", DRUGSTORE), "medikament" to v("💊", DRUGSTORE),
"vitamin" to v("💊", DRUGSTORE), "hustensaft" to v("💊", DRUGSTORE),
"sonnencreme" to v("🧴", DRUGSTORE), "sonnenmilch" to v("🧴", DRUGSTORE),
"creme" to v("🧴", DRUGSTORE), "lotion" to v("🧴", DRUGSTORE),
"binden" to v("🩹", DRUGSTORE), "tampon" to v("🩹", DRUGSTORE),
// --- Baby ---
"babyflasche" to v("🍼", BABY), "babybrei" to v("🍼", BABY),
"baby" to v("👶", BABY), "windel" to v("👶", BABY),
"brei" to v("🍼", BABY), "milchpulver" to v("🍼", BABY),
"folgemilch" to v("🍼", BABY), "schnuller" to v("🍼", BABY),
"feuchttücher" to v("👶", BABY),
// --- Haustier ---
"hundefutter" to v("🐕", PET), "katzenfutter" to v("🐈", PET),
"katzenstreu" to v("🐈", PET), "futter" to v("🐾", PET),
"hund" to v("🐕", PET), "katze" to v("🐈", PET),
"leckerli" to v("🐾", PET),
)
// Longest keyword first: specific compound entries win over their generic
// substrings ("apfelsaft" beats "apfel", "kokosmilch" beats "milch").
private val sortedEntries: List<Pair<String, ItemVisual>> =
dictionary.sortedByDescending { it.first.length }
/**
* Resolves the visual (emoji + category) for an item name. Unknown items fall
* back to the generic 🛒 / Sonstiges.
*/
fun itemVisualFor(name: String): ItemVisual {
val n = name.trim().lowercase()
if (n.isEmpty()) return ItemVisual(ItemCategory.OTHER.fallbackEmoji, ItemCategory.OTHER)
for ((keyword, visual) in sortedEntries) {
if (keyword in n) return visual
}
return ItemVisual(ItemCategory.OTHER.fallbackEmoji, ItemCategory.OTHER)
}

View file

@ -108,17 +108,24 @@ fun ListDetailScreen(
) {
Column {
suggestions.take(4).forEach { suggestion ->
Text(
text = suggestion,
style = MaterialTheme.typography.bodyMedium,
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
viewModel.addItem(suggestion, quantityInput)
quantityInput = ""
}
.padding(horizontal = 16.dp, vertical = 12.dp)
)
.padding(horizontal = 16.dp, vertical = 10.dp)
) {
val visual = remember(suggestion) { itemVisualFor(suggestion) }
Text(text = visual.emoji, fontSize = 18.sp)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = suggestion,
style = MaterialTheme.typography.bodyMedium
)
}
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f))
}
}
@ -135,6 +142,14 @@ fun ListDetailScreen(
placeholder = { Text("Artikel hinzufügen...") },
singleLine = true,
shape = RoundedCornerShape(16.dp),
leadingIcon = if (query.isBlank()) null else {
{
Text(
text = itemVisualFor(query).emoji,
fontSize = 18.sp
)
}
},
modifier = Modifier.weight(1.5f),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
@ -341,10 +356,23 @@ fun ItemRow(
onCheckedChange = { onToggle() }
)
val visual = remember(item.name) { itemVisualFor(item.name) }
Box(
modifier = Modifier
.size(40.dp)
.background(
color = visual.category.color.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Text(text = visual.emoji, fontSize = 18.sp)
}
Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
.padding(start = 12.dp)
) {
Text(
text = item.name,

View file

@ -0,0 +1,78 @@
package com.example.mitbringsl.ui.detail
import com.example.mitbringsl.ui.detail.ItemCategory.BABY
import com.example.mitbringsl.ui.detail.ItemCategory.BAKERY
import com.example.mitbringsl.ui.detail.ItemCategory.DAIRY
import com.example.mitbringsl.ui.detail.ItemCategory.DRINKS
import com.example.mitbringsl.ui.detail.ItemCategory.DRUGSTORE
import com.example.mitbringsl.ui.detail.ItemCategory.FROZEN
import com.example.mitbringsl.ui.detail.ItemCategory.HOUSEHOLD
import com.example.mitbringsl.ui.detail.ItemCategory.MEAT_FISH
import com.example.mitbringsl.ui.detail.ItemCategory.OTHER
import com.example.mitbringsl.ui.detail.ItemCategory.PANTRY
import com.example.mitbringsl.ui.detail.ItemCategory.PET
import com.example.mitbringsl.ui.detail.ItemCategory.PRODUCE
import com.example.mitbringsl.ui.detail.ItemCategory.SWEETS
import org.junit.Assert.assertEquals
import org.junit.Test
class ItemVisualsTest {
@Test
fun simpleProductsResolveToTheirEmoji() {
assertEquals("🥛", itemVisualFor("Milch").emoji)
assertEquals(DAIRY, itemVisualFor("Milch").category)
assertEquals("🍞", itemVisualFor("Brot").emoji)
assertEquals(PRODUCE, itemVisualFor("Banane").category)
}
@Test
fun longestKeywordWinsOverContainedGeneric() {
// "Apfelsaft" contains "apfel" but must resolve as a drink.
assertEquals(DRINKS, itemVisualFor("Apfelsaft").category)
// "Wassermelone" contains "wasser" but is produce.
assertEquals(PRODUCE, itemVisualFor("Wassermelone").category)
// "Kokosmilch" must not fall back to dairy milk.
assertEquals(PANTRY, itemVisualFor("Kokosmilch").category)
// "Eisbergsalat" must not be frozen.
assertEquals(PRODUCE, itemVisualFor("Eisbergsalat").category)
// "Teewurst": "wurst" (5) beats "tee" (3).
assertEquals(MEAT_FISH, itemVisualFor("Teewurst").category)
}
@Test
fun matchingIsCaseAndWhitespaceInsensitive() {
assertEquals(itemVisualFor("banane"), itemVisualFor("BANANE"))
assertEquals(itemVisualFor("banane"), itemVisualFor(" Banane "))
}
@Test
fun unknownAndBlankItemsFallBackToGenericCart() {
assertEquals(OTHER, itemVisualFor("Dingsbums").category)
assertEquals("🛒", itemVisualFor("Dingsbums").emoji)
assertEquals(OTHER, itemVisualFor("").category)
assertEquals(OTHER, itemVisualFor(" ").category)
}
@Test
fun categoriesCoverCommonSupermarketSections() {
mapOf(
"Brot" to BAKERY,
"Brötchen" to BAKERY,
"Hackfleisch" to MEAT_FISH,
"Lachs" to MEAT_FISH,
"Nudeln" to PANTRY,
"Tomatenmark" to PANTRY,
"Schokolade" to SWEETS,
"Pommes" to FROZEN,
"Spülmittel" to HOUSEHOLD,
"Klopapier" to HOUSEHOLD,
"Zahnpasta" to DRUGSTORE,
"Hundefutter" to PET,
"Windeln" to BABY,
"Eier" to DAIRY,
).forEach { (name, category) ->
assertEquals("$name should be $category", category, itemVisualFor(name).category)
}
}
}