190 lines
6.4 KiB
Swift
190 lines
6.4 KiB
Swift
import AppKit
|
|
import Combine
|
|
import SwiftUI
|
|
import XCTest
|
|
import MacToolsPluginKit
|
|
@testable import MacTools
|
|
|
|
@MainActor
|
|
final class PluginHostDisclosureStateTests: XCTestCase {
|
|
private let suiteName = "PluginHostDisclosureStateTests"
|
|
|
|
override func tearDown() {
|
|
UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
|
|
super.tearDown()
|
|
}
|
|
|
|
func testDisclosureExpansionDoesNotMarkPluginActive() {
|
|
let plugin = MockDisclosurePlugin()
|
|
let host = makeHost(plugin: plugin)
|
|
|
|
XCTAssertFalse(host.hasActivePlugin)
|
|
XCTAssertFalse(host.featureManagementItems[0].isActive)
|
|
XCTAssertFalse(host.panelItems[0].isExpanded)
|
|
|
|
host.setDisclosureExpanded(true, for: plugin.metadata.id)
|
|
|
|
XCTAssertTrue(host.panelItems[0].isExpanded)
|
|
XCTAssertFalse(host.featureManagementItems[0].isActive)
|
|
XCTAssertFalse(host.hasActivePlugin)
|
|
}
|
|
|
|
func testErrorMessageMapsToErrorDescriptionTone() {
|
|
let plugin = MockDisclosurePlugin()
|
|
plugin.errorMessage = "切换失败:显示器已断开连接"
|
|
let host = makeHost(plugin: plugin)
|
|
|
|
switch host.panelItems[0].descriptionTone {
|
|
case .error:
|
|
break
|
|
case .secondary:
|
|
XCTFail("Expected .error when state.errorMessage is non-nil")
|
|
}
|
|
}
|
|
|
|
func testRebuildReadsPanelStateOncePerPlugin() {
|
|
let plugin = MockDisclosurePlugin()
|
|
let host = makeHost(plugin: plugin)
|
|
plugin.stateReadCount = 0
|
|
|
|
host.setDisclosureExpanded(true, for: plugin.metadata.id)
|
|
|
|
XCTAssertEqual(plugin.stateReadCount, 1)
|
|
}
|
|
|
|
func testOptionalPrimaryPanelIndicatorMapsByPluginID() {
|
|
let plugin = MockDisclosurePlugin()
|
|
plugin.indicator = PluginPrimaryPanelIndicator(text: "屏幕常亮", systemImage: "display")
|
|
let host = makeHost(plugin: plugin)
|
|
|
|
XCTAssertEqual(host.primaryPanelIndicatorsByID[plugin.metadata.id], plugin.indicator)
|
|
}
|
|
|
|
func testIncrementalRebuildDoesNotRereadNilIndicatorForUnrelatedPlugin() async throws {
|
|
let changingPlugin = MockDisclosurePlugin(id: "changing", order: 1)
|
|
let stablePlugin = MockDisclosurePlugin(id: "stable", order: 2)
|
|
let host = makeHost(
|
|
plugins: [changingPlugin, stablePlugin],
|
|
pluginStateChangeRebuildDelay: .milliseconds(20)
|
|
)
|
|
changingPlugin.indicatorReadCount = 0
|
|
stablePlugin.indicatorReadCount = 0
|
|
|
|
changingPlugin.onStateChange?()
|
|
try await Task.sleep(for: .milliseconds(80))
|
|
|
|
XCTAssertEqual(changingPlugin.indicatorReadCount, 1)
|
|
XCTAssertEqual(stablePlugin.indicatorReadCount, 0)
|
|
XCTAssertTrue(host.primaryPanelIndicatorsByID.isEmpty)
|
|
}
|
|
|
|
func testPrimaryPanelIndicatorChangesPublishDirectly() async throws {
|
|
let plugin = MockDisclosurePlugin()
|
|
let host = makeHost(
|
|
plugins: [plugin],
|
|
pluginStateChangeRebuildDelay: .milliseconds(20)
|
|
)
|
|
let expectedIndicator = PluginPrimaryPanelIndicator(text: "屏幕常亮", systemImage: "display")
|
|
var publishedIndicators: [[String: PluginPrimaryPanelIndicator]] = []
|
|
let cancellable = host.$primaryPanelIndicatorsByID
|
|
.dropFirst()
|
|
.sink { publishedIndicators.append($0) }
|
|
|
|
plugin.indicator = expectedIndicator
|
|
plugin.onStateChange?()
|
|
try await Task.sleep(for: .milliseconds(80))
|
|
|
|
XCTAssertEqual(publishedIndicators.last?[plugin.metadata.id], expectedIndicator)
|
|
withExtendedLifetime(cancellable) {}
|
|
}
|
|
|
|
private func makeHost(plugin: MockDisclosurePlugin) -> PluginHost {
|
|
makeHost(plugins: [plugin])
|
|
}
|
|
|
|
private func makeHost(
|
|
plugins: [any MacToolsPlugin],
|
|
pluginStateChangeRebuildDelay: Duration = .milliseconds(80)
|
|
) -> PluginHost {
|
|
let defaults = UserDefaults(suiteName: suiteName)!
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
|
|
return PluginHost(
|
|
plugins: plugins,
|
|
shortcutStore: ShortcutStore(userDefaults: defaults),
|
|
pluginDisplayPreferencesStore: PluginDisplayPreferencesStore(userDefaults: defaults),
|
|
globalShortcutManager: GlobalShortcutManager(),
|
|
pluginStateChangeRebuildDelay: pluginStateChangeRebuildDelay
|
|
)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class MockDisclosurePlugin: MacToolsPlugin, PluginPrimaryPanel, PluginPrimaryPanelIndicatorProviding {
|
|
let metadata: PluginMetadata
|
|
|
|
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
|
|
controlStyle: .disclosure,
|
|
menuActionBehavior: .keepPresented
|
|
)
|
|
|
|
var onStateChange: (() -> Void)?
|
|
var requestPermissionGuidance: ((String) -> Void)?
|
|
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
|
|
var isExpanded = false
|
|
var errorMessage: String?
|
|
var indicator: PluginPrimaryPanelIndicator?
|
|
var stateReadCount = 0
|
|
var indicatorReadCount = 0
|
|
|
|
init(id: String = "mock-disclosure", order: Int = 1) {
|
|
metadata = PluginMetadata(
|
|
id: id,
|
|
title: "Mock Disclosure",
|
|
iconName: "display",
|
|
iconTint: Color(nsColor: .systemBlue),
|
|
order: order,
|
|
defaultDescription: "Mock plugin"
|
|
)
|
|
}
|
|
|
|
var primaryPanelIndicator: PluginPrimaryPanelIndicator? {
|
|
indicatorReadCount += 1
|
|
return indicator
|
|
}
|
|
|
|
var primaryPanelState: PluginPanelState {
|
|
stateReadCount += 1
|
|
return PluginPanelState(
|
|
subtitle: "Mock plugin",
|
|
isOn: false,
|
|
isExpanded: isExpanded,
|
|
isEnabled: true,
|
|
isVisible: true,
|
|
detail: nil,
|
|
errorMessage: errorMessage
|
|
)
|
|
}
|
|
|
|
var permissionRequirements: [PluginPermissionRequirement] { [] }
|
|
var settingsSections: [PluginSettingsSection] { [] }
|
|
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
|
|
|
|
func refresh() {}
|
|
|
|
func handleAction(_ action: PluginPanelAction) {
|
|
if case let .setDisclosureExpanded(value) = action {
|
|
isExpanded = value
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
func permissionState(for permissionID: String) -> PluginPermissionState {
|
|
PluginPermissionState(isGranted: true, footnote: nil)
|
|
}
|
|
|
|
func handlePermissionAction(id: String) {}
|
|
func handleSettingsAction(id: String) {}
|
|
func handleShortcutAction(id: String) {}
|
|
}
|