Unit tests, Lokalisierung, ShareExtension

This commit is contained in:
2026-04-20 15:01:50 +02:00
parent 187c3e4fc6
commit 7bea01caaf
24 changed files with 1711 additions and 10 deletions
+175
View File
@@ -0,0 +1,175 @@
import Testing
@testable import bookstax
// Note: ShareViewModel and its dependencies live in BookStaxShareExtension,
// which is a separate target. These tests are compiled against the extension
// module. If you build the tests via the main scheme, add BookStaxShareExtension
// sources to the bookstaxTests target membership as needed.
//
// The tests below use a MockShareAPIService injected at init time so no
// network or Keychain access is required.
// MARK: - Mock
final class MockShareAPIService: ShareAPIServiceProtocol, @unchecked Sendable {
var shelvesToReturn: [ShelfSummary] = []
var booksToReturn: [BookSummary] = []
var chaptersToReturn: [ChapterSummary] = []
var errorToThrow: Error?
func fetchShelves() async throws -> [ShelfSummary] {
if let error = errorToThrow { throw error }
return shelvesToReturn
}
func fetchBooks(shelfId: Int) async throws -> [BookSummary] {
if let error = errorToThrow { throw error }
return booksToReturn
}
func fetchChapters(bookId: Int) async throws -> [ChapterSummary] {
if let error = errorToThrow { throw error }
return chaptersToReturn
}
func createPage(bookId: Int, chapterId: Int?, title: String, markdown: String) async throws -> PageResult {
if let error = errorToThrow { throw error }
return PageResult(id: 42, name: title)
}
}
// MARK: - Tests
@Suite("ShareViewModel")
@MainActor
struct ShareViewModelTests {
// Isolated UserDefaults suite so tests don't pollute each other.
private func makeDefaults() -> UserDefaults {
let suiteName = "test.bookstax.shareext.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return defaults
}
// MARK: - 1. Shelves are loaded on start
@Test("Beim Start werden alle Regale geladen")
func shelvesLoadOnStart() async throws {
let mock = MockShareAPIService()
mock.shelvesToReturn = [
ShelfSummary(id: 1, name: "Regal A", slug: "a"),
ShelfSummary(id: 2, name: "Regal B", slug: "b")
]
let vm = ShareViewModel(
sharedText: "Testinhalt",
apiService: mock,
defaults: makeDefaults()
)
await vm.loadShelves()
#expect(vm.shelves.count == 2)
#expect(vm.shelves[0].name == "Regal A")
#expect(vm.shelves[1].name == "Regal B")
#expect(vm.isLoading == false)
#expect(vm.errorMessage == nil)
}
// MARK: - 2. Selecting a shelf loads its books
@Test("Shelf-Auswahl lädt Bücher nach")
func selectingShelfLoadsBooksAsync() async throws {
let mock = MockShareAPIService()
mock.shelvesToReturn = [ShelfSummary(id: 10, name: "Regal X", slug: "x")]
mock.booksToReturn = [
BookSummary(id: 100, name: "Buch 1", slug: "b1"),
BookSummary(id: 101, name: "Buch 2", slug: "b2")
]
let vm = ShareViewModel(
sharedText: "Test",
apiService: mock,
defaults: makeDefaults()
)
await vm.selectShelf(ShelfSummary(id: 10, name: "Regal X", slug: "x"))
#expect(vm.selectedShelf?.id == 10)
#expect(vm.books.count == 2)
#expect(vm.books[0].name == "Buch 1")
#expect(vm.isLoading == false)
}
// MARK: - 3. Last shelf / book are restored from UserDefaults
@Test("Gespeicherte Shelf- und Book-IDs werden beim Start wiederhergestellt")
func lastSelectionIsRestored() async throws {
let mock = MockShareAPIService()
mock.shelvesToReturn = [ShelfSummary(id: 5, name: "Gespeichertes Regal", slug: "saved")]
mock.booksToReturn = [BookSummary(id: 50, name: "Gespeichertes Buch", slug: "saved-b")]
let defaults = makeDefaults()
defaults.set(5, forKey: "shareExtension.lastShelfID")
defaults.set(50, forKey: "shareExtension.lastBookID")
let vm = ShareViewModel(
sharedText: "Test",
apiService: mock,
defaults: defaults
)
await vm.loadShelves()
#expect(vm.selectedShelf?.id == 5, "Letztes Regal soll wiederhergestellt sein")
#expect(vm.selectedBook?.id == 50, "Letztes Buch soll wiederhergestellt sein")
}
// MARK: - Additional: title auto-populated from first line
@Test("Seitentitel wird aus erster Zeile des geteilten Textes befüllt")
func titleAutoPopulatedFromFirstLine() {
let text = "Erste Zeile\nZweite Zeile\nDritte Zeile"
let vm = ShareViewModel(
sharedText: text,
apiService: MockShareAPIService()
)
#expect(vm.pageTitle == "Erste Zeile")
}
// MARK: - Additional: save page sets isSaved
@Test("Seite speichern setzt isSaved auf true")
func savePageSetsisSaved() async throws {
let mock = MockShareAPIService()
let vm = ShareViewModel(
sharedText: "Inhalt der Seite",
apiService: mock
)
vm.pageTitle = "Mein Titel"
vm.selectedBook = BookSummary(id: 1, name: "Buch", slug: "buch")
await vm.savePage()
#expect(vm.isSaved == true)
#expect(vm.errorMessage == nil)
}
// MARK: - Additional: isSaveDisabled logic
@Test("Speichern ist deaktiviert ohne Titel oder Buch")
func isSaveDisabledWithoutTitleOrBook() {
let vm = ShareViewModel(sharedText: "Test", apiService: MockShareAPIService())
// No title, no book
vm.pageTitle = ""
#expect(vm.isSaveDisabled == true)
// Title but no book
vm.pageTitle = "Titel"
#expect(vm.isSaveDisabled == true)
// Title + book enabled
vm.selectedBook = BookSummary(id: 1, name: "Buch", slug: "b")
#expect(vm.isSaveDisabled == false)
}
}