Files
i7t5--edmund/Tests/EdmundTests/FileIntegrationTests.swift
wehub-resource-sync 5b99bf6bca
CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:54 +08:00

478 lines
16 KiB
Swift

import Testing
import AppKit
import Foundation
@testable import EdmundCore
// These tests exercise the exact same data path as Document:
//
// File Open: file → Data → String(data:encoding:) → editor.loadContent()
// File Save: editor.rawSource → Data(using: .utf8) → file
//
// Document.read(from:ofType:) and Document.data(ofType:) do nothing more
// than the above, so these tests cover the real I/O contract.
// MARK: - File I/O Helpers
/// Simulates Document.read(from:ofType:) → showWindows() pipeline.
/// Reads a file from disk, decodes as UTF-8, and loads into the editor.
@MainActor
private func openFile(_ url: URL, into editor: EditorTextView) throws {
let data = try Data(contentsOf: url)
guard let content = String(data: data, encoding: .utf8) else {
throw NSError(domain: "TestError", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Not valid UTF-8"])
}
editor.loadContent(content)
}
/// Simulates Document.data(ofType:) pipeline.
/// Encodes rawSource as UTF-8 and writes to a file.
@MainActor
private func saveFile(_ url: URL, from editor: EditorTextView) throws {
guard let data = editor.rawSource.data(using: .utf8) else {
throw NSError(domain: "TestError", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Could not encode as UTF-8"])
}
try data.write(to: url)
}
/// Creates a temporary .md file with the given content and returns its URL.
private func makeTempFile(content: String, ext: String = "md") throws -> URL {
let dir = FileManager.default.temporaryDirectory
let name = UUID().uuidString + "." + ext
let url = dir.appendingPathComponent(name)
try content.data(using: .utf8)!.write(to: url)
return url
}
/// Removes a file at the given URL (best-effort, no-throw).
private func removeTempFile(_ url: URL) {
try? FileManager.default.removeItem(at: url)
}
// ============================================================================
// MARK: - File Open
// ============================================================================
@Suite("Integration — File Open")
struct FileOpenTests {
@Test("Open plain text file loads content into editor")
@MainActor func openPlainText() throws {
let url = try makeTempFile(content: "Hello, world!")
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == "Hello, world!")
#expect(editor.blocks.count == 1)
#expect(editor.blocks[0].content == "Hello, world!")
}
@Test("Open markdown file preserves all syntax")
@MainActor func openMarkdownFile() throws {
let md = "# Title\n\nSome **bold** and *italic* text.\n\n- item 1\n- item 2\n\n> a quote"
let url = try makeTempFile(content: md)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == md)
#expect(editor.blocks.count == 8) // each line is a block
#expect(editor.blocks[0].content == "# Title")
#expect(editor.blocks[2].content == "Some **bold** and *italic* text.")
#expect(editor.blocks[4].content == "- item 1")
}
@Test("Open empty file creates one empty block")
@MainActor func openEmptyFile() throws {
let url = try makeTempFile(content: "")
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == "")
#expect(editor.blocks.count == 1)
#expect(editor.blocks[0].content == "")
}
@Test("Open file with only newlines creates correct number of blocks")
@MainActor func openNewlinesOnly() throws {
let url = try makeTempFile(content: "\n\n\n")
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == "\n\n\n")
#expect(editor.blocks.count == 4) // 3 newlines → 4 blocks
}
@Test("Open file with Unicode content preserves characters")
@MainActor func openUnicodeFile() throws {
let content = "Héllo wörld 🌍\nEmoji: 🎉🚀\nChinese: 你好世界"
let url = try makeTempFile(content: content)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == content)
#expect(editor.blocks.count == 3)
}
@Test("Open .txt file works same as .md")
@MainActor func openTxtFile() throws {
let url = try makeTempFile(content: "plain text file", ext: "txt")
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == "plain text file")
}
@Test("Open file clears undo/redo stacks")
@MainActor func openClearsUndo() throws {
let url = try makeTempFile(content: "file content")
defer { removeTempFile(url) }
let editor = makeEditor()
type("some typing", into: editor) // creates undo history
#expect(!editor.undoStack.isEmpty)
try openFile(url, into: editor)
#expect(editor.undoStack.isEmpty)
#expect(editor.redoStack.isEmpty)
#expect(editor.rawSource == "file content")
}
}
// ============================================================================
// MARK: - File Save
// ============================================================================
@Suite("Integration — File Save")
struct FileSaveTests {
@Test("Save writes rawSource to disk as UTF-8")
@MainActor func saveBasic() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(url) }
let editor = makeEditor()
editor.loadContent("Hello, world!")
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == "Hello, world!")
}
@Test("Save preserves markdown syntax exactly")
@MainActor func savePreservesMarkdown() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(url) }
let md = "# Title\n\n**bold** *italic* `code`\n\n- list\n\n> quote"
let editor = makeEditor()
editor.loadContent(md)
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == md)
}
@Test("Save after editing writes modified content")
@MainActor func saveAfterEditing() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(url) }
let editor = makeEditor()
editor.loadContent("original")
type(" modified", into: editor)
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved.contains("modified"))
}
@Test("Save preserves Unicode content")
@MainActor func saveUnicode() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(url) }
let content = "Emoji: 🎉🚀\n你好世界\nCafé"
let editor = makeEditor()
editor.loadContent(content)
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == content)
}
@Test("Save empty document writes empty file")
@MainActor func saveEmpty() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(url) }
let editor = makeEditor()
// Editor starts with empty content
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == "")
}
@Test("Save overwrites existing file content")
@MainActor func saveOverwrites() throws {
let url = try makeTempFile(content: "old content")
defer { removeTempFile(url) }
let editor = makeEditor()
editor.loadContent("new content")
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == "new content")
}
}
// ============================================================================
// MARK: - Open → Edit → Save Round-Trip
// ============================================================================
@Suite("Integration — File Round-Trip")
struct FileRoundTripTests {
@Test("Open file, edit, save, reopen — content matches")
@MainActor func openEditSaveReopen() throws {
let url = try makeTempFile(content: "Hello\n\nWorld")
defer { removeTempFile(url) }
// Open
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == "Hello\n\nWorld")
// Edit: cursor starts at block 0 position 0. Type at start.
type("# ", into: editor)
#expect(editor.rawSource.hasPrefix("# Hello"))
// Save
try saveFile(url, from: editor)
// Reopen in a fresh editor
let editor2 = makeEditor()
try openFile(url, into: editor2)
#expect(editor2.rawSource == "# Hello\n\nWorld")
}
@Test("Open markdown, edit inline formatting, save preserves syntax")
@MainActor func editFormattingSave() throws {
let url = try makeTempFile(content: "plain text")
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
// Cursor at 0 in active block. Move to end of block and type.
let blockLen = (editor.blocks[0].content as NSString).length
editor.setSelectedRange(NSRange(location: blockLen, length: 0))
type(" **bold** and *italic*", into: editor)
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved.contains("**bold**"))
#expect(saved.contains("*italic*"))
}
@Test("Multiple save cycles preserve content integrity")
@MainActor func multipleSaveCycles() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(url) }
let editor = makeEditor()
// Cycle 1: type and save
type("line1", into: editor)
try saveFile(url, from: editor)
var saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == "line1")
// Cycle 2: add line and save
pressEnter(in: editor)
type("line2", into: editor)
try saveFile(url, from: editor)
saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == "line1\nline2")
// Cycle 3: add more and save
pressEnter(in: editor)
type("line3", into: editor)
try saveFile(url, from: editor)
saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == "line1\nline2\nline3")
}
@Test("Open file, undo all edits, save restores original")
@MainActor func undoAllAndSave() throws {
let original = "Title\n\nBody text"
let url = try makeTempFile(content: original)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
// Edit: cursor is at start of block 0 (active). Type at cursor.
type("extra ", into: editor)
#expect(editor.rawSource != original)
// Undo the edit
editor.undo(nil)
#expect(editor.rawSource == original)
// Save
try saveFile(url, from: editor)
let saved = try String(contentsOf: url, encoding: .utf8)
#expect(saved == original)
}
@Test("Open file A, open file B in same editor replaces content")
@MainActor func openSecondFileReplacesFirst() throws {
let urlA = try makeTempFile(content: "File A content")
let urlB = try makeTempFile(content: "File B content")
defer { removeTempFile(urlA); removeTempFile(urlB) }
let editor = makeEditor()
try openFile(urlA, into: editor)
#expect(editor.rawSource == "File A content")
try openFile(urlB, into: editor)
#expect(editor.rawSource == "File B content")
#expect(editor.blocks.count == 1)
#expect(editor.blocks[0].content == "File B content")
}
}
// ============================================================================
// MARK: - File Content Edge Cases
// ============================================================================
@Suite("Integration — File Edge Cases")
struct FileEdgeCaseTests {
@Test("File with trailing newline preserves it through round-trip")
@MainActor func trailingNewline() throws {
let content = "line1\nline2\n"
let url = try makeTempFile(content: content)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == content)
let saveURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(saveURL) }
try saveFile(saveURL, from: editor)
let saved = try String(contentsOf: saveURL, encoding: .utf8)
#expect(saved == content)
}
@Test("Large file loads correctly")
@MainActor func largeFile() throws {
// 1000 lines of markdown
var lines: [String] = []
for i in 0..<1000 {
lines.append("Line \(i): some **bold** and *italic* text here.")
}
let content = lines.joined(separator: "\n")
let url = try makeTempFile(content: content)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.blocks.count == 1000)
#expect(editor.rawSource == content)
}
@Test("File with mixed line content round-trips correctly")
@MainActor func mixedContentRoundTrip() throws {
let content = """
# Heading
Regular paragraph with **bold**, *italic*, and `code`.
- [ ] todo unchecked
- [x] todo checked
- bullet
1. first
2. second
> blockquote
~~strikethrough~~ and ==highlight==
[link](https://example.com)
"""
let url = try makeTempFile(content: content)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
#expect(editor.rawSource == content)
// Save to a new file
let saveURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(saveURL) }
try saveFile(saveURL, from: editor)
let saved = try String(contentsOf: saveURL, encoding: .utf8)
#expect(saved == content)
}
@Test("File with special characters round-trips correctly")
@MainActor func specialCharacters() throws {
let content = "Tabs:\t\there\nBackslash: \\\nQuotes: \"double\" 'single'\nAngle: <tag>"
let url = try makeTempFile(content: content)
defer { removeTempFile(url) }
let editor = makeEditor()
try openFile(url, into: editor)
let saveURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".md")
defer { removeTempFile(saveURL) }
try saveFile(saveURL, from: editor)
let saved = try String(contentsOf: saveURL, encoding: .utf8)
#expect(saved == content)
}
}