Files
sst/Tests/SstDataTests/ViewSupportTests.swift
T

191 lines
8.2 KiB
Swift

import Foundation
import Testing
import SstCore
@testable import SstData
struct StopwatchFormatTests {
@Test func formatsMinutesSecondsAndTenths() {
#expect(StopwatchFormat.string(0) == "00:00.0")
#expect(StopwatchFormat.string(7.46) == "00:07.4")
#expect(StopwatchFormat.string(65.2) == "01:05.2")
#expect(StopwatchFormat.string(-3) == "00:00.0")
#expect(StopwatchFormat.string(6039.9) == "00:39.9") // minutes wrap at 100
}
}
struct CorrectionMarkupTests {
func correction(_ start: Int, _ length: Int, heard: String = "x", wrote: String = "y") -> AppliedCorrection {
AppliedCorrection(heard: heard, wrote: wrote, rule: heard, start: start, length: length)
}
@Test func noCorrectionsGivesOnePlainSegment() {
#expect(CorrectionMarkup.segments(finalText: "hallo wereld", corrections: []) == [TextSegment(text: "hallo wereld")])
}
@Test func emptyTextGivesNoSegments() {
#expect(CorrectionMarkup.segments(finalText: "", corrections: [correction(0, 1)]).isEmpty)
}
@Test func splitsAroundACorrectionInTheMiddle() {
let c = correction(11, 11, heard: "cloud code", wrote: "Claude Code")
let segments = CorrectionMarkup.segments(finalText: "ik gebruik Claude Code vandaag", corrections: [c])
#expect(segments == [
TextSegment(text: "ik gebruik "),
TextSegment(text: "Claude Code", correction: c),
TextSegment(text: " vandaag"),
])
}
@Test func handlesCorrectionsAtTheStartAndEnd() {
let first = correction(0, 3), last = correction(8, 3)
let segments = CorrectionMarkup.segments(finalText: "abc def ghi", corrections: [last, first])
#expect(segments.map(\.text) == ["abc", " def ", "ghi"])
#expect(segments[0].correction == first && segments[2].correction == last && segments[1].correction == nil)
}
@Test func skipsOutOfRangeAndOverlappingCorrections() {
let ok = correction(0, 3), overlapping = correction(2, 3), outside = correction(50, 4), empty = correction(4, 0)
let segments = CorrectionMarkup.segments(finalText: "abc def", corrections: [ok, overlapping, outside, empty])
#expect(segments.map(\.text) == ["abc", " def"])
}
@Test func offsetsAreUTF16SoEmojiDoNotShiftThem() {
// "😀 " is 3 UTF-16 units; the corrected word starts at 3.
let c = correction(3, 6, heard: "wereld", wrote: "wereld")
let segments = CorrectionMarkup.segments(finalText: "😀 wereld", corrections: [c])
#expect(segments.map(\.text) == ["😀 ", "wereld"])
}
}
@MainActor
struct HistoryViewModelTests {
func store(_ texts: [String]) -> HistoryStore {
let store = HistoryStore(fileURL: FileManager.default.temporaryDirectory.appending(path: "sst-vm-\(UUID().uuidString)/h.json"))
for (index, text) in texts.enumerated() {
store.add(HistoryEntry(date: Date(timeIntervalSince1970: 1_700_000_000 + Double(index)), duration: 1, rawText: text, finalText: text))
}
return store
}
@Test func listsAllEntriesNewestFirstAndFiltersByQuery() {
let model = HistoryViewModel(store: store(["eerste", "tweede zin", "derde"]))
#expect(model.totalCount == 3)
#expect(model.days.flatMap(\.entries).map(\.finalText) == ["derde", "tweede zin", "eerste"])
model.query = "tweede"
#expect(model.days.flatMap(\.entries).map(\.finalText) == ["tweede zin"])
#expect(model.totalCount == 3)
}
@Test func selectionIsClearedWhenItDisappearsFromTheFilter() {
let history = store(["alpha", "beta"])
let model = HistoryViewModel(store: history)
model.selection = history.entries.first { $0.finalText == "alpha" }?.id
#expect(model.selectedEntry?.finalText == "alpha")
model.query = "beta"
#expect(model.selection == nil)
}
@Test func deleteRemovesFromStoreAndList() {
let history = store(["a", "b"])
let model = HistoryViewModel(store: history)
model.delete(id: history.entries[0].id)
#expect(history.entries.count == 1)
#expect(model.totalCount == 1)
}
@Test func refreshPicksUpNewEntries() {
let history = store(["a"])
let model = HistoryViewModel(store: history)
history.add(HistoryEntry(date: .now, duration: 1, rawText: "nieuw", finalText: "nieuw"))
model.refresh()
#expect(model.totalCount == 2)
}
}
@MainActor
struct DictionaryViewModelTests {
func makeModel(_ text: String = "") throws -> (DictionaryViewModel, DictionaryStore) {
let url = FileManager.default.temporaryDirectory.appending(path: "sst-dict-\(UUID().uuidString)/dictionary.txt")
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
try text.write(to: url, atomically: true, encoding: .utf8)
let store = DictionaryStore(fileURL: url)
try store.load()
return (DictionaryViewModel(store: store, knowledge: FakeKnowledge()), store)
}
@Test func rowsCarryWarnings() throws {
let (model, _) = try makeModel("Supabase\ncloud => Claude\n")
#expect(model.rows.count == 2)
#expect(model.rows[0].warnings.isEmpty)
#expect(model.rows[1].worst == .warning)
}
@Test func duplicateIsFlaggedOnTheLaterEntryOnly() throws {
let (model, _) = try makeModel("cloud code => Claude Code\nCloud-Code => Other\n")
#expect(model.rows[0].worst != .error)
#expect(model.rows[1].worst == .error)
}
@Test func addUpdateAndRemoveGoThroughTheStore() throws {
let (model, store) = try makeModel()
let id = model.add(.term("Vercel"))!
#expect(store.entries == [term("Vercel")])
model.update(id: id, to: .correction(heard: "versel", wrote: "Vercel"))
#expect(store.entries == [pair("versel", "Vercel")])
model.remove(id: id)
#expect(store.entries.isEmpty)
#expect(model.rows.isEmpty)
}
@Test func undoRestoresTheLastChangeStepByStep() throws {
let (model, store) = try makeModel("Supabase\n")
#expect(!model.canUndo)
let id = model.add(.term("Vercel"))!
model.remove(id: id)
#expect(store.entries == [term("Supabase")])
model.undo()
#expect(store.entries == [term("Supabase"), term("Vercel")])
model.undo()
#expect(store.entries == [term("Supabase")])
#expect(!model.canUndo)
model.undo() // nothing left: harmless
#expect(store.entries == [term("Supabase")])
}
@Test func searchMatchesBothSidesIgnoringCaseAndDiacritics() throws {
let (model, _) = try makeModel("cloud code => Claude Code\nCafé\nVercel\n")
model.query = "CLAUDE"
#expect(model.visibleRows.count == 1)
model.query = "cafe"
#expect(model.visibleRows.count == 1)
model.query = ""
#expect(model.visibleRows.count == 3)
}
@Test func editorWarningsIgnoreTheEntryBeingEdited() throws {
let (model, store) = try makeModel("cloud code => Claude Code\n")
let id = store.entries[0].id
let editingItself = model.warnings(for: .correction(heard: "cloud code", wrote: "Claude Code"), editing: id)
#expect(!editingItself.contains { $0.level == .error })
let asNew = model.warnings(for: .correction(heard: "cloud code", wrote: "Other"), editing: nil)
#expect(asNew.contains { $0.level == .error })
}
@Test func removingAnUnknownIdChangesNothing() throws {
let (model, store) = try makeModel("Supabase\n")
let before = store.entries
model.remove(id: UUID())
#expect(store.entries == before)
#expect(model.canUndo == false)
}
@Test func editingTheEarlierOfTwoDuplicatesShowsNoDuplicateError() throws {
let (model, store) = try makeModel("cloud code => A\nCloud-Code => B\n")
let first = store.entries[0]
let second = store.entries[1]
#expect(!model.warnings(for: first.kind, editing: first.id).contains { $0.level == .error })
#expect(model.warnings(for: second.kind, editing: second.id).contains { $0.level == .error })
#expect(model.warnings(for: first.kind, editing: nil).contains { $0.level == .error })
}
}