105 lines
6.0 KiB
Swift
105 lines
6.0 KiB
Swift
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 = "<p style=\"font-family: Georgia; color: #ff0000; font-size: 24px\"><b>Grüße</b> <i>Welt</i></p>"
|
|
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<p><b>Hallo</b> Welt</p>".utf8), id: "html")
|
|
#expect(htmlOnly.htmlBody?.contains("<b>Hallo</b>") == 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@example.com>") == "Max Muster <max@example.com>")
|
|
#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 = """
|
|
<p id="styled" style="color: rgb(255, 0, 0); font-size:24px"><b>HTML</b></p>
|
|
<img src="https://example.invalid/tracker.png">
|
|
<style>body { background-image:url(https://example.invalid/background.png) }</style>
|
|
<script>window.mailScriptRan = true;</script>
|
|
"""
|
|
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)
|
|
}
|
|
}
|