Nudge-Screen

This commit is contained in:
2026-04-20 09:41:18 +02:00
parent a48e857ada
commit 187c3e4fc6
26 changed files with 2542 additions and 229 deletions
@@ -0,0 +1,244 @@
import Testing
@testable import bookstax
import Foundation
// MARK: - Helpers
private func makePageDTO(markdown: String? = "# Hello", tags: [TagDTO] = []) -> PageDTO {
PageDTO(
id: 42,
bookId: 1,
chapterId: nil,
name: "Test Page",
slug: "test-page",
html: nil,
markdown: markdown,
priority: 0,
draftStatus: false,
tags: tags,
createdAt: Date(),
updatedAt: Date()
)
}
// MARK: - Initialisation
@Suite("PageEditorViewModel Initialisation")
struct PageEditorViewModelInitTests {
@Test("Create mode starts with empty title and content")
func createModeDefaults() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
#expect(vm.title.isEmpty)
#expect(vm.markdownContent.isEmpty)
#expect(vm.tags.isEmpty)
#expect(vm.isHtmlOnlyPage == false)
}
@Test("Edit mode populates title and markdown from page")
func editModePopulates() {
let page = makePageDTO(markdown: "## Content")
let vm = PageEditorViewModel(mode: .edit(page: page))
#expect(vm.title == "Test Page")
#expect(vm.markdownContent == "## Content")
#expect(vm.isHtmlOnlyPage == false)
}
@Test("Edit mode with nil markdown sets isHtmlOnlyPage")
func htmlOnlyPage() {
let page = makePageDTO(markdown: nil)
let vm = PageEditorViewModel(mode: .edit(page: page))
#expect(vm.isHtmlOnlyPage == true)
#expect(vm.markdownContent.isEmpty)
}
@Test("Edit mode with existing tags loads them")
func editModeLoadsTags() {
let tags = [TagDTO(name: "topic", value: "swift", order: 0)]
let page = makePageDTO(tags: tags)
let vm = PageEditorViewModel(mode: .edit(page: page))
#expect(vm.tags.count == 1)
#expect(vm.tags.first?.name == "topic")
}
}
// MARK: - hasUnsavedChanges
@Suite("PageEditorViewModel hasUnsavedChanges")
struct PageEditorViewModelUnsavedTests {
@Test("No changes after init → hasUnsavedChanges is false")
func noChangesAfterInit() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
#expect(vm.hasUnsavedChanges == false)
}
@Test("Changing title → hasUnsavedChanges is true")
func titleChange() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.title = "New Title"
#expect(vm.hasUnsavedChanges == true)
}
@Test("Changing markdownContent → hasUnsavedChanges is true")
func contentChange() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.markdownContent = "Some text"
#expect(vm.hasUnsavedChanges == true)
}
@Test("Adding a tag → hasUnsavedChanges is true")
func tagAddition() {
let page = makePageDTO()
let vm = PageEditorViewModel(mode: .edit(page: page))
vm.addTag(name: "new-tag")
#expect(vm.hasUnsavedChanges == true)
}
@Test("Restoring original values → hasUnsavedChanges is false again")
func revertChanges() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.title = "Changed"
vm.title = ""
#expect(vm.hasUnsavedChanges == false)
}
}
// MARK: - isSaveDisabled
@Suite("PageEditorViewModel isSaveDisabled")
struct PageEditorViewModelSaveDisabledTests {
@Test("Empty title disables save in create mode")
func emptyTitleCreate() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.markdownContent = "Some content"
#expect(vm.isSaveDisabled == true)
}
@Test("Empty content disables save in create mode even if title is set")
func emptyContentCreate() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.title = "My Page"
vm.markdownContent = ""
#expect(vm.isSaveDisabled == true)
}
@Test("Whitespace-only content disables save in create mode")
func whitespaceContentCreate() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.title = "My Page"
vm.markdownContent = " \n "
#expect(vm.isSaveDisabled == true)
}
@Test("Title and content both set enables save in create mode")
func validCreate() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.title = "My Page"
vm.markdownContent = "Hello world"
#expect(vm.isSaveDisabled == false)
}
@Test("Edit mode only requires title empty content is allowed")
func editOnlyNeedsTitle() {
let page = makePageDTO(markdown: nil)
let vm = PageEditorViewModel(mode: .edit(page: page))
vm.title = "Existing Page"
vm.markdownContent = ""
#expect(vm.isSaveDisabled == false)
}
@Test("Empty title disables save in edit mode")
func emptyTitleEdit() {
let page = makePageDTO()
let vm = PageEditorViewModel(mode: .edit(page: page))
vm.title = ""
#expect(vm.isSaveDisabled == true)
}
}
// MARK: - Tag Management
@Suite("PageEditorViewModel Tags")
struct PageEditorViewModelTagTests {
@Test("addTag appends a new tag")
func addTag() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: "status", value: "draft")
#expect(vm.tags.count == 1)
#expect(vm.tags.first?.name == "status")
#expect(vm.tags.first?.value == "draft")
}
@Test("addTag trims whitespace from name and value")
func addTagTrims() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: " topic ", value: " swift ")
#expect(vm.tags.first?.name == "topic")
#expect(vm.tags.first?.value == "swift")
}
@Test("addTag with empty name after trimming is ignored")
func addTagEmptyNameIgnored() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: " ")
#expect(vm.tags.isEmpty)
}
@Test("addTag prevents duplicate (same name + value) entries")
func addTagNoDuplicates() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: "lang", value: "swift")
vm.addTag(name: "lang", value: "swift")
#expect(vm.tags.count == 1)
}
@Test("addTag allows same name with different value")
func addTagSameNameDifferentValue() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: "env", value: "dev")
vm.addTag(name: "env", value: "prod")
#expect(vm.tags.count == 2)
}
@Test("removeTag removes the matching tag by id")
func removeTag() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: "remove-me", value: "yes")
let tag = vm.tags[0]
vm.removeTag(tag)
#expect(vm.tags.isEmpty)
}
@Test("removeTag does not remove non-matching tags")
func removeTagKeepsOthers() {
let vm = PageEditorViewModel(mode: .create(bookId: 1))
vm.addTag(name: "keep", value: "")
vm.addTag(name: "remove", value: "")
let toRemove = vm.tags.first { $0.name == "remove" }!
vm.removeTag(toRemove)
#expect(vm.tags.count == 1)
#expect(vm.tags.first?.name == "keep")
}
}
// MARK: - uploadTargetPageId
@Suite("PageEditorViewModel uploadTargetPageId")
struct PageEditorViewModelUploadIDTests {
@Test("Create mode returns 0 as upload target")
func createModeUploadTarget() {
let vm = PageEditorViewModel(mode: .create(bookId: 5))
#expect(vm.uploadTargetPageId == 0)
}
@Test("Edit mode returns the existing page id")
func editModeUploadTarget() {
let page = makePageDTO()
let vm = PageEditorViewModel(mode: .edit(page: page))
#expect(vm.uploadTargetPageId == 42)
}
}