cards-native/Tests/UnitTests/TypingTests.swift
Till JS 505aa9db19 feat(study): Typing-Karten + Levenshtein-Match-Logik
CardRenderer für typing ist nicht mehr Placeholder. Web-Vorbild:
TypingView.svelte + cards-domain/typing.ts.

Typing.swift (Sources/Core/Domain/):
- check(input:answer:aliases:) → TypingMatch (correct/close/wrong)
- Normalisierung: trim + lowercase + NFD-Decomp + Combining-Marks
  strippen (Diakritika: ä → a)
- Aliases-Support (Komma-getrennt aus card.fields["aliases"])
- Levenshtein-Threshold max(1, floor(len * 0.2)) → "close"

TypingCardView (Features/Study/):
- TextField mit Auto-Focus 0.15s nach onAppear, Return = Submit
- Submit-Button mit Return-Symbol + primary background
- Nach Submit: Badge (✓ Richtig / ≈ Fast / ✗ Falsch) + User-
  Eingabe in „…" Quotes + Divider + erwartete Antwort
- Haptic-Feedback: heavy bei correct, light bei close/wrong
- Reset on card.id change

TypingTests: 8 Tests für check() — exact, case+whitespace,
NFD-Umlauts, aliases, Levenshtein-close (Berln → Berlin),
empty-input, sowie Levenshtein-Helper-Sanity.

Build 8 → 9. 43 Tests grün (war 35).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:39:39 +02:00

55 lines
1.9 KiB
Swift

import Foundation
import Testing
@testable import CardsNative
@Suite("Typing-Match")
struct TypingTests {
@Test("Exact match → correct")
func exactMatch() {
#expect(Typing.check(input: "Berlin", answer: "Berlin") == .correct)
}
@Test("Case + Whitespace normalisiert")
func caseAndWhitespace() {
#expect(Typing.check(input: " berlin ", answer: "Berlin") == .correct)
#expect(Typing.check(input: "BERLIN", answer: "berlin") == .correct)
}
@Test("Umlaute via NFD-Diakritika-Stripping")
func umlauts() {
#expect(Typing.check(input: "Munchen", answer: "München") == .correct)
#expect(Typing.check(input: "muenchen", answer: "München") != .correct)
// muenchen != munchen via NFD: ä a, aber ue ü
}
@Test("Aliases akzeptiert als correct")
func aliasesCorrect() {
let aliases = "Frankfurt am Main,Frankfurt/Main"
#expect(Typing.check(input: "Frankfurt/Main", answer: "Frankfurt", aliases: aliases) == .correct)
}
@Test("Levenshtein-1 bei 5+ Zeichen → close")
func closeMatch() {
#expect(Typing.check(input: "Berln", answer: "Berlin") == .close)
#expect(Typing.check(input: "Berlim", answer: "Berlin") == .close)
}
@Test("Großer Unterschied → wrong")
func wrongMatch() {
#expect(Typing.check(input: "Madrid", answer: "Berlin") == .wrong)
}
@Test("Leer-Input → wrong")
func emptyInput() {
#expect(Typing.check(input: "", answer: "Berlin") == .wrong)
#expect(Typing.check(input: " ", answer: "Berlin") == .wrong)
}
@Test("Levenshtein-Helper")
func levenshteinSanity() {
#expect(Typing.levenshtein("", "") == 0)
#expect(Typing.levenshtein("abc", "abc") == 0)
#expect(Typing.levenshtein("abc", "abd") == 1)
#expect(Typing.levenshtein("kitten", "sitting") == 3)
}
}