diff --git a/ios/Sources/GutenbergKit/Sources/EditorLocalization.swift b/ios/Sources/GutenbergKit/Sources/EditorLocalization.swift index 6f11ad9ae..8d8173d83 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorLocalization.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorLocalization.swift @@ -21,6 +21,13 @@ public enum EditorLocalizableString { // MARK: - Editor Loading case loadingEditor case editorError + + // MARK: - Lockdown Mode + case lockdownModeTitle + case lockdownModeWarning + case lockdownModeExcludeHint + case lockdownModeLearnMore + case lockdownModeDismiss } /// Provides localized strings for the editor. @@ -46,6 +53,11 @@ public final class EditorLocalization { case .patternsCategoryAll: "All" case .loadingEditor: "Loading Editor" case .editorError: "Editor Error" + case .lockdownModeTitle: "Lockdown Mode Detected" + case .lockdownModeWarning: "Lockdown Mode is enabled. The editor may not work correctly." + case .lockdownModeExcludeHint: "You can exclude this app from Lockdown Mode in Settings to restore full functionality." + case .lockdownModeLearnMore: "Learn More" + case .lockdownModeDismiss: "Dismiss" } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index bfd11bc82..38d9bdeb6 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -109,6 +109,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro private let mediaPicker: MediaPickerController? private let controller: GutenbergEditorController private let bundleProvider: EditorAssetBundleProvider + private let lockdownModeMonitor: LockdownModeMonitor // MARK: - Private Properties (UI) @@ -170,7 +171,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro ) self.bundleProvider = EditorAssetBundleProvider(httpClient: httpClient) self.mediaPicker = mediaPicker - self.controller = GutenbergEditorController(configuration: configuration) + self.lockdownModeMonitor = LockdownModeMonitor() + self.controller = GutenbergEditorController(configuration: configuration, lockdownModeMonitor: self.lockdownModeMonitor) // The `allowFileAccessFromFileURLs` allows the web view to access the // files from the local filesystem. @@ -211,6 +213,14 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro controller.delegate = self webView.navigationDelegate = controller + // Set up Lockdown Mode monitoring with foreground detection + lockdownModeMonitor.setup( + presentingViewController: self, + onResetReadyState: { [weak self] in + self?.isReady = false + } + ) + // FIXME: implement with CSS (bottom toolbar) webView.scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 47, right: 0) @@ -223,7 +233,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro webView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor) ]) - // WebView starts hidden; fades in when editor is ready (see didLoadEditor()) + // WebView starts hidden and fades in when editor navigation completes webView.alpha = 0 // Warmup mode - load HTML without dependencies for WebKit prewarming @@ -583,6 +593,21 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro evaluate("editor.appendTextAtCursor(decodeURIComponent('\(escapedText)'));") } + /// Sets focus to the editor if the content is empty. + /// + /// This method programmatically focuses the editor, placing the cursor in the content area + /// so the user can begin typing immediately. Focus is only applied when the editor is displaying + /// empty content to avoid disrupting existing content editing. + /// + /// Use the `force` parameter to apply focus even when there is content in the editor. + /// + /// - Parameter force: When true, applies focus even when there is content in the editor. + public func focus(force: Bool = false) { + if force || configuration.content.isEmpty { + evaluate("editor.focus();") + } + } + // MARK: - Navigation Overlay private func setupNavigationOverlay() { @@ -712,20 +737,23 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro self.hideActivityView() self.isReady = true - // Fade in the WebView - it was hidden (alpha = 0) since viewDidLoad() + // Fade in the WebView now that navigation is complete UIView.animate(withDuration: 0.2, delay: 0.1, options: [.allowUserInteraction]) { self.webView.alpha = 1 } + // If lockdown mode was detected, show the sheet — skip autofocus entirely + // since the editor may not function correctly with Lockdown Mode restrictions. + if !lockdownModeMonitor.isLockdownModeEnabled { + self.focus() + } + lockdownModeMonitor.presentSheetIfNeeded(onDismiss: {}) + // Log performance timing for monitoring let duration = CFAbsoluteTimeGetCurrent() - timestampInit print("gutenbergkit-measure_editor-first-render:", duration) delegate?.editorDidLoad(self) - - if configuration.content.isEmpty { - evaluate("editor.focus();") - } } // MARK: - Warmup @@ -762,9 +790,11 @@ private final class GutenbergEditorController: NSObject, WKNavigationDelegate, W weak var delegate: GutenbergEditorControllerDelegate? let configuration: EditorConfiguration private let navigationPolicy: EditorNavigationPolicy + private let lockdownModeMonitor: LockdownModeMonitor - init(configuration: EditorConfiguration) { + init(configuration: EditorConfiguration, lockdownModeMonitor: LockdownModeMonitor) { self.configuration = configuration + self.lockdownModeMonitor = lockdownModeMonitor let devServerURL = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"].flatMap(URL.init) self.navigationPolicy = EditorNavigationPolicy(devServerURL: devServerURL) super.init() @@ -795,6 +825,7 @@ private final class GutenbergEditorController: NSObject, WKNavigationDelegate, W func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { NSLog("navigation: \(String(describing: navigation))") + lockdownModeMonitor.detectLockdownMode(for: webView) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { diff --git a/ios/Sources/GutenbergKit/Sources/Services/LockdownModeMonitor.swift b/ios/Sources/GutenbergKit/Sources/Services/LockdownModeMonitor.swift new file mode 100644 index 000000000..94e67b4f9 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Services/LockdownModeMonitor.swift @@ -0,0 +1,201 @@ +import Foundation +import SwiftUI +import WebKit +import OSLog + +#if canImport(UIKit) +import UIKit + +/// Protocol for objects that can be checked for Lockdown Mode status. +/// +/// This protocol enables testability by allowing mock implementations +/// that simulate different Lockdown Mode states. +@MainActor +protocol LockdownModeDetectable: AnyObject { + /// Returns `true` if Lockdown Mode is enabled for this object. + var isLockdownModeEnabled: Bool { get } + + /// Reloads the content if supported by this object. + func reloadForLockdownMode() +} + +/// Extension to make WKWebView conform to LockdownModeDetectable. +extension WKWebView: LockdownModeDetectable { + var isLockdownModeEnabled: Bool { + configuration.defaultWebpagePreferences.isLockdownModeEnabled + } + + // WKWebView.reload() returns WKNavigation?, which doesn't satisfy the + // Void-returning protocol requirement. This wrapper discards the result. + func reloadForLockdownMode() { + _ = reload() as WKNavigation? + } +} + +/// Monitors Lockdown Mode status and presents warning UI when needed. +/// +/// This class handles detection of iOS Lockdown Mode in the WebView and manages +/// the presentation of a warning sheet to inform users about potential editor limitations. +@MainActor +class LockdownModeMonitor: ObservableObject { + + @Published + public var isLockdownModeEnabled: Bool + + /// Indicates whether the Lockdown Mode sheet has been shown to the user. + private var hasShownSheet = false + + /// Indicates whether we should show the lockdown sheet on next editor load. + private var shouldShowSheet = false + + /// Weak reference to the view controller that will present the sheet. + private weak var presentingViewController: UIViewController? + + /// Weak reference to the detectable object for reloading on foreground. + private weak var detectable: LockdownModeDetectable? + + /// Callback invoked when the editor needs to reset its ready state. + private var onResetReadyState: (() -> Void)? + + init(isLockdownModeEnabled: Bool = false) { + self.isLockdownModeEnabled = isLockdownModeEnabled + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /// Detects Lockdown Mode status in the detectable object and triggers sheet presentation if needed. + /// + /// - Parameter detectable: The object to check for Lockdown Mode status. + public func detectLockdownMode(for detectable: LockdownModeDetectable) { + Logger.navigation.debug("Detecting Lockdown Mode") + + // Store weak reference to detectable object for later use (foreground reloads) + self.detectable = detectable + + let wasEnabled = self.isLockdownModeEnabled + self.isLockdownModeEnabled = detectable.isLockdownModeEnabled + + // Handle transition from disabled to enabled: show sheet + if self.isLockdownModeEnabled && !wasEnabled && !hasShownSheet { + shouldShowSheet = true + } + + // Handle transition from enabled to disabled: clear sheet state + // This happens when user excludes app from Lockdown Mode + if !self.isLockdownModeEnabled && wasEnabled { + hasShownSheet = false + shouldShowSheet = false + } + } + + /// Sets up the monitor with required dependencies and starts observing foreground notifications. + /// + /// - Parameters: + /// - viewController: The view controller to use for sheet presentation. + /// - onResetReadyState: Callback invoked when the editor should reset its ready state. + public func setup( + presentingViewController viewController: UIViewController, + onResetReadyState: @escaping () -> Void + ) { + self.presentingViewController = viewController + self.onResetReadyState = onResetReadyState + + // Observe foreground notifications to re-check Lockdown Mode + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + @objc private func handleWillEnterForeground() { + Logger.navigation.debug("Will enter foreground") + + // Always re-check on foreground to detect Lockdown Mode state changes. + // This handles both: + // 1. User excluded app from Lockdown Mode while in Settings (enabled -> disabled) + // 2. User enabled Lockdown Mode for app while in Settings (disabled -> enabled) + + // Reset the monitor to allow re-detection and potentially show sheet again + resetForForegroundCheck() + + // Reset the editor's ready state so it goes through loading flow again + onResetReadyState?() + + // Dismiss the sheet if presented, then reload to re-run detection + dismissSheetIfPresented { [weak self] in + // Reload triggers navigation delegate which calls detectLockdownMode() + // If Lockdown Mode state changed: + // - Now enabled: sheet will be shown + // - Now disabled: editor will fade in normally without sheet + self?.detectable?.reloadForLockdownMode() + } + } + + /// Presents the Lockdown Mode warning sheet if needed. + /// + /// - Parameters: + /// - onDismiss: Callback invoked when the user dismisses the sheet. + /// - Returns: `true` if the sheet was presented, `false` otherwise. + @discardableResult + public func presentSheetIfNeeded(onDismiss: @escaping () -> Void) -> Bool { + guard shouldShowSheet, let presentingViewController else { + return false + } + + hasShownSheet = true + shouldShowSheet = false + + let sheetView = LockdownModeSheet( + onDismiss: { [weak presentingViewController] in + guard let presentingViewController else { return } + presentingViewController.dismiss(animated: true) { + onDismiss() + } + }, + onLearnMore: { + // Open support article directly to the exclusion section using text fragment + if let url = URL(string: "https://support.apple.com/en-us/105120#:~:text=How%20to%20exclude%20apps%20or%20websites%20from%20Lockdown%20Mode") { + UIApplication.shared.open(url) + } + } + ) + + let hostingController = UIHostingController(rootView: sheetView) + hostingController.modalPresentationStyle = .pageSheet + hostingController.isModalInPresentation = true + + if let sheet = hostingController.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = false + } + + presentingViewController.present(hostingController, animated: true) + return true + } + + /// Resets the monitor state to re-check Lockdown Mode status. + /// + /// Call this when the app returns from background to re-evaluate Lockdown Mode + /// and potentially show the sheet again if it's still enabled. + public func resetForForegroundCheck() { + hasShownSheet = false + } + + /// Dismisses the sheet if it's currently presented. + /// + /// - Parameter completion: Optional callback invoked after dismissal completes. + public func dismissSheetIfPresented(completion: (() -> Void)? = nil) { + guard let presentingViewController, presentingViewController.presentedViewController != nil else { + completion?() + return + } + + presentingViewController.dismiss(animated: false, completion: completion) + } +} + +#endif diff --git a/ios/Sources/GutenbergKit/Sources/Views/LockdownModeBannerView.swift b/ios/Sources/GutenbergKit/Sources/Views/LockdownModeBannerView.swift new file mode 100644 index 000000000..3a60a0cd8 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Views/LockdownModeBannerView.swift @@ -0,0 +1,82 @@ +import SwiftUI + +#if canImport(UIKit) +import UIKit + +/// A sheet that warns users about Lockdown Mode potentially affecting editor functionality. +/// +/// Lockdown Mode applies additional security restrictions to WebKit that can +/// impact the performance and functionality of the Gutenberg editor. +struct LockdownModeSheet: View { + let onDismiss: () -> Void + let onLearnMore: () -> Void + + var body: some View { + VStack { + VStack(spacing: 20) { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .font(.title2) + .foregroundColor(.orange) + + Text(EditorLocalization.localize(.lockdownModeTitle)) + .font(.headline) + + Spacer() + } + + VStack(alignment: .leading, spacing: 12) { + Text(EditorLocalization.localize(.lockdownModeWarning)) + .font(.body) + .foregroundColor(.secondary) + + Text(EditorLocalization.localize(.lockdownModeExcludeHint)) + .font(.body) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + }.padding(24) + + Spacer(minLength: 0) + + VStack(spacing: 12) { + Button { + onLearnMore() + } label: { + Text(EditorLocalization.localize(.lockdownModeLearnMore)) + .font(.body) + .fontWeight(.semibold) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(Color.accentColor, in: RoundedRectangle(cornerRadius: 10)) + } + .buttonStyle(.plain) + + Button { + onDismiss() + } label: { + Text(EditorLocalization.localize(.lockdownModeDismiss)) + .font(.body) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.plain) + } + .padding(24) + .background(Color(UIColor.systemBackground)) + }.ignoresSafeArea(edges: .bottom) + } +} + +#Preview { + NavigationStack { + VStack {}.navigationTitle("Demo") + }.sheet(isPresented: .constant(true)) { + LockdownModeSheet(onDismiss: {}, onLearnMore: {}) + .presentationDetents([.medium]) + } +} + +#endif diff --git a/ios/Tests/GutenbergKitTests/Services/LockdownModeMonitorTests.swift b/ios/Tests/GutenbergKitTests/Services/LockdownModeMonitorTests.swift new file mode 100644 index 000000000..c158a1b76 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Services/LockdownModeMonitorTests.swift @@ -0,0 +1,271 @@ +import Foundation +import Testing +import WebKit +import UIKit + +@testable import GutenbergKit + +@Suite +@MainActor +struct LockdownModeMonitorTests { + + // MARK: - Test Helpers + + private func makeMonitor() -> LockdownModeMonitor { + return LockdownModeMonitor() + } + + /// Mock implementation of LockdownModeDetectable for testing. + @MainActor + private class MockLockdownModeDetectable: LockdownModeDetectable { + var isLockdownModeEnabled: Bool + var reloadCallCount = 0 + + init(isLockdownModeEnabled: Bool) { + self.isLockdownModeEnabled = isLockdownModeEnabled + } + + func reloadForLockdownMode() { + reloadCallCount += 1 + } + } + + // MARK: - Initialization Tests + + @Test("Monitor initializes with Lockdown Mode disabled") + func monitorInitializesWithLockdownModeDisabled() { + let monitor = makeMonitor() + #expect(monitor.isLockdownModeEnabled == false) + } + + @Test("Monitor can be initialized with Lockdown Mode enabled") + func monitorCanBeInitializedWithLockdownModeEnabled() { + let monitor = LockdownModeMonitor(isLockdownModeEnabled: true) + #expect(monitor.isLockdownModeEnabled == true) + } + + // MARK: - Setup Tests + + @Test("Setup stores callback without invoking it") + func setupStoresCallbackWithoutInvokingIt() { + let monitor = makeMonitor() + let viewController = UIViewController() + var callbackInvoked = false + + monitor.setup( + presentingViewController: viewController, + onResetReadyState: { + callbackInvoked = true + } + ) + + #expect(!callbackInvoked) + } + + // MARK: - Detection Logic Tests + + @Test("detectLockdownMode updates isLockdownModeEnabled property") + func detectLockdownModeUpdatesProperty() { + let monitor = makeMonitor() + let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + + monitor.detectLockdownMode(for: webView) + + #expect(monitor.isLockdownModeEnabled == webView.configuration.defaultWebpagePreferences.isLockdownModeEnabled) + } + + @Test("State transitions from enabled to disabled clear internal flags") + func stateTransitionFromEnabledToDisabledClearsFlags() { + let monitor = LockdownModeMonitor(isLockdownModeEnabled: true) + let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + + monitor.detectLockdownMode(for: webView) + + #expect(monitor.isLockdownModeEnabled == webView.configuration.defaultWebpagePreferences.isLockdownModeEnabled) + } + + // MARK: - Sheet Presentation Tests + + @Test("presentSheetIfNeeded returns false when not needed") + func presentSheetIfNeededReturnsFalseWhenNotNeeded() { + let monitor = makeMonitor() + + let result = monitor.presentSheetIfNeeded {} + + #expect(result == false) + } + + @Test("presentSheetIfNeeded returns false without presenting view controller") + func presentSheetIfNeededReturnsFalseWithoutViewController() { + let monitor = makeMonitor() + + let result = monitor.presentSheetIfNeeded {} + + #expect(result == false) + } + + // MARK: - Foreground Handling Tests + + @Test("dismissSheetIfPresented calls completion immediately when no sheet") + func dismissSheetCallsCompletionImmediatelyWithoutSheet() { + let monitor = makeMonitor() + let viewController = UIViewController() + + monitor.setup( + presentingViewController: viewController, + onResetReadyState: {} + ) + + var completionCalled = false + monitor.dismissSheetIfPresented { + completionCalled = true + } + + #expect(completionCalled == true) + } + + // MARK: - Scenario Tests + + @Test("Scenario: User excludes app from Lockdown Mode") + func scenarioUserExcludesAppFromLockdownMode() { + let monitor = LockdownModeMonitor(isLockdownModeEnabled: true) + + let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + monitor.detectLockdownMode(for: webView) + + #expect(monitor.isLockdownModeEnabled == false) + } + + @Test("Scenario: Lockdown Mode remains enabled after detection") + func scenarioLockdownModeRemainsEnabled() { + let monitor = LockdownModeMonitor(isLockdownModeEnabled: true) + + #expect(monitor.isLockdownModeEnabled == true) + } + + // MARK: - Edge Case Tests + + @Test("Multiple detectLockdownMode calls handle gracefully") + func multipleDetectCallsHandleGracefully() { + let monitor = makeMonitor() + let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + + monitor.detectLockdownMode(for: webView) + monitor.detectLockdownMode(for: webView) + monitor.detectLockdownMode(for: webView) + + #expect(monitor.isLockdownModeEnabled == webView.configuration.defaultWebpagePreferences.isLockdownModeEnabled) + } + + @Test("Setup can be called multiple times") + func setupCanBeCalledMultipleTimes() { + let monitor = makeMonitor() + let vc1 = UIViewController() + let vc2 = UIViewController() + + monitor.setup(presentingViewController: vc1, onResetReadyState: {}) + monitor.setup(presentingViewController: vc2, onResetReadyState: {}) + + // Should handle gracefully + } + + // MARK: - Mock-Based Detection Tests + + @Test("Mock detectable with Lockdown Mode enabled updates monitor state") + func mockDetectableWithLockdownEnabledUpdatesState() { + let monitor = makeMonitor() + let mockDetectable = MockLockdownModeDetectable(isLockdownModeEnabled: true) + + monitor.detectLockdownMode(for: mockDetectable) + + #expect(monitor.isLockdownModeEnabled == true) + } + + @Test("Mock detectable with Lockdown Mode disabled updates monitor state") + func mockDetectableWithLockdownDisabledUpdatesState() { + let monitor = makeMonitor() + let mockDetectable = MockLockdownModeDetectable(isLockdownModeEnabled: false) + + monitor.detectLockdownMode(for: mockDetectable) + + #expect(monitor.isLockdownModeEnabled == false) + } + + // MARK: - State Transition Tests + // + // These tests verify the detection state machine without calling presentSheetIfNeeded, + // which would trigger UIViewController.present() and deadlock in CI (no window hierarchy). + + @Test("Disabled-to-enabled transition sets isLockdownModeEnabled") + func disabledToEnabledTransitionSetsState() { + let monitor = makeMonitor() + + let disabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: false) + monitor.detectLockdownMode(for: disabledMock) + #expect(monitor.isLockdownModeEnabled == false) + + let enabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: true) + monitor.detectLockdownMode(for: enabledMock) + #expect(monitor.isLockdownModeEnabled == true) + } + + @Test("Enabled-to-disabled transition clears isLockdownModeEnabled") + func enabledToDisabledTransitionClearsState() { + let monitor = makeMonitor() + + let enabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: true) + monitor.detectLockdownMode(for: enabledMock) + #expect(monitor.isLockdownModeEnabled == true) + + let disabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: false) + monitor.detectLockdownMode(for: disabledMock) + #expect(monitor.isLockdownModeEnabled == false) + } + + @Test("Multiple state transitions track correctly") + func multipleStateTransitionsTrackCorrectly() { + let monitor = makeMonitor() + + let enabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: true) + let disabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: false) + + // Disabled -> Enabled + monitor.detectLockdownMode(for: enabledMock) + #expect(monitor.isLockdownModeEnabled == true) + + // Enabled -> Disabled + monitor.detectLockdownMode(for: disabledMock) + #expect(monitor.isLockdownModeEnabled == false) + + // Disabled -> Enabled again + monitor.detectLockdownMode(for: enabledMock) + #expect(monitor.isLockdownModeEnabled == true) + } + + @Test("presentSheetIfNeeded returns false without setup even after detection") + func presentSheetReturnsFalseWithoutSetup() { + let monitor = makeMonitor() + + // Trigger a disabled-to-enabled transition (sets shouldShowSheet) + let enabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: true) + monitor.detectLockdownMode(for: enabledMock) + + // Without setup(), presentingViewController is nil so this returns false + let didPresent = monitor.presentSheetIfNeeded {} + #expect(didPresent == false) + } + + @Test("resetForForegroundCheck allows re-detection") + func resetForForegroundCheckAllowsRedetection() { + let monitor = makeMonitor() + + let enabledMock = MockLockdownModeDetectable(isLockdownModeEnabled: true) + monitor.detectLockdownMode(for: enabledMock) + #expect(monitor.isLockdownModeEnabled == true) + + monitor.resetForForegroundCheck() + + // State should still reflect the last detection + #expect(monitor.isLockdownModeEnabled == true) + } +}