Files
ggbond268--mactools/Tests/Core/Plugins/PluginHostComponentSupportTests.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

1169 lines
43 KiB
Swift

import AppKit
import SwiftUI
import XCTest
import MacToolsPluginKit
@testable import MacTools
@MainActor
final class PluginHostComponentSupportTests: XCTestCase {
private let suiteName = "PluginHostComponentSupportTests"
override func tearDown() {
UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
super.tearDown()
}
func testComponentPanelPluginOnlyAppearsInComponentItems() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [componentPanelPlugin])
XCTAssertTrue(host.panelItems.isEmpty)
XCTAssertEqual(host.componentItems.map(\.id), ["component"])
XCTAssertEqual(host.featureManagementItems.map(\.presentation), [.componentPanel])
}
func testRefreshingLocalizationDiscardsCachedComponentViewsWithoutRefreshingPlugin() {
let plugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [plugin])
_ = host.componentViewItem(for: "component", dismiss: {})
let makeViewCallCount = plugin.makeViewCallCount
let refreshCallCount = plugin.refreshCallCount
host.refreshLocalization()
XCTAssertFalse(host.isComponentViewCached(for: "component"))
XCTAssertEqual(plugin.makeViewCallCount, makeViewCallCount)
XCTAssertEqual(plugin.refreshCallCount, refreshCallCount)
XCTAssertEqual(plugin.localizationRefreshCount, 1)
XCTAssertTrue(host.componentItems.first?.isActive == false)
}
func testTwoLocalizationRefreshesPreservePluginState() {
let plugin = MockComponentPanelPlugin(id: "component", isActive: true)
let host = makeHost(plugins: [plugin])
let refreshCallCount = plugin.refreshCallCount
let initialRevision = host.localizationRevision
// Runtime language switches are covered by PluginRuntimeLocalizationTests.
// Exercise the host refresh twice directly so this state-preservation
// test is independent of the global locale source shared by parallel tests.
host.refreshLocalization()
host.refreshLocalization()
XCTAssertEqual(host.localizationRevision, initialRevision + 2)
XCTAssertEqual(plugin.refreshCallCount, refreshCallCount)
XCTAssertEqual(plugin.localizationRefreshCount, 2)
XCTAssertTrue(host.componentItems.first?.isActive == true)
}
func testComponentVisibilityUsesSharedDisplayPreferences() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [componentPanelPlugin])
host.setFeatureVisibility(false, for: "component")
XCTAssertTrue(host.componentItems.isEmpty)
XCTAssertEqual(host.featureManagementItems.first?.isVisible, false)
}
func testBuiltInVisibilityLifecycleReceivesHideShowChanges() {
let plugin = MockVisibilityLifecyclePlugin(id: "visibility-lifecycle")
let host = makeHost(plugins: [plugin])
host.setFeatureVisibility(false, for: "visibility-lifecycle")
host.setFeatureVisibility(true, for: "visibility-lifecycle")
XCTAssertEqual(plugin.visibilityChanges, [false, true])
}
func testHiddenBuiltInVisibilityLifecycleIsPausedOnLaunch() {
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
let displayPreferencesStore = PluginDisplayPreferencesStore(userDefaults: defaults)
displayPreferencesStore.setVisibility(
false,
for: "visibility-lifecycle",
defaultPluginIDs: ["visibility-lifecycle"]
)
let plugin = MockVisibilityLifecyclePlugin(id: "visibility-lifecycle")
let host = PluginHost(
plugins: [plugin],
shortcutStore: ShortcutStore(userDefaults: defaults),
pluginDisplayPreferencesStore: displayPreferencesStore,
globalShortcutManager: GlobalShortcutManager()
)
XCTAssertEqual(plugin.visibilityChanges, [false])
XCTAssertEqual(host.featureManagementItems.first?.isVisible, false)
}
func testComponentOrderUsesSharedDisplayPreferences() {
let first = MockComponentPanelPlugin(id: "first", order: 1)
let second = MockComponentPanelPlugin(id: "second", order: 2)
let host = makeHost(plugins: [first, second])
host.moveFeatureManagementItem(id: "second", toOffset: 0)
XCTAssertEqual(host.componentItems.map(\.id), ["second", "first"])
XCTAssertEqual(host.featureManagementItems.map(\.id), ["second", "first"])
}
func testDisplayPreferencesReadingWithEmptyDefaultPluginIDsDoesNotDiscardStoredOrder() {
let store = makeDisplayPreferencesStore()
store.setOrderedPluginIDs(["second", "first"], defaultPluginIDs: ["first", "second"])
XCTAssertTrue(store.orderedPluginIDs(defaultPluginIDs: []).isEmpty)
XCTAssertEqual(
store.orderedPluginIDs(defaultPluginIDs: ["first", "second"]),
["second", "first"]
)
}
func testDisplayPreferencesReadingWithPartialDefaultPluginIDsDoesNotDiscardMissingPluginOrder() {
let store = makeDisplayPreferencesStore()
store.setOrderedPluginIDs(["third", "second", "first"], defaultPluginIDs: ["first", "second", "third"])
XCTAssertEqual(
store.orderedPluginIDs(defaultPluginIDs: ["first"]),
["first"]
)
XCTAssertEqual(
store.orderedPluginIDs(defaultPluginIDs: ["first", "second", "third"]),
["third", "second", "first"]
)
}
func testDisplayPreferencesHiddenPluginSurvivesTemporaryMissingPlugin() {
let store = makeDisplayPreferencesStore()
store.setVisibility(false, for: "dynamic", defaultPluginIDs: ["dynamic"])
XCTAssertTrue(store.isVisible("dynamic", defaultPluginIDs: []))
XCTAssertFalse(store.isVisible("dynamic", defaultPluginIDs: ["dynamic"]))
}
func testComponentOnlyPluginContributesSettingsPermissionsAndShortcuts() {
let componentPanelPlugin = MockComponentPanelPlugin(
id: "component",
permissionRequirements: [
PluginPermissionRequirement(
id: "accessibility",
kind: .accessibility,
title: "辅助功能",
description: "需要辅助功能权限。"
)
],
settingsSections: [
PluginSettingsSection(
id: "settings",
title: "组件设置",
description: "组件设置说明。",
status: .init(text: "正常", systemImage: "checkmark", tone: .positive),
footnote: nil,
buttonTitle: "执行",
actionID: "settings-action"
)
],
shortcutDefinitions: [
PluginShortcutDefinition(
id: "shortcut",
title: "组件快捷键",
description: "触发组件动作。",
actionID: "shortcut-action",
scope: .whilePluginActive,
defaultBinding: nil,
isRequired: false
)
]
)
let host = makeHost(plugins: [componentPanelPlugin])
XCTAssertEqual(host.permissionCards.map(\.pluginID), ["component"])
XCTAssertEqual(host.permissionCards.map(\.iconSystemImage), ["accessibility"])
XCTAssertEqual(host.permissionCards.map(\.iconVisualScale), [1.0])
XCTAssertEqual(host.settingsCards.map(\.pluginID), ["component"])
XCTAssertEqual(host.shortcutItems.map(\.pluginID), ["component"])
XCTAssertEqual(host.pluginConfigurationItems.map(\.id), ["component"])
XCTAssertEqual(host.pluginConfigurationItems.first?.settingsCards.map(\.id), ["component.settings"])
XCTAssertEqual(host.pluginConfigurationItems.first?.permissionCards.map(\.permissionID), ["accessibility"])
XCTAssertEqual(host.pluginConfigurationItems.first?.shortcutItems.map(\.pluginID), ["component"])
}
func testShortcutItemsCarryOptionalSettingsGroupingMetadata() {
let componentPanelPlugin = MockComponentPanelPlugin(
id: "component",
shortcutDefinitions: [
PluginShortcutDefinition(
id: "display-down",
title: "降低亮度",
description: "降低亮度。",
actionID: "display-down",
scope: .global,
defaultBinding: nil,
isRequired: false,
sharedBindingGroupID: "brightness.down",
settingsGroupID: "display.one",
settingsGroupTitle: "Studio Display",
settingsGroupDescription: "可与其他显示器使用相同快捷键,同时调节。",
settingsControlTitle: "降低",
settingsControlSystemImage: "sun.min.fill"
)
]
)
let host = makeHost(plugins: [componentPanelPlugin])
let item = host.shortcutItems.first
XCTAssertEqual(item?.settingsGroupID, "display.one")
XCTAssertEqual(item?.settingsGroupTitle, "Studio Display")
XCTAssertEqual(item?.settingsGroupDescription, "可与其他显示器使用相同快捷键,同时调节。")
XCTAssertEqual(item?.settingsControlTitle, "降低")
XCTAssertEqual(item?.settingsControlSystemImage, "sun.min.fill")
}
func testShortcutsInSameSharedBindingGroupCanUseSameBinding() {
let binding = ShortcutBinding(keyCode: 18, modifiers: [.command, .option])
let componentPanelPlugin = MockComponentPanelPlugin(
id: "component",
shortcutDefinitions: [
PluginShortcutDefinition(
id: "first",
title: "第一个",
description: "第一个动作。",
actionID: "first",
scope: .global,
defaultBinding: nil,
isRequired: false,
sharedBindingGroupID: "brightness.down"
),
PluginShortcutDefinition(
id: "second",
title: "第二个",
description: "第二个动作。",
actionID: "second",
scope: .global,
defaultBinding: nil,
isRequired: false,
sharedBindingGroupID: "brightness.down"
)
]
)
let host = makeHost(plugins: [componentPanelPlugin])
host.setShortcutBinding(binding, for: "component.shortcut.first")
host.setShortcutBinding(binding, for: "component.shortcut.second")
XCTAssertNil(host.shortcutItems.first { $0.id == "component.shortcut.second" }?.errorMessage)
}
func testShortcutsInDifferentSharedBindingGroupsStillRejectDuplicateBindings() {
let binding = ShortcutBinding(keyCode: 18, modifiers: [.command, .option])
let componentPanelPlugin = MockComponentPanelPlugin(
id: "component",
shortcutDefinitions: [
PluginShortcutDefinition(
id: "first",
title: "第一个",
description: "第一个动作。",
actionID: "first",
scope: .global,
defaultBinding: nil,
isRequired: false,
sharedBindingGroupID: "brightness.down"
),
PluginShortcutDefinition(
id: "second",
title: "第二个",
description: "第二个动作。",
actionID: "second",
scope: .global,
defaultBinding: nil,
isRequired: false,
sharedBindingGroupID: "brightness.up"
)
]
)
let host = makeHost(plugins: [componentPanelPlugin])
host.setShortcutBinding(binding, for: "component.shortcut.first")
host.setShortcutBinding(binding, for: "component.shortcut.second")
XCTAssertNotNil(host.shortcutItems.first { $0.id == "component.shortcut.second" }?.errorMessage)
}
func testPluginsWithoutConfigurationSurfaceAreHiddenFromConfigurationList() {
let primaryPanelPlugin = MockPrimaryPanelPlugin(id: "feature")
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(
plugins: [primaryPanelPlugin, componentPanelPlugin]
)
XCTAssertTrue(host.pluginConfigurationItems.isEmpty)
XCTAssertEqual(host.selectedFeatureSettingsPane, .installed)
}
func testConfigurationListUsesSharedPluginOrderAndSelectsFirstItem() {
let first = MockComponentPanelPlugin(
id: "first",
order: 1,
permissionRequirements: [
PluginPermissionRequirement(
id: "first-permission",
kind: .accessibility,
title: "辅助功能",
description: "需要辅助功能权限。"
)
]
)
let second = MockComponentPanelPlugin(
id: "second",
order: 2,
shortcutDefinitions: [
PluginShortcutDefinition(
id: "second-shortcut",
title: "快捷键",
description: "触发动作。",
actionID: "shortcut-action",
scope: .whilePluginActive,
defaultBinding: nil,
isRequired: false
)
]
)
let host = makeHost(plugins: [first, second])
XCTAssertEqual(host.pluginConfigurationItems.map(\.id), ["first", "second"])
XCTAssertEqual(host.selectedFeatureSettingsPane, .installed)
host.moveFeatureManagementItem(id: "second", toOffset: 0)
XCTAssertEqual(host.pluginConfigurationItems.map(\.id), ["second", "first"])
XCTAssertEqual(host.selectedFeatureSettingsPane, .installed)
}
func testFeatureSettingsSelectionIgnoresMissingConfigurationItem() {
let componentPanelPlugin = MockComponentPanelPlugin(
id: "component",
permissionRequirements: [
PluginPermissionRequirement(
id: "accessibility",
kind: .accessibility,
title: "辅助功能",
description: "需要辅助功能权限。"
)
]
)
let host = makeHost(plugins: [componentPanelPlugin])
host.selectFeatureSettingsPane(.configuration("missing"))
XCTAssertEqual(host.selectedFeatureSettingsPane, .installed)
host.selectFeatureSettingsPane(.configuration("component"))
XCTAssertEqual(host.selectedFeatureSettingsPane, .configuration("component"))
}
func testCustomPluginConfigurationContributesConfigurationItemAndCachesView() {
let configurationCounter = ConfigurationRenderCounter()
let componentPanelPlugin = MockComponentPanelPlugin(
id: "component",
configuration: PluginConfiguration(description: "自定义配置") { context in
configurationCounter.makeView(context: context)
}
)
let host = makeHost(plugins: [componentPanelPlugin])
XCTAssertEqual(host.pluginConfigurationItems.map(\.id), ["component"])
XCTAssertEqual(host.pluginConfigurationItems.first?.description, "自定义配置")
XCTAssertEqual(host.pluginConfigurationItems.first?.hasCustomConfiguration, true)
_ = host.pluginConfigurationViewItem(for: "component")
_ = host.pluginConfigurationViewItem(for: "component")
XCTAssertEqual(configurationCounter.callCount, 1)
}
func testPluginStateChangesAreCoalescedAndInvalidateDirtyConfigurationViewCache() async {
let configurationCounter = ConfigurationRenderCounter()
let componentPanelPlugin = MutableComponentPanelPlugin(
id: "component",
configuration: PluginConfiguration(description: "自定义配置") { context in
configurationCounter.makeView(context: context)
}
)
let host = makeHost(
plugins: [componentPanelPlugin],
pluginStateChangeRebuildDelay: .milliseconds(20)
)
_ = host.pluginConfigurationViewItem(for: "component")
XCTAssertEqual(configurationCounter.callCount, 1)
XCTAssertEqual(componentPanelPlugin.componentStateReadCount, 1)
componentPanelPlugin.isActive = true
componentPanelPlugin.triggerStateChange()
componentPanelPlugin.triggerStateChange()
componentPanelPlugin.triggerStateChange()
XCTAssertEqual(componentPanelPlugin.componentStateReadCount, 1)
for _ in 0..<20 where componentPanelPlugin.componentStateReadCount < 2 {
try? await Task.sleep(for: .milliseconds(50))
}
XCTAssertEqual(componentPanelPlugin.componentStateReadCount, 2)
XCTAssertEqual(host.featureManagementItems.first?.isActive, true)
_ = host.pluginConfigurationViewItem(for: "component")
XCTAssertEqual(configurationCounter.callCount, 2)
}
func testPluginStateChangesOnlyReadDirtyPanelState() async throws {
let changingPlugin = CountingPrimaryPanelPlugin(id: "changing", order: 1)
let stablePlugin = CountingPrimaryPanelPlugin(id: "stable", order: 2)
let host = makeHost(
plugins: [changingPlugin, stablePlugin],
pluginStateChangeRebuildDelay: .milliseconds(20)
)
changingPlugin.panelStateReadCount = 0
stablePlugin.panelStateReadCount = 0
changingPlugin.primarySubtitle = "changed"
changingPlugin.onStateChange?()
changingPlugin.primarySubtitle = "changed again"
changingPlugin.onStateChange?()
changingPlugin.onStateChange?()
try await Task.sleep(for: .milliseconds(80))
XCTAssertEqual(changingPlugin.panelStateReadCount, 1)
XCTAssertEqual(stablePlugin.panelStateReadCount, 0)
XCTAssertEqual(host.panelItems.map(\.description), ["changed again", "Feature stable"])
}
func testComponentActiveStateContributesToHasActivePlugin() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component", isActive: true)
let host = makeHost(plugins: [componentPanelPlugin])
XCTAssertTrue(host.hasActivePlugin)
XCTAssertEqual(host.featureManagementItems.first?.isActive, true)
}
func testComponentViewsAreCachedForFastPanelPresentation() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [componentPanelPlugin])
XCTAssertFalse(host.isComponentViewCached(for: "component"))
let first = host.componentViewItem(for: "component", dismiss: {})
let second = host.componentViewItem(for: "component", dismiss: {})
XCTAssertEqual(first.id, "component")
XCTAssertEqual(second.id, "component")
XCTAssertEqual(componentPanelPlugin.makeViewCallCount, 1)
XCTAssertTrue(host.isComponentViewCached(for: "component"))
}
func testDiscardComponentViewsReleasesCachedComponentContent() {
let first = MockComponentPanelPlugin(id: "first", order: 1)
let second = MockComponentPanelPlugin(id: "second", order: 2)
let host = makeHost(plugins: [first, second])
_ = host.componentViewItem(for: "first", dismiss: {})
_ = host.componentViewItem(for: "second", dismiss: {})
host.discardComponentViews()
_ = host.componentViewItem(for: "first", dismiss: {})
_ = host.componentViewItem(for: "second", dismiss: {})
XCTAssertEqual(first.makeViewCallCount, 2)
XCTAssertEqual(second.makeViewCallCount, 2)
}
func testComponentContextUsesVisiblePresentationForCachedViews() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [componentPanelPlugin])
_ = host.componentViewItem(for: "component", dismiss: {})
XCTAssertEqual(componentPanelPlugin.receivedPanelVisibilityValues, [true])
}
func testPrewarmingComponentViewsBuildsStableVisiblePresentationOnce() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [componentPanelPlugin])
host.prewarmComponentViews(dismiss: {})
_ = host.componentViewItem(for: "component", dismiss: {})
XCTAssertEqual(componentPanelPlugin.makeViewCallCount, 1)
XCTAssertEqual(componentPanelPlugin.receivedPanelVisibilityValues, [true])
XCTAssertTrue(host.isComponentViewCached(for: "component"))
}
func testComponentSurfaceLifecycleEventsAreSentWhenPanelVisibilityChanges() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let host = makeHost(plugins: [componentPanelPlugin])
host.setPanelSurface(.component, visible: true)
host.setPanelSurface(.component, visible: true)
host.setPanelSurface(.component, visible: false)
host.setPanelSurface(.component, visible: false)
XCTAssertEqual(componentPanelPlugin.surfaceEvents, [
.visible(.component),
.hidden(.component)
])
}
func testComponentSurfaceLifecycleHidesPluginWhenFeatureIsHidden() {
let componentPanelPlugin = MockComponentPanelPlugin(id: "component")
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
let host = PluginHost(
plugins: [componentPanelPlugin],
shortcutStore: ShortcutStore(userDefaults: defaults),
pluginDisplayPreferencesStore: PluginDisplayPreferencesStore(userDefaults: defaults),
globalShortcutManager: GlobalShortcutManager()
)
host.setPanelSurface(.component, visible: true)
host.setFeatureVisibility(false, for: "component")
XCTAssertEqual(componentPanelPlugin.surfaceEvents, [
.visible(.component),
.hidden(.component)
])
XCTAssertTrue(host.componentItems.isEmpty)
}
func testPrimaryPanelPluginAppearsOnlyInPanelItems() {
let primaryPanelPlugin = MockPrimaryPanelPlugin(id: "feature")
let host = makeHost(plugins: [primaryPanelPlugin])
XCTAssertEqual(host.panelItems.map(\.id), ["feature"])
XCTAssertTrue(host.componentItems.isEmpty)
XCTAssertEqual(host.featureManagementItems.map(\.presentation), [.featurePanel])
}
func testPluginCanContributeFeatureAndComponentPanels() {
let plugin = MockCombinedPlugin(id: "combined")
let host = makeHost(plugins: [plugin])
XCTAssertEqual(host.panelItems.map(\.id), ["combined"])
XCTAssertEqual(host.componentItems.map(\.id), ["combined"])
XCTAssertEqual(host.featureManagementItems.map(\.presentation), [.featureAndComponentPanel])
}
func testDynamicPluginManagerCanRemoveEnabledPluginFromDerivedState() {
let firstPlugin = MockPrimaryPanelPlugin(id: "dynamic")
let secondPlugin = MockPrimaryPanelPlugin(id: "second")
let rootDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent("PluginHostComponentSupportTests-\(UUID().uuidString)")
let store = PluginPackageStore(
rootDirectory: rootDirectory,
userDefaults: UserDefaults(suiteName: suiteName)!,
hostVersion: "1.0.0"
)
let dynamicRecord = installTestPluginPackage(
id: "dynamic",
bundleName: "Dynamic.bundle",
capabilities: .init(primaryPanel: true),
store: store
)
_ = installTestPluginPackage(
id: "second",
bundleName: "Second.bundle",
capabilities: .init(primaryPanel: true),
store: store
)
let loader = StubDynamicPluginLoader { records in
records.map { record in
let plugin = record.id == "dynamic" ? firstPlugin : secondPlugin
return DynamicPluginLoadResult(record: record, plugins: [plugin], errorMessage: nil)
}
}
let manager = DynamicPluginManager(
packageStore: store,
pluginLoader: loader
)
let host = makeHost(plugins: [], dynamicPluginManager: manager)
XCTAssertEqual(host.panelItems.map(\.id), ["dynamic", "second"])
try? FileManager.default.removeItem(at: dynamicRecord.packageURL)
manager.reloadInstalledPlugins()
XCTAssertEqual(host.panelItems.map(\.id), ["second"])
XCTAssertEqual(host.featureManagementItems.map(\.id), ["second"])
try? FileManager.default.removeItem(at: rootDirectory)
}
func testDynamicPluginConfigurationGetterIsNotReadWhenManifestDoesNotDeclareConfiguration() {
let plugin = ConfigurationTrapPlugin(id: "dynamic")
let rootDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent("PluginHostComponentSupportTests-\(UUID().uuidString)")
let store = PluginPackageStore(
rootDirectory: rootDirectory,
userDefaults: UserDefaults(suiteName: suiteName)!,
hostVersion: "1.0.0"
)
_ = installTestPluginPackage(
id: "dynamic",
bundleName: "Dynamic.bundle",
capabilities: .init(primaryPanel: true, configuration: false),
store: store
)
let loader = StubDynamicPluginLoader { records in
records.map { record in
DynamicPluginLoadResult(record: record, plugins: [plugin], errorMessage: nil)
}
}
let manager = DynamicPluginManager(
packageStore: store,
pluginLoader: loader
)
let host = makeHost(plugins: [], dynamicPluginManager: manager)
XCTAssertEqual(host.panelItems.map(\.id), ["dynamic"])
XCTAssertTrue(host.pluginConfigurationItems.isEmpty)
XCTAssertEqual(plugin.configurationReadCount, 0)
try? FileManager.default.removeItem(at: rootDirectory)
}
private func makeHost(
plugins: [any MacToolsPlugin] = [],
dynamicPluginManager: DynamicPluginManager? = nil,
displayConfigurationObserver: (any DisplayConfigurationObserving)? = nil,
displayTopologyRefreshDelay: Duration = .milliseconds(180),
pluginStateChangeRebuildDelay: Duration = .milliseconds(80)
) -> PluginHost {
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return PluginHost(
plugins: plugins,
dynamicPluginManager: dynamicPluginManager,
shortcutStore: ShortcutStore(userDefaults: defaults),
pluginDisplayPreferencesStore: PluginDisplayPreferencesStore(userDefaults: defaults),
globalShortcutManager: GlobalShortcutManager(),
displayConfigurationObserver: displayConfigurationObserver,
displayTopologyRefreshDelay: displayTopologyRefreshDelay,
pluginStateChangeRebuildDelay: pluginStateChangeRebuildDelay
)
}
private func makeDisplayPreferencesStore() -> PluginDisplayPreferencesStore {
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return PluginDisplayPreferencesStore(userDefaults: defaults)
}
private func installTestPluginPackage(
id: String,
bundleName: String,
capabilities: PluginPackageManifest.Capabilities = .init(),
store: PluginPackageStore
) -> PluginPackageRecord {
let sourceURL = store.rootDirectory
.appendingPathComponent("Source", isDirectory: true)
.appendingPathComponent(id, isDirectory: true)
.appendingPathExtension("mactoolsplugin")
let bundleURL = sourceURL.appendingPathComponent(bundleName, isDirectory: true)
try? FileManager.default.createDirectory(at: bundleURL, withIntermediateDirectories: true)
let manifest = PluginPackageManifest(
id: id,
displayName: id,
version: "1.0.0",
minHostVersion: "0.1.0",
bundleRelativePath: bundleName,
capabilities: capabilities
)
let data = try? JSONEncoder().encode(manifest)
try? data?.write(to: sourceURL.appendingPathComponent("plugin.json"))
return try! store.installPackage(from: sourceURL)
}
}
@MainActor
private final class StubDynamicPluginLoader: DynamicPluginLoading {
private let handler: ([PluginPackageRecord]) -> [DynamicPluginLoadResult]
private(set) var receivedRecordIDs: [String] = []
init(handler: @escaping ([PluginPackageRecord]) -> [DynamicPluginLoadResult]) {
self.handler = handler
}
func loadInstalledPlugins(from records: [PluginPackageRecord]) -> [DynamicPluginLoadResult] {
receivedRecordIDs = records.map(\.id)
return handler(records)
}
}
@MainActor
private final class MockDisplayConfigurationObserver: DisplayConfigurationObserving {
var onConfigurationChange: (() -> Void)?
func triggerChange() {
onConfigurationChange?()
}
}
@MainActor
private final class MockVisibilityLifecyclePlugin: MacToolsPlugin, PluginPrimaryPanel, PluginFeatureVisibilityLifecycleHandling {
let metadata: PluginMetadata
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .keepPresented
)
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
private(set) var visibilityChanges: [Bool] = []
init(id: String) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemPurple),
order: 1,
defaultDescription: "Visibility lifecycle \(id)"
)
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: "Visible",
isOn: false,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
func handleAction(_ action: PluginPanelAction) {}
func featureVisibilityDidChange(_ isVisible: Bool) {
visibilityChanges.append(isVisible)
}
}
@MainActor
private final class MockComponentPanelPlugin: MacToolsPlugin, PluginComponentPanel, PluginPanelSurfaceLifecycleHandling, PluginRuntimeLocalizationRefreshing {
enum SurfaceEvent: Equatable {
case visible(PluginPanelSurface)
case hidden(PluginPanelSurface)
}
let metadata: PluginMetadata
let descriptor: PluginComponentDescriptor
let permissionRequirements: [PluginPermissionRequirement]
let settingsSections: [PluginSettingsSection]
let shortcutDefinitions: [PluginShortcutDefinition]
let configuration: PluginConfiguration?
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
private let isActive: Bool
private(set) var makeViewCallCount = 0
private(set) var refreshCallCount = 0
private(set) var localizationRefreshCount = 0
private(set) var receivedPanelVisibilityValues: [Bool] = []
private(set) var surfaceEvents: [SurfaceEvent] = []
init(
id: String,
order: Int = 1,
span: PluginComponentSpan = .oneByOne,
isActive: Bool = false,
permissionRequirements: [PluginPermissionRequirement] = [],
settingsSections: [PluginSettingsSection] = [],
shortcutDefinitions: [PluginShortcutDefinition] = [],
configuration: PluginConfiguration? = nil
) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemPurple),
order: order,
defaultDescription: "Component \(id)"
)
self.descriptor = PluginComponentDescriptor(span: span)
self.isActive = isActive
self.permissionRequirements = permissionRequirements
self.settingsSections = settingsSections
self.shortcutDefinitions = shortcutDefinitions
self.configuration = configuration
}
var componentPanelState: PluginComponentState {
PluginComponentState(
subtitle: "Component subtitle",
isActive: isActive,
isEnabled: true,
isVisible: true,
errorMessage: nil
)
}
func makeView(context: PluginComponentContext) -> AnyView {
makeViewCallCount += 1
receivedPanelVisibilityValues.append(context.isPanelVisible)
return AnyView(Text(context.pluginID))
}
func panelSurfaceDidBecomeVisible(_ surface: PluginPanelSurface) {
surfaceEvents.append(.visible(surface))
}
func panelSurfaceDidBecomeHidden(_ surface: PluginPanelSurface) {
surfaceEvents.append(.hidden(surface))
}
func refresh() {
refreshCallCount += 1
}
func refreshLocalization() {
localizationRefreshCount += 1
}
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
func handlePermissionAction(id: String) {}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {}
}
@MainActor
private final class MutableComponentPanelPlugin: MacToolsPlugin, PluginComponentPanel {
let metadata: PluginMetadata
let descriptor = PluginComponentDescriptor(span: .oneByOne)
let configuration: PluginConfiguration?
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
var isActive = false
var onComponentStateRead: (() -> Void)?
private(set) var componentStateReadCount = 0
init(id: String, configuration: PluginConfiguration? = nil) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemPurple),
order: 1,
defaultDescription: "Component \(id)"
)
self.configuration = configuration
}
var componentPanelState: PluginComponentState {
componentStateReadCount += 1
onComponentStateRead?()
return PluginComponentState(
subtitle: "Component subtitle",
isActive: isActive,
isEnabled: true,
isVisible: true,
errorMessage: nil
)
}
func makeView(context: PluginComponentContext) -> AnyView {
AnyView(Text(context.pluginID))
}
func triggerStateChange() {
onStateChange?()
}
}
@MainActor
private final class ConfigurationRenderCounter {
private(set) var callCount = 0
func makeView(context: PluginConfigurationContext) -> AnyView {
callCount += 1
return AnyView(Text(context.pluginID))
}
}
@MainActor
private final class MockPrimaryPanelPlugin: MacToolsPlugin, PluginPrimaryPanel {
let metadata: PluginMetadata
let primaryPanelDescriptor: PluginPrimaryPanelDescriptor
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
var refreshCallCount = 0
init(id: String, order: Int = 1) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemBlue),
order: order,
defaultDescription: "Feature \(id)"
)
self.primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .keepPresented
)
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: "Feature subtitle",
isOn: false,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var permissionRequirements: [PluginPermissionRequirement] { [] }
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
func refresh() {
refreshCallCount += 1
}
func handleAction(_ action: PluginPanelAction) {}
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
func handlePermissionAction(id: String) {}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {}
}
@MainActor
private final class CountingPrimaryPanelPlugin: MacToolsPlugin, PluginPrimaryPanel {
let metadata: PluginMetadata
let primaryPanelDescriptor: PluginPrimaryPanelDescriptor
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
var primarySubtitle: String
var panelStateReadCount = 0
init(id: String, order: Int) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemBlue),
order: order,
defaultDescription: "Feature \(id)"
)
self.primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .keepPresented
)
self.primarySubtitle = ""
}
var primaryPanelState: PluginPanelState {
panelStateReadCount += 1
return PluginPanelState(
subtitle: primarySubtitle,
isOn: false,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var permissionRequirements: [PluginPermissionRequirement] { [] }
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
func refresh() {}
func handleAction(_ action: PluginPanelAction) {}
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
func handlePermissionAction(id: String) {}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {}
}
@MainActor
private final class ConfigurationTrapPlugin: MacToolsPlugin, PluginPrimaryPanel {
let metadata: PluginMetadata
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .button,
menuActionBehavior: .dismissBeforeHandling,
buttonTitle: "执行"
)
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
private(set) var configurationReadCount = 0
init(id: String) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemIndigo),
order: 1,
defaultDescription: "Dynamic \(id)"
)
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: "Dynamic subtitle",
isOn: false,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var configuration: PluginConfiguration? {
configurationReadCount += 1
return PluginConfiguration(description: "Should not be read") { _ in
Text("Unexpected")
}
}
func handleAction(_ action: PluginPanelAction) {}
}
@MainActor
private final class MockCombinedPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginComponentPanel {
let metadata: PluginMetadata
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .keepPresented
)
let descriptor = PluginComponentDescriptor(span: .oneByOne)
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
init(id: String) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "sparkles",
iconTint: Color(nsColor: .systemPurple),
order: 1,
defaultDescription: "Combined \(id)"
)
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: "Combined subtitle",
isOn: false,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var componentPanelState: PluginComponentState {
PluginComponentState(
subtitle: "Combined component subtitle",
isActive: false,
isEnabled: true,
isVisible: true,
errorMessage: nil
)
}
func makeView(context: PluginComponentContext) -> AnyView {
AnyView(Text(context.pluginID))
}
func handleAction(_ action: PluginPanelAction) {}
}
@MainActor
private final class MockDisplayTopologyPlugin: MacToolsPlugin, PluginPrimaryPanel, DisplayTopologyRefreshing {
let metadata: PluginMetadata
let primaryPanelDescriptor: PluginPrimaryPanelDescriptor
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
var refreshCallCount = 0
var refreshDisplayTopologyCallCount = 0
init(id: String) {
self.metadata = PluginMetadata(
id: id,
title: id,
iconName: "display",
iconTint: Color(nsColor: .systemBlue),
order: 1,
defaultDescription: "Display \(id)"
)
self.primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .disclosure,
menuActionBehavior: .keepPresented
)
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: "Display subtitle \(refreshDisplayTopologyCallCount)",
isOn: false,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var permissionRequirements: [PluginPermissionRequirement] { [] }
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
func refresh() {
refreshCallCount += 1
}
func refreshDisplayTopology() {
refreshDisplayTopologyCallCount += 1
}
func handleAction(_ action: PluginPanelAction) {}
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
func handlePermissionAction(id: String) {}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {}
}