Files
ggbond268--mactools/Tests/Core/RightClick/RightClickConfigurationTests.swift
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

238 lines
9.5 KiB
Swift

import XCTest
@testable import MacTools
final class RightClickConfigurationStoreTests: XCTestCase {
private func makeTempFileURL() -> URL {
FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
.appendingPathComponent("right-click-menu.json")
}
func testSaveThenLoadRoundTrips() throws {
let fileURL = makeTempFileURL()
defer { try? FileManager.default.removeItem(at: fileURL.deletingLastPathComponent()) }
var config = RightClickConfiguration.activeDefault
config.preferredLanguages = ["en"]
config.openInTerminal = false
config.copyFileURL = false
config.openWithApps = [
RightClickOpenWithApp(name: "Code", appPath: "/Applications/Code.app", fileExtensions: ["txt", "md"])
]
XCTAssertTrue(RightClickConfigurationStore.save(config, to: fileURL))
let loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertEqual(loaded, config)
XCTAssertEqual(loaded.preferredLanguages, ["en"])
XCTAssertFalse(loaded.openInTerminal)
XCTAssertEqual(loaded.openWithApps.first?.name, "Code")
XCTAssertEqual(loaded.openWithApps.first?.fileExtensions, ["txt", "md"])
}
func testLoadMissingFileReturnsInactiveDefault() {
XCTAssertEqual(RightClickConfigurationStore.load(from: makeTempFileURL()), .inactiveDefault)
}
func testLoadCorruptDataReturnsInactiveDefault() throws {
let fileURL = makeTempFileURL()
defer { try? FileManager.default.removeItem(at: fileURL.deletingLastPathComponent()) }
try FileManager.default.createDirectory(
at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true
)
try Data("not json".utf8).write(to: fileURL)
XCTAssertEqual(RightClickConfigurationStore.load(from: fileURL), .inactiveDefault)
}
/// A config written before newer keys existed must decode with defaults for
/// the missing keys instead of failing the whole decode.
func testDecodeToleratesMissingKeys() throws {
let fileURL = makeTempFileURL()
defer { try? FileManager.default.removeItem(at: fileURL.deletingLastPathComponent()) }
try FileManager.default.createDirectory(
at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true
)
try Data(#"{"newFolder":false}"#.utf8).write(to: fileURL)
let loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertTrue(loaded.menuEnabled) // missing key -> active default for existing configs
XCTAssertNil(loaded.preferredLanguages)
XCTAssertFalse(loaded.newFolder) // present key honored
XCTAssertTrue(loaded.copyFileName) // missing key → default
XCTAssertTrue(loaded.openInTerminal) // missing key → default
XCTAssertEqual(loaded.openWithApps, [])
}
func testSetMenuEnabledInitializesActiveDefaultsWhenMissing() {
let fileURL = makeTempFileURL()
defer { try? FileManager.default.removeItem(at: fileURL.deletingLastPathComponent()) }
RightClickConfigurationStore.setMenuEnabled(true, fileURL: fileURL)
let loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertTrue(loaded.menuEnabled)
XCTAssertEqual(loaded, .activeDefault)
}
func testSetMenuEnabledPreservesExistingSettings() throws {
let fileURL = makeTempFileURL()
defer { try? FileManager.default.removeItem(at: fileURL.deletingLastPathComponent()) }
var config = RightClickConfiguration.activeDefault
config.copyFileName = false
config.openWithApps = [
RightClickOpenWithApp(name: "Code", appPath: "/Applications/Code.app")
]
XCTAssertTrue(RightClickConfigurationStore.save(config, to: fileURL))
RightClickConfigurationStore.setMenuEnabled(false, fileURL: fileURL)
var loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertFalse(loaded.menuEnabled)
XCTAssertFalse(loaded.copyFileName)
XCTAssertEqual(loaded.openWithApps.first?.name, "Code")
RightClickConfigurationStore.setMenuEnabled(true, fileURL: fileURL)
loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertTrue(loaded.menuEnabled)
XCTAssertFalse(loaded.copyFileName)
XCTAssertEqual(loaded.openWithApps.first?.name, "Code")
}
func testTwoLanguageSwitchesPreserveFinderMenuStateAndUseFreshStrings() {
let fileURL = makeTempFileURL()
defer { try? FileManager.default.removeItem(at: fileURL.deletingLastPathComponent()) }
var config = RightClickConfiguration.activeDefault
config.copyFileName = false
config.openInTerminal = true
config.preferredLanguages = ["en"]
XCTAssertTrue(RightClickConfigurationStore.save(config, to: fileURL))
var loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertEqual(
RightClickLocalization.string(
"finder.openInTerminal",
defaultValue: "在终端打开",
preferredLanguages: loaded.preferredLanguages
),
"Open in Terminal"
)
config.preferredLanguages = ["zh-Hans"]
XCTAssertTrue(RightClickConfigurationStore.save(config, to: fileURL))
loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertEqual(
RightClickLocalization.string(
"finder.openInTerminal",
defaultValue: "Open in Terminal",
preferredLanguages: loaded.preferredLanguages
),
"在终端打开"
)
config.preferredLanguages = ["en"]
XCTAssertTrue(RightClickConfigurationStore.save(config, to: fileURL))
loaded = RightClickConfigurationStore.load(from: fileURL)
XCTAssertEqual(loaded.preferredLanguages, ["en"])
XCTAssertFalse(loaded.copyFileName)
XCTAssertTrue(loaded.openInTerminal)
}
}
final class RightClickOpenWithAppTests: XCTestCase {
func testMatchesListedExtensionsCaseInsensitively() {
let app = RightClickOpenWithApp(name: "Editor", appPath: "/E.app", fileExtensions: ["txt", "md"])
XCTAssertTrue(app.matches(fileExtension: "txt"))
XCTAssertTrue(app.matches(fileExtension: "TXT"))
XCTAssertTrue(app.matches(fileExtension: "md"))
XCTAssertFalse(app.matches(fileExtension: "png"))
}
func testEmptyExtensionsMatchesEverything() {
let app = RightClickOpenWithApp(name: "Editor", appPath: "/E.app", fileExtensions: [])
XCTAssertTrue(app.matches(fileExtension: "anything"))
XCTAssertTrue(app.matches(fileExtension: ""))
}
}
final class RightClickOpenWithParsingTests: XCTestCase {
private func url(_ string: String) -> URL {
var components = URLComponents()
components.scheme = "mactools"
components.host = "right-click"
components.path = "/open-with"
components.queryItems = queryItems(from: string)
return components.url!
}
private func queryItems(from pairs: String) -> [URLQueryItem] {
pairs.split(separator: "&").map { pair in
let parts = pair.split(separator: "=", maxSplits: 1).map(String.init)
return URLQueryItem(name: parts[0], value: parts.count > 1 ? parts[1] : nil)
}
}
func testParseAcceptsValidAppAndFiles() {
let request = RightClickURLRouter.parseOpenWithRequest(
url("app=/Applications/Code.app&file=/tmp/a.txt&file=/tmp/b.md"),
fileExists: { _ in true },
isApplicationBundle: { _ in true }
)
XCTAssertEqual(request?.appURL.path, "/Applications/Code.app")
XCTAssertEqual(request?.files.map(\.path), ["/tmp/a.txt", "/tmp/b.md"])
}
func testParseRejectsNonAppBundle() {
XCTAssertNil(RightClickURLRouter.parseOpenWithRequest(
url("app=/tmp/x&file=/tmp/a.txt"),
fileExists: { _ in true },
isApplicationBundle: { _ in false }
))
}
func testParseRejectsWhenNoFilesExist() {
XCTAssertNil(RightClickURLRouter.parseOpenWithRequest(
url("app=/Applications/Code.app&file=/tmp/missing"),
fileExists: { _ in false },
isApplicationBundle: { _ in true }
))
}
func testParseDropsMissingFilesKeepsExisting() {
let request = RightClickURLRouter.parseOpenWithRequest(
url("app=/Applications/Code.app&file=/tmp/exists&file=/tmp/missing"),
fileExists: { $0 == "/tmp/exists" },
isApplicationBundle: { _ in true }
)
XCTAssertEqual(request?.files.map(\.path), ["/tmp/exists"])
}
}
final class RightClickURLSchemeTests: XCTestCase {
func testBundleURLSchemesReadsDebugSchemeFromBundleInfo() {
let bundle = URLSchemeBundleMock(urlTypes: [
["CFBundleURLSchemes": ["mactools-dev"]]
])
XCTAssertEqual(RightClickURLRouter.bundleURLSchemes(bundle: bundle), ["mactools-dev"])
}
func testBundleURLSchemesFallsBackToReleaseSchemeWhenMissing() {
XCTAssertEqual(RightClickURLRouter.bundleURLSchemes(bundle: URLSchemeBundleMock(urlTypes: nil)), ["mactools"])
}
}
private final class URLSchemeBundleMock: Bundle, @unchecked Sendable {
private let urlTypes: [[String: Any]]?
init(urlTypes: [[String: Any]]?) {
self.urlTypes = urlTypes
super.init()
}
override func object(forInfoDictionaryKey key: String) -> Any? {
key == "CFBundleURLTypes" ? urlTypes : nil
}
}