Fix #12: Interessen als farbige Chips mit Autocomplete
- InterestTagHelper: parse/join/add/remove/suggestions (alphabetisch sortiert) - InterestChipRow: wiederverwendbare Display-Komponente (grün/rot) - InterestTagEditor: Chip-Editor mit × + Tipp-Autocomplete - AddPersonView, PersonDetailView, IchView auf neue Komponenten umgestellt - 20 InterestTagHelper-Tests in StoreTests - Lokalisierung: "Tag hinzufügen…" → "Add tag…" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ struct AddPersonView: View {
|
||||
|
||||
var existingPerson: Person? = nil
|
||||
|
||||
@Query private var allPeople: [Person]
|
||||
|
||||
@State private var name = ""
|
||||
@State private var selectedTag: PersonTag = .other
|
||||
@State private var occupation = ""
|
||||
@@ -84,7 +86,12 @@ struct AddPersonView: View {
|
||||
RowDivider()
|
||||
inlineField("Wohnort", text: $location)
|
||||
RowDivider()
|
||||
inlineField("Interessen", text: $interests)
|
||||
InterestTagEditor(
|
||||
label: "Interessen",
|
||||
text: $interests,
|
||||
suggestions: interestSuggestions,
|
||||
tagColor: .green
|
||||
)
|
||||
RowDivider()
|
||||
inlineField("Herkunft", text: $culturalBackground)
|
||||
RowDivider()
|
||||
@@ -379,6 +386,14 @@ struct AddPersonView: View {
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
|
||||
private var interestSuggestions: [String] {
|
||||
InterestTagHelper.allSuggestions(
|
||||
from: allPeople,
|
||||
likes: UserProfileStore.shared.likes,
|
||||
dislikes: UserProfileStore.shared.dislikes
|
||||
)
|
||||
}
|
||||
|
||||
private func loadExisting() {
|
||||
guard let p = existingPerson else { return }
|
||||
name = p.name
|
||||
|
||||
+26
-30
@@ -220,11 +220,11 @@ struct IchView: View {
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
if !profileStore.likes.isEmpty {
|
||||
preferenceRow(label: "Mag ich", text: profileStore.likes, color: .green)
|
||||
InterestChipRow(label: "Mag ich", text: profileStore.likes, color: .green)
|
||||
if !profileStore.dislikes.isEmpty { RowDivider() }
|
||||
}
|
||||
if !profileStore.dislikes.isEmpty {
|
||||
preferenceRow(label: "Mag ich nicht", text: profileStore.dislikes, color: .red)
|
||||
InterestChipRow(label: "Mag ich nicht", text: profileStore.dislikes, color: .red)
|
||||
}
|
||||
}
|
||||
.background(theme.surfaceCard)
|
||||
@@ -233,32 +233,6 @@ struct IchView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func preferenceRow(label: String, text: String, color: Color) -> some View {
|
||||
let items = text.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
return VStack(alignment: .leading, spacing: 8) {
|
||||
Text(label)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(theme.contentTertiary)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(items, id: \.self) { item in
|
||||
Text(item)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(color.opacity(0.12))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
|
||||
private func infoRow(label: String, value: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Text(label)
|
||||
@@ -309,6 +283,8 @@ struct IchEditView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@EnvironmentObject var profileStore: UserProfileStore
|
||||
|
||||
@Query private var allPeople: [Person]
|
||||
|
||||
@State private var name: String
|
||||
@State private var hasBirthday: Bool
|
||||
@State private var birthday: Date
|
||||
@@ -402,9 +378,19 @@ struct IchEditView: View {
|
||||
// Vorlieben
|
||||
formSection("Vorlieben") {
|
||||
VStack(spacing: 0) {
|
||||
inlineField("Mag ich", text: $likes)
|
||||
InterestTagEditor(
|
||||
label: "Mag ich",
|
||||
text: $likes,
|
||||
suggestions: preferenceSuggestions,
|
||||
tagColor: .green
|
||||
)
|
||||
Divider().padding(.leading, 16)
|
||||
inlineField("Mag ich nicht", text: $dislikes)
|
||||
InterestTagEditor(
|
||||
label: "Mag nicht",
|
||||
text: $dislikes,
|
||||
suggestions: preferenceSuggestions,
|
||||
tagColor: .red
|
||||
)
|
||||
}
|
||||
.background(theme.surfaceCard)
|
||||
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
|
||||
@@ -562,6 +548,16 @@ struct IchEditView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Suggestion Pool
|
||||
|
||||
private var preferenceSuggestions: [String] {
|
||||
InterestTagHelper.allSuggestions(
|
||||
from: allPeople,
|
||||
likes: profileStore.likes,
|
||||
dislikes: profileStore.dislikes
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@@ -4050,16 +4050,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Noch zu wenig Verlauf für persönliche Vorschläge." : {
|
||||
"comment" : "AddMomentView – conversation suggestions insufficient data message",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Not enough history yet for personal suggestions."
|
||||
}
|
||||
}
|
||||
}
|
||||
"noch keine" : {
|
||||
|
||||
},
|
||||
"Noch keine Einträge" : {
|
||||
"comment" : "LogbuchView – empty state title",
|
||||
@@ -4139,6 +4131,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Noch zu wenig Verlauf für persönliche Vorschläge." : {
|
||||
"comment" : "AddMomentView – conversation suggestions insufficient data message",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Not enough history yet for personal suggestions."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Notiz" : {
|
||||
"comment" : "VisitRatingFlowView / VisitSummaryView – note field label (singular)",
|
||||
"localizations" : {
|
||||
@@ -4809,6 +4812,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tag hinzufügen…" : {
|
||||
"comment" : "InterestTagEditor – placeholder for the tag input field",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Add tag…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Teile WhatsApp-Nachrichten direkt in nahbar – sie werden als Momente gespeichert." : {
|
||||
"comment" : "FeatureTourStep description – WhatsApp share feature",
|
||||
"localizations" : {
|
||||
|
||||
@@ -2,6 +2,9 @@ import SwiftUI
|
||||
import SwiftData
|
||||
import CoreData
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
|
||||
private let todoNotificationLogger = Logger(subsystem: "nahbar", category: "TodoNotification")
|
||||
|
||||
struct PersonDetailView: View {
|
||||
@Environment(\.nahbarTheme) var theme
|
||||
@@ -387,7 +390,7 @@ struct PersonDetailView: View {
|
||||
RowDivider()
|
||||
}
|
||||
if let interests = person.interests, !interests.isEmpty {
|
||||
InfoRowView(label: "Interessen", value: interests)
|
||||
InterestChipRow(label: "Interessen", text: interests, color: .green)
|
||||
RowDivider()
|
||||
}
|
||||
if let bg = person.culturalBackground, !bg.isEmpty {
|
||||
@@ -1247,12 +1250,19 @@ struct EditTodoView: View {
|
||||
|
||||
private func scheduleReminder() {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound]) { granted, _ in
|
||||
guard granted else { return }
|
||||
center.requestAuthorization(options: [.alert, .sound]) { granted, error in
|
||||
if let error {
|
||||
todoNotificationLogger.error("Berechtigung-Fehler: \(error.localizedDescription)")
|
||||
}
|
||||
guard granted else {
|
||||
todoNotificationLogger.warning("Notification-Berechtigung abgelehnt – keine Todo-Erinnerung.")
|
||||
return
|
||||
}
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = todo.person?.firstName ?? ""
|
||||
content.body = todo.title
|
||||
content.sound = .default
|
||||
content.userInfo = ["todoID": todo.id.uuidString]
|
||||
let components = Calendar.current.dateComponents(
|
||||
[.year, .month, .day, .hour, .minute], from: reminderDate
|
||||
)
|
||||
@@ -1262,7 +1272,13 @@ struct EditTodoView: View {
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
center.add(request)
|
||||
center.add(request) { error in
|
||||
if let error {
|
||||
todoNotificationLogger.error("Todo-Erinnerung konnte nicht geplant werden: \(error.localizedDescription)")
|
||||
} else {
|
||||
todoNotificationLogger.info("Todo-Erinnerung geplant: \(todo.id.uuidString)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,217 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
// MARK: - Interest Tag Helper
|
||||
|
||||
/// Rein-statische Hilfsfunktionen für kommaseparierte Interessen-Tags.
|
||||
enum InterestTagHelper {
|
||||
|
||||
/// Zerlegt einen kommaseparierten String in bereinigte, nicht-leere Tags.
|
||||
static func parse(_ text: String) -> [String] {
|
||||
text.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
/// Sortiert Tags alphabetisch und verbindet sie zu einem kommaseparierten String.
|
||||
static func join(_ tags: [String]) -> String {
|
||||
tags.sorted(by: { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending })
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
|
||||
/// Fügt einen Tag hinzu (ignoriert Duplikate, Groß-/Kleinschreibung irrelevant).
|
||||
/// Ergebnis ist alphabetisch sortiert.
|
||||
static func addTag(_ tag: String, to text: String) -> String {
|
||||
let trimmed = tag.trimmingCharacters(in: .whitespaces)
|
||||
guard !trimmed.isEmpty else { return text }
|
||||
var current = parse(text)
|
||||
let alreadyExists = current.contains { $0.localizedCaseInsensitiveCompare(trimmed) == .orderedSame }
|
||||
guard !alreadyExists else { return text }
|
||||
current.append(trimmed)
|
||||
return join(current)
|
||||
}
|
||||
|
||||
/// Entfernt einen Tag aus dem kommaseparierten String.
|
||||
static func removeTag(_ tag: String, from text: String) -> String {
|
||||
let updated = parse(text).filter { $0.localizedCaseInsensitiveCompare(tag) != .orderedSame }
|
||||
return join(updated)
|
||||
}
|
||||
|
||||
/// Sammelt alle vorhandenen Tags aus Personen-Interessen, User-Likes und Dislikes.
|
||||
/// Dedupliziert und alphabetisch sortiert.
|
||||
static func allSuggestions(from people: [Person], likes: String, dislikes: String) -> [String] {
|
||||
let personTags = people.flatMap { parse($0.interests ?? "") }
|
||||
let userTags = parse(likes) + parse(dislikes)
|
||||
let combined = Set(personTags + userTags)
|
||||
return combined.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interest Chip Row (Display-only)
|
||||
|
||||
/// Zeigt kommaseparierte Interessen-Tags als horizontale Chip-Reihe an.
|
||||
/// Verwendung in PersonDetailView und IchView (Display-Modus).
|
||||
struct InterestChipRow: View {
|
||||
@Environment(\.nahbarTheme) private var theme
|
||||
let label: String
|
||||
let text: String
|
||||
let color: Color
|
||||
|
||||
private var tags: [String] { InterestTagHelper.parse(text) }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(label)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(theme.contentTertiary)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
Text(tag)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(color.opacity(0.12))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interest Tag Editor
|
||||
|
||||
/// Bearbeitbares Tag-Eingabefeld mit Autocomplete.
|
||||
/// Bestehendes Tags werden als entfernbare farbige Chips gezeigt.
|
||||
/// Beim Tippen erscheint eine horizontale Chip-Reihe passender Vorschläge.
|
||||
struct InterestTagEditor: View {
|
||||
@Environment(\.nahbarTheme) private var theme
|
||||
let label: String
|
||||
@Binding var text: String
|
||||
let suggestions: [String]
|
||||
let tagColor: Color
|
||||
|
||||
@State private var inputText = ""
|
||||
@FocusState private var inputFocused: Bool
|
||||
|
||||
private var tags: [String] { InterestTagHelper.parse(text) }
|
||||
|
||||
private var filteredSuggestions: [String] {
|
||||
let q = inputText.trimmingCharacters(in: .whitespaces)
|
||||
guard !q.isEmpty else { return [] }
|
||||
return suggestions.filter { suggestion in
|
||||
suggestion.localizedCaseInsensitiveContains(q) &&
|
||||
!tags.contains { $0.localizedCaseInsensitiveCompare(suggestion) == .orderedSame }
|
||||
}
|
||||
}
|
||||
|
||||
private func addTag(_ tag: String) {
|
||||
text = InterestTagHelper.addTag(tag, to: text)
|
||||
inputText = ""
|
||||
}
|
||||
|
||||
private func removeTag(_ tag: String) {
|
||||
text = InterestTagHelper.removeTag(tag, from: text)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
|
||||
// Zeile 1: bestehende Tags + ggf. Platzhalter
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Text(label)
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(theme.contentTertiary)
|
||||
.frame(width: 80, alignment: .leading)
|
||||
.padding(.top, tags.isEmpty ? 0 : 2)
|
||||
|
||||
if tags.isEmpty {
|
||||
Text("noch keine")
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(theme.contentTertiary.opacity(0.45))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { inputFocused = true }
|
||||
} else {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
HStack(spacing: 3) {
|
||||
Text(tag)
|
||||
.font(.system(size: 13))
|
||||
Button {
|
||||
removeTag(tag)
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
}
|
||||
}
|
||||
.foregroundStyle(tagColor)
|
||||
.padding(.leading, 10)
|
||||
.padding(.trailing, 7)
|
||||
.padding(.vertical, 5)
|
||||
.background(tagColor.opacity(0.12))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeile 2: Texteingabe
|
||||
HStack(spacing: 12) {
|
||||
Spacer().frame(width: 80)
|
||||
TextField("Tag hinzufügen…", text: $inputText)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(theme.contentPrimary)
|
||||
.tint(theme.accent)
|
||||
.focused($inputFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit {
|
||||
addTag(inputText)
|
||||
}
|
||||
.onChange(of: inputText) { _, new in
|
||||
// Komma-Eingabe als Trennzeichen
|
||||
if new.hasSuffix(",") {
|
||||
let candidate = String(new.dropLast())
|
||||
addTag(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeile 3: Vorschlags-Chips (nur während Eingabe)
|
||||
if !filteredSuggestions.isEmpty {
|
||||
HStack(spacing: 12) {
|
||||
Spacer().frame(width: 80)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(filteredSuggestions, id: \.self) { suggestion in
|
||||
Button { addTag(suggestion) } label: {
|
||||
Text(suggestion)
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(theme.contentSecondary)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(theme.backgroundSecondary)
|
||||
.clipShape(Capsule())
|
||||
.overlay(
|
||||
Capsule().stroke(theme.borderSubtle, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Person Avatar
|
||||
|
||||
|
||||
@@ -349,3 +349,141 @@ struct PaywallTargetingTests {
|
||||
#expect(target(isPro: false) != target(isPro: true))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InterestTagHelper Tests
|
||||
|
||||
@Suite("InterestTagHelper – parse")
|
||||
struct InterestTagHelperParseTests {
|
||||
|
||||
@Test("Leerer String → leeres Array")
|
||||
func emptyStringReturnsEmpty() {
|
||||
#expect(InterestTagHelper.parse("") == [])
|
||||
}
|
||||
|
||||
@Test("Einzelner Tag ohne Komma")
|
||||
func singleTag() {
|
||||
#expect(InterestTagHelper.parse("Fußball") == ["Fußball"])
|
||||
}
|
||||
|
||||
@Test("Mehrere Tags kommasepariert")
|
||||
func multipleTags() {
|
||||
let result = InterestTagHelper.parse("Fußball, Musik, Lesen")
|
||||
#expect(result == ["Fußball", "Musik", "Lesen"])
|
||||
}
|
||||
|
||||
@Test("Whitespace wird getrimmt")
|
||||
func whitespaceTrimmed() {
|
||||
let result = InterestTagHelper.parse(" Kino , Sport ")
|
||||
#expect(result == ["Kino", "Sport"])
|
||||
}
|
||||
|
||||
@Test("Leere Segmente werden gefiltert")
|
||||
func emptySegmentsFiltered() {
|
||||
let result = InterestTagHelper.parse("Kino,,Musik,")
|
||||
#expect(result == ["Kino", "Musik"])
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("InterestTagHelper – join")
|
||||
struct InterestTagHelperJoinTests {
|
||||
|
||||
@Test("Leeres Array → leerer String")
|
||||
func emptyArrayReturnsEmpty() {
|
||||
#expect(InterestTagHelper.join([]) == "")
|
||||
}
|
||||
|
||||
@Test("Einzelner Tag bleibt unverändert")
|
||||
func singleTagUnchanged() {
|
||||
#expect(InterestTagHelper.join(["Fußball"]) == "Fußball")
|
||||
}
|
||||
|
||||
@Test("Tags werden alphabetisch sortiert")
|
||||
func tagsSortedAlphabetically() {
|
||||
let result = InterestTagHelper.join(["Musik", "Fußball", "Lesen"])
|
||||
#expect(result == "Fußball, Lesen, Musik")
|
||||
}
|
||||
|
||||
@Test("Sortierung ist case-insensitive")
|
||||
func sortingCaseInsensitive() {
|
||||
let result = InterestTagHelper.join(["bier", "Apfel", "Chips"])
|
||||
#expect(result == "Apfel, bier, Chips")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("InterestTagHelper – addTag")
|
||||
struct InterestTagHelperAddTagTests {
|
||||
|
||||
@Test("Tag zu leerem String hinzufügen")
|
||||
func addToEmpty() {
|
||||
#expect(InterestTagHelper.addTag("Kino", to: "") == "Kino")
|
||||
}
|
||||
|
||||
@Test("Tag zu bestehendem String – alphabetisch einsortiert")
|
||||
func addSortsAlphabetically() {
|
||||
let result = InterestTagHelper.addTag("Musik", to: "Fußball")
|
||||
#expect(result == "Fußball, Musik")
|
||||
}
|
||||
|
||||
@Test("Duplikat wird ignoriert")
|
||||
func duplicateIgnored() {
|
||||
let result = InterestTagHelper.addTag("Kino", to: "Kino, Sport")
|
||||
#expect(result == "Kino, Sport")
|
||||
}
|
||||
|
||||
@Test("Duplikat ignoriert (Groß-/Kleinschreibung)")
|
||||
func duplicateCaseInsensitive() {
|
||||
let result = InterestTagHelper.addTag("kino", to: "Kino")
|
||||
#expect(result == "Kino")
|
||||
}
|
||||
|
||||
@Test("Leerer String wird ignoriert")
|
||||
func emptyTagIgnored() {
|
||||
let result = InterestTagHelper.addTag("", to: "Kino")
|
||||
#expect(result == "Kino")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("InterestTagHelper – removeTag")
|
||||
struct InterestTagHelperRemoveTagTests {
|
||||
|
||||
@Test("Tag aus String entfernen")
|
||||
func removeExistingTag() {
|
||||
let result = InterestTagHelper.removeTag("Musik", from: "Fußball, Musik, Sport")
|
||||
#expect(result == "Fußball, Sport")
|
||||
}
|
||||
|
||||
@Test("Nicht vorhandener Tag → unveränderter String")
|
||||
func removeNonExistentTag() {
|
||||
let result = InterestTagHelper.removeTag("Kino", from: "Fußball, Sport")
|
||||
#expect(result == "Fußball, Sport")
|
||||
}
|
||||
|
||||
@Test("Letzten Tag entfernen → leerer String")
|
||||
func removeLastTagReturnsEmpty() {
|
||||
let result = InterestTagHelper.removeTag("Kino", from: "Kino")
|
||||
#expect(result == "")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("InterestTagHelper – allSuggestions")
|
||||
struct InterestTagHelperSuggestionsTests {
|
||||
|
||||
@Test("Keine Personen + leere Vorlieben → leere Vorschläge")
|
||||
func emptyInputsReturnEmpty() {
|
||||
let result = InterestTagHelper.allSuggestions(from: [], likes: "", dislikes: "")
|
||||
#expect(result.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Vorschläge aus likes und dislikes kombiniert und sortiert")
|
||||
func combinesLikesAndDislikes() {
|
||||
let result = InterestTagHelper.allSuggestions(from: [], likes: "Musik, Kino", dislikes: "Sport")
|
||||
#expect(result == ["Kino", "Musik", "Sport"])
|
||||
}
|
||||
|
||||
@Test("Duplikate werden dedupliziert")
|
||||
func deduplicates() {
|
||||
let result = InterestTagHelper.allSuggestions(from: [], likes: "Kino, Musik", dislikes: "Kino")
|
||||
#expect(!result.contains { result.filter { $0 == "Kino" }.count > 1 })
|
||||
#expect(result.filter { $0 == "Kino" }.count == 1)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user