import Foundation import AppKit import WebKit import Testing @testable import MikeMail @MainActor struct MailPresentationTests { @Test func htmlAlternativeAndAttachmentsSurviveArchive() throws { let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } let html = "

Grüße Welt

" let bytes = Data([0, 1, 254, 255]) let raw = try MIME.compose(from: "from@example.com", to: ["to@example.com"], subject: "HTML", body: "Grüße Welt", attachments: [MailAttachment(name: "Probe.bin", mimeType: "application/octet-stream", data: bytes)], htmlBody: html) let archive = try MailArchive(root: root) try archive.store(raw: raw, folderID: "inbox", validity: "1", uid: "1", flags: []) let message = try #require(archive.messages(folderID: "inbox").first) #expect(message.body == "Grüße Welt") #expect(message.htmlBody == html) #expect(message.attachments.first?.data == bytes) let htmlOnly = MIME.parse(Data("Content-Type: text/html; charset=utf-8\r\n\r\n

Hallo Welt

".utf8), id: "html") #expect(htmlOnly.htmlBody?.contains("Hallo") == true) #expect(htmlOnly.body.contains("Hallo Welt")) } @Test func selectedFormattingExportsHTMLWithoutChangingOtherText() throws { let editor = MailEditorModel() let view = NSTextView() view.string = "Hallo Welt" view.font = NSFont(name: "Helvetica", size: 14) editor.textView = view view.setSelectedRange(NSRange(location: 0, length: 5)) editor.toggleTrait(.boldFontMask, enabled: true) editor.setAttribute(.foregroundColor, value: NSColor(srgbRed: 1, green: 0, blue: 0, alpha: 1)) editor.setFont(size: 24) let selected = try #require(view.textStorage?.attribute(.font, at: 0, effectiveRange: nil) as? NSFont) let remaining = try #require(view.textStorage?.attribute(.font, at: 6, effectiveRange: nil) as? NSFont) #expect(NSFontManager.shared.traits(of: selected).contains(.boldFontMask)) #expect(selected.pointSize == 24) #expect(remaining.pointSize == 14) let html = try editor.html() #expect(html.contains("24")) // Cocoa's HTML writer converts color profiles, so compare the exported color with tolerance. let colorRange = try #require(html.range(of: "#[0-9a-fA-F]{6}", options: .regularExpression)) let rgb = try #require(UInt32(html[colorRange].dropFirst(), radix: 16)) #expect((rgb >> 16) > 230 && ((rgb >> 8) & 255) < 25 && (rgb & 255) < 25) #expect(editor.plainText == "Hallo Welt") } @Test func accountOrderPersistsInBothDirections() throws { let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } let archive = try MailArchive(root: root) let accounts = ["A", "B", "C"].map { MailAccount(name: $0) } try archive.saveConfiguration(ArchiveConfiguration(accounts: accounts, folders: [:], recipients: [])) let store = MailStore(archive: archive) store.moveAccounts(from: IndexSet(integer: 0), to: 3) #expect(store.accounts.map(\.name) == ["B", "C", "A"]) #expect(try MailArchive(root: root).configuration()?.accounts.map(\.name) == ["B", "C", "A"]) store.moveAccounts(from: IndexSet(integer: 2), to: 0) #expect(store.accounts == accounts) #expect(try archive.configuration()?.accounts == accounts) } @Test func senderAndDatePresentation() throws { #expect(MailDisplay.sender("\"Max Muster\" ") == "Max Muster ") #expect(MailDisplay.sender("max@example.com") == "max@example.com") let date = try #require(ISO8601DateFormatter().date(from: "2026-11-07T15:04:00Z")) #expect(MailDisplay.date(date, timeZone: try #require(TimeZone(secondsFromGMT: 0))) == "07.11.2026 15:04") } @Test func htmlRendererBlocksRemoteResourcesAndScripts() async throws { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() config.defaultWebpagePreferences.allowsContentJavaScript = false // An application-owned script records CSP violations without allowing mail scripts. config.userContentController.addUserScript(WKUserScript(source: """ window.blockedResources = []; document.addEventListener('securitypolicyviolation', e => window.blockedResources.push(e.blockedURI)); """, injectionTime: .atDocumentStart, forMainFrameOnly: true)) let view = WKWebView(frame: NSRect(x: 0, y: 0, width: 600, height: 400), configuration: config) let html = """

HTML

""" view.loadHTMLString(MailHTML.document(html, attachments: [], blocked: true, font: "Helvetica", size: 14), baseURL: nil) var violations: [String] = [] for _ in 0..<100 { try await Task.sleep(for: .milliseconds(50)) violations = (try? await view.evaluateJavaScript("window.blockedResources")) as? [String] ?? [] if violations.contains(where: { $0.contains("tracker.png") }) { break } } #expect(violations.contains { $0.contains("tracker.png") }) #expect(violations.contains { $0.contains("background.png") }) let style = try await view.evaluateJavaScript("getComputedStyle(document.getElementById('styled')).color") as? String #expect(style == "rgb(255, 0, 0)") let scriptRan = try await view.evaluateJavaScript("window.mailScriptRan === true") as? Bool #expect(scriptRan == false) } }