diff --git a/Sources/RichTextKit/Coordinator/RichTextCoordinator+Subscriptions.swift b/Sources/RichTextKit/Coordinator/RichTextCoordinator+Subscriptions.swift index a5e7d53d3..44a433589 100644 --- a/Sources/RichTextKit/Coordinator/RichTextCoordinator+Subscriptions.swift +++ b/Sources/RichTextKit/Coordinator/RichTextCoordinator+Subscriptions.swift @@ -78,40 +78,40 @@ private extension RichTextCoordinator { func subscribeToAlignment() { richTextContext.$textAlignment .sink( - receiveCompletion: { _ in }, receiveValue: { [weak self] in self?.textView.setRichTextAlignment($0) - }) + } + ) .store(in: &cancellables) } func subscribeToFontName() { richTextContext.$fontName .sink( - receiveCompletion: { _ in }, receiveValue: { [weak self] in self?.textView.setRichTextFontName($0) - }) + } + ) .store(in: &cancellables) } func subscribeToFontSize() { richTextContext.$fontSize .sink( - receiveCompletion: { _ in }, receiveValue: { [weak self] in self?.textView.setRichTextFontSize($0) - }) + } + ) .store(in: &cancellables) } func subscribeToIsEditingText() { richTextContext.$isEditingText .sink( - receiveCompletion: { _ in }, receiveValue: { [weak self] in self?.setIsEditing(to: $0) - }) + } + ) .store(in: &cancellables) } } diff --git a/Tests/RichTextKitTests/RichTextCoordinatorIntegrationTests+UIKit.swift b/Tests/RichTextKitTests/RichTextCoordinatorIntegrationTests+UIKit.swift new file mode 100644 index 000000000..86464ac52 --- /dev/null +++ b/Tests/RichTextKitTests/RichTextCoordinatorIntegrationTests+UIKit.swift @@ -0,0 +1,80 @@ +// +// RichTextCoordinatorIntegrationTests+UIKit.swift +// +// +// Created by Dominik Bucher on 19.1.2024. +// +#if os(iOS) +import UIKit + +import CoreGraphics +import SwiftUI +@testable import RichTextKit +import XCTest + +final class RichTextCoordinatorIntegrationTests: XCTestCase { + private var text: NSAttributedString! + private var textBinding: Binding! + private var textView: RichTextView! + private var textContext: RichTextContext! + private var coordinator: RichTextCoordinator! + + static let initialAttributedString: NSAttributedString = { + let text = NSMutableAttributedString(string: "This is red text") + text.addAttributes([.foregroundColor: ColorRepresentable.red], range: text.richTextRange) + text.addAttributes([.font: FontRepresentable.systemFont(ofSize: 16)], range: text.richTextRange) + return text + }() + + + override func setUp() { + super.setUp() + + text = Self.initialAttributedString + textBinding = Binding(get: { self.text }, set: { self.text = $0 }) + textView = RichTextView(string: text) + textContext = RichTextContext() + coordinator = RichTextCoordinator( + text: textBinding, + textView: textView, + richTextContext: textContext) + textView.selectedRange = NSRange(location: 0, length: 0) + textView.setup(with: text, format: .archivedData) + } + + override func tearDown() { + text = nil + textBinding = nil + textView = nil + textContext = nil + coordinator = nil + + super.tearDown() + } + + private let firstTypingPart = "String without any attributes" + private let imageToPaste = UIGraphicsImageRenderer(size: .init(width: 20, height: 20)).image { rendererContext in + UIColor.gray.setFill() + rendererContext.fill(CGRect(origin: .zero, size: .init(width: 20, height: 20))) + } + + func test_behavior_whenInitialState_keepsConfiguration() { + // When starting RichTextEditor we want to check if the font and color is set correctly. + textContext.selectRange(.init(location: 0, length: Self.initialAttributedString.length)) + + // Only ArchivedData textView format support images... + coordinator.pasteImage(.init(content: imageToPaste, at: textView.richText.length, moveCursor: true)) + + XCTAssertTrue(textView.richText.containsAttachments(in: textView.richTextRange)) + + textView.simulateTyping(of: firstTypingPart) + + textContext.selectRange(NSRange(location: Self.initialAttributedString.length, length: firstTypingPart.count)) + XCTAssertEqual(textView.richTextAttributes[.font] as? FontRepresentable, FontRepresentable.systemFont(ofSize: 16)) + XCTAssertEqual(textView.richTextAttributes[.foregroundColor] as? ColorRepresentable, ColorRepresentable.red) + + textView.setRichTextStyle(.strikethrough, to: true, at: textView.selectedRange) + XCTAssertEqual(textView.richTextAttributes[.strikethroughStyle] as? Int, 1) + } +} +#endif diff --git a/Tests/RichTextKitTests/RichTextViewIntegrationTests+UIKit.swift b/Tests/RichTextKitTests/RichTextViewIntegrationTests+UIKit.swift new file mode 100644 index 000000000..b73872355 --- /dev/null +++ b/Tests/RichTextKitTests/RichTextViewIntegrationTests+UIKit.swift @@ -0,0 +1,125 @@ +// +// RichTextViewIntegrationTests+UIKit.swift +// +// +// Created by Dominik Bucher on 19.1.2024. +// + +#if os(iOS) +import UIKit + +import CoreGraphics +import SwiftUI +@testable import RichTextKit +import XCTest + +final class RichTextViewIntegrationTests: XCTestCase { + private var text: NSAttributedString! + private var textBinding: Binding! + private var textView: RichTextView! + private var textContext: RichTextContext! + private var coordinator: RichTextCoordinator! + + override func setUp() { + super.setUp() + + text = NSAttributedString.empty + textBinding = Binding(get: { self.text }, set: { self.text = $0 }) + textView = RichTextView(string: text) + textContext = RichTextContext() + coordinator = RichTextCoordinator( + text: textBinding, + textView: textView, + richTextContext: textContext) + textView.selectedRange = NSRange(location: 0, length: 1) + textView.setRichTextAlignment(.justified) + } + + override func tearDown() { + text = nil + textBinding = nil + textView = nil + textContext = nil + coordinator = nil + + super.tearDown() + } + + private let stringWithoutAttributes = "String without any attributes" + private let lastStringToAppend = " Last addition..." + private let otherStringToAppend = ". And This is some text with other attributes!" + + func test_behavior_forFontStyle() throws { + // When starting RichTextEditor we want to check if the font and color is set correctly. + textContext.selectRange(.init(location: 0, length: 0)) + + XCTAssertEqual(textView.richTextAttributes[.font] as? FontRepresentable, FontRepresentable.systemFont(ofSize: 16)) + XCTAssertEqual(textView.richTextAttributes[.foregroundColor] as? ColorRepresentable, ColorRepresentable.label) + + // First we fill in the empty textView with some text, select it and set bold and italic to it. + assertFirstTextPart() + // After that we append more text, asserting that this text carries same attributes as the one before + // and we change it. + assertSecondTextPart() + // Finally, we set typingAttributes before we append last text and check if those typingAttributes are set to our + // new text. + assertFinalTextPart() + } + + private func assertFirstTextPart() { + textView.simulateTyping(of: stringWithoutAttributes) + + textContext.selectRange(.init(location: 0, length: stringWithoutAttributes.count)) + + XCTAssertEqual(textView.richTextAttributes[.font] as? FontRepresentable, FontRepresentable.systemFont(ofSize: 16)) + XCTAssertEqual(textView.richTextAttributes[.foregroundColor] as? ColorRepresentable, ColorRepresentable.label) + + textView.setRichTextStyle(.bold, to: true, at: textView.richTextRange) + textView.setRichTextStyle(.italic, to: true, at: textView.richTextRange) + + XCTAssertTrue(try XCTUnwrap(textView.richTextFont?.fontDescriptor.symbolicTraits.contains(.traitBold))) + XCTAssertTrue(try XCTUnwrap(textView.richTextFont?.fontDescriptor.symbolicTraits.contains(.traitItalic))) + + textView.setRichTextStyle(.bold, to: false, at: textView.richTextRange) + textView.setRichTextStyle(.italic, to: false, at: textView.richTextRange) + XCTAssertFalse(try XCTUnwrap(textView.richTextFont?.fontDescriptor.symbolicTraits.contains(.traitBold))) + XCTAssertFalse(try XCTUnwrap(textView.richTextFont?.fontDescriptor.symbolicTraits.contains(.traitItalic))) + } + + private func assertSecondTextPart() { + textView.simulateTyping(of: otherStringToAppend) + + textContext.selectRange(NSRange(location: stringWithoutAttributes.count , length: otherStringToAppend.count)) + let selectedRange = textView.selectedRange + XCTAssertFalse(try XCTUnwrap(textView.richTextFont?.fontDescriptor.symbolicTraits.contains(.traitBold))) + textView.setRichTextStyle(.strikethrough, to: true, at: selectedRange) + + XCTAssertEqual(textView.richTextAttributes[.strikethroughStyle] as? Int, 1) + XCTAssertEqual(textView.richTextAttributes(at: selectedRange)[.strikethroughStyle] as? Int, 1) + XCTAssertNil( + textView.richTextAttributes(at: NSRange(location: .zero, length: stringWithoutAttributes.count))[.strikethroughStyle] + ) + + textContext.selectRange(NSRange(location: 2 , length: .zero)) + XCTAssertNil(textView.richTextAttributes[.strikethroughStyle]) + } + + private func assertFinalTextPart() { + + textView.setRichTextStyle(.bold, to: true) + // Refactor replace into type in tests. + // document this... + textView.simulateTyping(of: lastStringToAppend) + + XCTAssertTrue(try XCTUnwrap(textView.richTextFont?.fontDescriptor.symbolicTraits.contains(.traitBold))) + + let lastPartLocation = NSRange( + location: stringWithoutAttributes.count + otherStringToAppend.count, + length: lastStringToAppend.count + ) + + let fontForLastString = textView.richTextAttributes(at: lastPartLocation)[.font] as? FontRepresentable + XCTAssertTrue(try XCTUnwrap(fontForLastString?.fontDescriptor.symbolicTraits.contains(.traitBold))) + } +} +#endif diff --git a/Tests/RichTextKitTests/Utils/RichTextView+Helpers.swift b/Tests/RichTextKitTests/Utils/RichTextView+Helpers.swift new file mode 100644 index 000000000..34b06fd9f --- /dev/null +++ b/Tests/RichTextKitTests/Utils/RichTextView+Helpers.swift @@ -0,0 +1,32 @@ +// +// RichTextView+Helpers.swift +// +// +// Created by Dominik Bucher on 19.01.2024. +// + +import Foundation +import RichTextKit + +#if os(iOS) || os(tvOS) +import UIKit + +extension UITextView { + func simulateTyping(of text: String) { + replace( + textRange( + from: endOfDocument, + to: endOfDocument + )!, + withText: text + ) + } +} +#elseif os(macOS) +import AppKit +extension NSTextView { + func simulateTyping(of text: String) { + // TODO: Implement + } +} +#endif