164 lines
8.5 KiB
Swift
164 lines
8.5 KiB
Swift
import AppKit
|
|
import ConjureKit
|
|
import Testing
|
|
@testable import Conjure
|
|
|
|
@Test @MainActor
|
|
func noteFormattingHidesSyntaxButPreservesMarkdownAndLiteralCode() throws {
|
|
_ = NSApplication.shared
|
|
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 600, height: 800))
|
|
let source = "# Heading\n## Subheading\n*italic* **bold** _emphasis_ __strong__ ~~gone~~ `*literal*`\n> Quote\n[Label](https://example.com)\n```text\n# Code *literal*\n```\nUnmatched * and \\*escaped*"
|
|
editor.setMarkdownContents(NSAttributedString(string: source))
|
|
MarkdownHighlighting.apply(to: editor)
|
|
editor.refreshTasks()
|
|
let layout = try #require(editor.layoutManager)
|
|
let container = try #require(editor.textContainer)
|
|
layout.ensureLayout(for: container)
|
|
func visibleText() -> String {
|
|
var result = ""
|
|
let text = editor.string as NSString
|
|
for index in 0..<text.length {
|
|
let glyph = layout.glyphIndexForCharacter(at: index)
|
|
if !layout.propertyForGlyph(at: glyph).contains(.null) {
|
|
result += text.substring(with: NSRange(location: index, length: 1))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
#expect(visibleText() == "Heading\nSubheading\nitalic bold emphasis strong gone *literal*\nQuote\nLabel\n\n# Code *literal*\n\nUnmatched * and \\*escaped*")
|
|
#expect(editor.markdownString == source)
|
|
editor.setSelectedRange(NSRange(location: 0, length: editor.string.utf16.count))
|
|
let pasteboard = NSPasteboard.withUniqueName()
|
|
defer { pasteboard.releaseGlobally() }
|
|
#expect(editor.writeSelection(to: pasteboard, types: editor.writablePasteboardTypes))
|
|
#expect(pasteboard.string(forType: .string) == source)
|
|
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let store = try NotesStore(directory: directory)
|
|
let id = try store.create()
|
|
try store.update(id: id, contents: editor.markdownContents)
|
|
let reloaded = try #require(NotesStore(directory: directory).notes.first)
|
|
#expect(reloaded.contents.string == source)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func hiddenNoteMarkupKeepsTypingDeletionFormattingAndUndoAtVisibleBoundaries() throws {
|
|
_ = NSApplication.shared
|
|
let suite = "NotesMarkup.\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(suite)
|
|
defer {
|
|
defaults.removePersistentDomain(forName: suite)
|
|
try? FileManager.default.removeItem(at: directory)
|
|
}
|
|
let controller = NotesController(defaults: defaults, directory: directory)
|
|
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 570, height: 404),
|
|
styleMask: .borderless, backing: .buffered, defer: false)
|
|
window.contentViewController = controller
|
|
defer { _ = controller.close(); window.orderOut(nil) }
|
|
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
|
let editor = try #require(descendants(controller.view).compactMap { $0 as? NotesTextView }.first)
|
|
window.makeFirstResponder(editor)
|
|
editor.insertText("# Title\n*word*", replacementRange: NSRange(location: 0, length: 0))
|
|
editor.setSelectedRange(NSRange(location: 0, length: 0))
|
|
editor.insertText("A", replacementRange: editor.selectedRange())
|
|
#expect(editor.markdownString == "# ATitle\n*word*")
|
|
editor.breakUndoCoalescing()
|
|
editor.undoManager?.removeAllActions()
|
|
editor.undoManager?.beginUndoGrouping()
|
|
editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0))
|
|
editor.deleteBackward(nil)
|
|
editor.undoManager?.endUndoGrouping()
|
|
#expect(editor.markdownString == "# ATitle\n*wor*")
|
|
editor.undoManager?.undo()
|
|
#expect(editor.markdownString == "# ATitle\n*word*")
|
|
let word = (editor.string as NSString).range(of: "word")
|
|
editor.setSelectedRange(NSRange(location: word.location, length: 0))
|
|
editor.moveRight(nil)
|
|
#expect(editor.selectedRange().location == word.location + 1)
|
|
editor.setSelectedRange(NSRange(location: NSMaxRange(word), length: 0))
|
|
editor.moveRight(nil)
|
|
#expect(editor.selectedRange().location == editor.string.utf16.count)
|
|
editor.setSelectedRange(NSRange(location: word.location, length: 0))
|
|
let italic = try #require(descendants(controller.view).compactMap { $0 as? NSButton }.first { $0.accessibilityLabel() == "Italic" })
|
|
// Selecting the body and toggling formatting must remove its hidden delimiters.
|
|
editor.setSelectedRange(word)
|
|
italic.performClick(nil)
|
|
#expect(editor.markdownString == "# ATitle\nword")
|
|
#expect(controller.save())
|
|
#expect(try NotesStore(directory: directory).notes.first?.contents.string == "# ATitle\nword")
|
|
}
|
|
|
|
@Test @MainActor
|
|
func notesFormattingShortcutsToggleCombinedStylesAndRespectPopupFocus() throws {
|
|
_ = NSApplication.shared
|
|
let suite = "NotesFormattingKeys.\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(suite)
|
|
defer {
|
|
defaults.removePersistentDomain(forName: suite)
|
|
try? FileManager.default.removeItem(at: directory)
|
|
}
|
|
let launcher = LauncherWindowController(
|
|
history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }),
|
|
defaults: defaults, notesDirectory: directory)
|
|
let window = try #require(launcher.window)
|
|
defer { launcher.dismiss(restoringFocus: false) }
|
|
launcher.present(command: .notes)
|
|
let content = try #require(window.contentView)
|
|
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
|
let editor = try #require(descendants(content).compactMap { $0 as? NotesTextView }.first)
|
|
let controller = try #require(editor.delegate as? NotesController)
|
|
func shortcut(_ key: String, shift: Bool = false, option: Bool = false) throws -> Bool {
|
|
var flags: NSEvent.ModifierFlags = .command
|
|
if shift { flags.insert(.shift) }
|
|
if option { flags.insert(.option) }
|
|
let event = try #require(NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: flags,
|
|
timestamp: 1, windowNumber: window.windowNumber, context: nil,
|
|
characters: shift ? key.uppercased() : key, charactersIgnoringModifiers: shift ? key.uppercased() : key,
|
|
isARepeat: false, keyCode: 0))
|
|
return window.performKeyEquivalent(with: event)
|
|
}
|
|
editor.insertText("fox 🦊", replacementRange: editor.selectedRange())
|
|
editor.setSelectedRange(NSRange(location: 0, length: editor.string.utf16.count))
|
|
#expect(try shortcut("b"))
|
|
#expect(editor.markdownString == "**fox 🦊**")
|
|
#expect(try shortcut("i"))
|
|
#expect(editor.markdownString == "***fox 🦊***")
|
|
let font = try #require(editor.textStorage?.attribute(.font, at: editor.selectedRange().location,
|
|
effectiveRange: nil) as? NSFont)
|
|
let traits = NSFontManager.shared.traits(of: font)
|
|
#expect(traits.contains(.boldFontMask) && traits.contains(.italicFontMask))
|
|
#expect(try shortcut("i"))
|
|
#expect(editor.markdownString == "**fox 🦊**")
|
|
#expect(try shortcut("b"))
|
|
#expect(editor.markdownString == "fox 🦊")
|
|
|
|
let hide = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.toolTip == "Hide formatting" })
|
|
hide.performClick(nil)
|
|
#expect(try shortcut("x", shift: true))
|
|
#expect(editor.markdownString == "~~fox 🦊~~")
|
|
#expect(try shortcut("x", shift: true))
|
|
#expect(editor.markdownString == "fox 🦊")
|
|
#expect(!(try shortcut("b", option: true)))
|
|
#expect(editor.markdownString == "fox 🦊")
|
|
|
|
editor.breakUndoCoalescing()
|
|
editor.undoManager?.removeAllActions()
|
|
editor.undoManager?.beginUndoGrouping()
|
|
#expect(try shortcut("e"))
|
|
editor.undoManager?.endUndoGrouping()
|
|
#expect(editor.markdownString == "`fox 🦊`")
|
|
editor.undoManager?.undo()
|
|
#expect(editor.markdownString == "fox 🦊")
|
|
#expect(try shortcut("k"))
|
|
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first { $0.accessibilityLabel() == "Search notes actions" })
|
|
#expect(window.firstResponder !== editor)
|
|
_ = try shortcut("b")
|
|
#expect(editor.markdownString == "fox 🦊")
|
|
#expect(search.stringValue.isEmpty)
|
|
#expect(controller.handle(#selector(NSResponder.cancelOperation(_:))))
|
|
#expect(controller.save())
|
|
#expect(try NotesStore(directory: directory).notes.first?.contents.string == "fox 🦊")
|
|
}
|