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) } }