diff --git a/SharedPackages/BrowserServicesKit/Sources/PrivacyConfig/Features/PrivacyFeature.swift b/SharedPackages/BrowserServicesKit/Sources/PrivacyConfig/Features/PrivacyFeature.swift index 886a89beba9..579d9ef30a8 100644 --- a/SharedPackages/BrowserServicesKit/Sources/PrivacyConfig/Features/PrivacyFeature.swift +++ b/SharedPackages/BrowserServicesKit/Sources/PrivacyConfig/Features/PrivacyFeature.swift @@ -86,7 +86,6 @@ public enum PrivacyFeature: String { case duckAiChatHistory case serp case popupBlocking - case combinedPermissionView case pageContext case webExtensions case forceDarkModeOnWebsites diff --git a/macOS/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/macOS/DuckDuckGo-macOS.xcodeproj/project.pbxproj index 34d9a026eb6..22eeb836e7d 100644 --- a/macOS/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/macOS/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -862,7 +862,7 @@ 3706FCD6293F65D500E42796 /* httpsMobileV2FalsePositives.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B67742A255DBEB800025BD8 /* httpsMobileV2FalsePositives.json */; }; 3706FCD8293F65D500E42796 /* BookmarksBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */; }; 3706FCDB293F65D500E42796 /* Feedback.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3863C427A1E28F00749AB5 /* Feedback.storyboard */; }; - 3706FCE1293F65D500E42796 /* PermissionAuthorization.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */; }; + 3706FCE6293F65D500E42796 /* social_images in Resources */ = {isa = PBXBuildFile; fileRef = EA18D1C9272F0DC8006DC101 /* social_images */; }; 3706FCEA293F65D500E42796 /* PasswordManager.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85625993269C8F9600EE44BC /* PasswordManager.storyboard */; }; 3706FCED293F65D500E42796 /* httpsMobileV2Bloom.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B677428255DBEB800025BD8 /* httpsMobileV2Bloom.bin */; }; @@ -2328,7 +2328,7 @@ 8400DC4F2C6E2770006509D2 /* SteppedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8400DC4D2C6E2770006509D2 /* SteppedScrollView.swift */; }; 8402A7BD2E9F8C5B00ACA20C /* FireCoordinatorIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8402A7BA2E9F7CA000ACA20C /* FireCoordinatorIntegrationTests.swift */; }; 8402A7BE2E9F8C5B00ACA20C /* FireCoordinatorIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8402A7BA2E9F7CA000ACA20C /* FireCoordinatorIntegrationTests.swift */; }; - 8409BD742ED878B200E39FA7 /* PopupHandlingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409BD732ED878B200E39FA7 /* PopupHandlingUITests.swift */; }; + 8409BD782ED878C400E39FA7 /* popup-delayed.html in Resources */ = {isa = PBXBuildFile; fileRef = 8409BD752ED878C400E39FA7 /* popup-delayed.html */; }; 8409BD792ED878C400E39FA7 /* popup-servicenow.html in Resources */ = {isa = PBXBuildFile; fileRef = 8409BD772ED878C400E39FA7 /* popup-servicenow.html */; }; 8409BD7A2ED878C400E39FA7 /* popup-links.html in Resources */ = {isa = PBXBuildFile; fileRef = 8409BD762ED878C400E39FA7 /* popup-links.html */; }; @@ -3159,7 +3159,7 @@ B645D8F629FA95440024461F /* WKProcessPoolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */; }; B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */; }; B647EFBB2922584B00BA628D /* AdClickAttributionTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B647EFBA2922584B00BA628D /* AdClickAttributionTabExtension.swift */; }; - B64C84DE2692D7400048FEBE /* PermissionAuthorization.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */; }; + B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C84E22692DC9F0048FEBE /* PermissionAuthorizationViewController.swift */; }; B64C84EB2692DD650048FEBE /* PermissionAuthorizationPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C84EA2692DD650048FEBE /* PermissionAuthorizationPopover.swift */; }; B64C84F1269310120048FEBE /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C84F0269310120048FEBE /* PermissionManager.swift */; }; @@ -4183,7 +4183,7 @@ EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; EE3ED86E2E561B770026B962 /* DismissableSyncDeviceButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3ED86D2E561B770026B962 /* DismissableSyncDeviceButtonModel.swift */; }; EE3ED86F2E561B770026B962 /* DismissableSyncDeviceButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3ED86D2E561B770026B962 /* DismissableSyncDeviceButtonModel.swift */; }; - EE42CBCC2BC8004700AD411C /* PermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */; }; + EE4624E42E69DB53001CD9E6 /* FeatureFlagger+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4624E32E69DB49001CD9E6 /* FeatureFlagger+Sync.swift */; }; EE4624E52E69DB53001CD9E6 /* FeatureFlagger+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4624E32E69DB49001CD9E6 /* FeatureFlagger+Sync.swift */; }; EE4E15FC2EBCB98A006D1C2D /* NewReportFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4E15FB2EBCB983006D1C2D /* NewReportFeedbackView.swift */; }; @@ -5626,7 +5626,7 @@ 8400DC4A2C6E26AE006509D2 /* BookmarksBarCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarCollectionView.swift; sourceTree = ""; }; 8400DC4D2C6E2770006509D2 /* SteppedScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SteppedScrollView.swift; sourceTree = ""; }; 8402A7BA2E9F7CA000ACA20C /* FireCoordinatorIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireCoordinatorIntegrationTests.swift; sourceTree = ""; }; - 8409BD732ED878B200E39FA7 /* PopupHandlingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupHandlingUITests.swift; sourceTree = ""; }; + 8409BD752ED878C400E39FA7 /* popup-delayed.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "popup-delayed.html"; sourceTree = ""; }; 8409BD762ED878C400E39FA7 /* popup-links.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "popup-links.html"; sourceTree = ""; }; 8409BD772ED878C400E39FA7 /* popup-servicenow.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "popup-servicenow.html"; sourceTree = ""; }; @@ -6152,7 +6152,7 @@ B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNonexistentDomainTests.swift; sourceTree = ""; }; B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKProcessPoolExtension.swift; sourceTree = ""; }; B647EFBA2922584B00BA628D /* AdClickAttributionTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdClickAttributionTabExtension.swift; sourceTree = ""; }; - B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PermissionAuthorization.storyboard; sourceTree = ""; }; + B64C84E22692DC9F0048FEBE /* PermissionAuthorizationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAuthorizationViewController.swift; sourceTree = ""; }; B64C84EA2692DD650048FEBE /* PermissionAuthorizationPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAuthorizationPopover.swift; sourceTree = ""; }; B64C84F0269310120048FEBE /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; @@ -6715,7 +6715,7 @@ EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; EE3ED86D2E561B770026B962 /* DismissableSyncDeviceButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableSyncDeviceButtonModel.swift; sourceTree = ""; }; - EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionsTests.swift; sourceTree = ""; }; + EE4624E32E69DB49001CD9E6 /* FeatureFlagger+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeatureFlagger+Sync.swift"; sourceTree = ""; }; EE4E15FB2EBCB983006D1C2D /* NewReportFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewReportFeedbackView.swift; sourceTree = ""; }; EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksAndFavoritesTests.swift; sourceTree = ""; }; @@ -9452,10 +9452,8 @@ 844905502E55C9C80054F4FD /* MaliciousSiteProtectionUITests.swift */, 8449054A2E55BBD60054F4FD /* NavigationProtectionUITests.swift */, 56A054522C2592CE007D8FAB /* OnboardingUITests.swift */, - EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */, 1DE28D602EEB4A7E00C07F15 /* NewPermissionViewTests.swift */, BB4339DA2C7F9606005D7ED7 /* PinnedTabsTests.swift */, - 8409BD732ED878B200E39FA7 /* PopupHandlingUITests.swift */, 842FE38A2E0C28EF00E4F9E4 /* PrintingTests.swift */, 8449054E2E55BBEE0054F4FD /* PrivacyDashboardUITests.swift */, 5680BCA62F17EC6400BD624E /* SSLErrorTests.swift */, @@ -11745,7 +11743,6 @@ B64C84DC2692D6FC0048FEBE /* View */ = { isa = PBXGroup; children = ( - B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */, B64C84E22692DC9F0048FEBE /* PermissionAuthorizationViewController.swift */, 1D532BB32ED8D3D300D219FA /* PermissionAuthorizationSwiftUIView.swift */, B64C84EA2692DD650048FEBE /* PermissionAuthorizationPopover.swift */, @@ -13913,7 +13910,6 @@ 3706FCD8293F65D500E42796 /* BookmarksBar.storyboard in Resources */, 3706FCDB293F65D500E42796 /* Feedback.storyboard in Resources */, B658BAB72B0F848D00D1F2C7 /* Localizable.xcstrings in Resources */, - 3706FCE1293F65D500E42796 /* PermissionAuthorization.storyboard in Resources */, CD2AB5C62C8222FE0019EB49 /* phishingFilterSet.json in Resources */, 7B122A992DE4717400B84F63 /* AppStoreInfoPlist.xcstrings in Resources */, 3706FCE6293F65D500E42796 /* social_images in Resources */, @@ -14146,7 +14142,6 @@ 4BD18F05283F151F00058124 /* BookmarksBar.storyboard in Resources */, AA3863C527A1E28F00749AB5 /* Feedback.storyboard in Resources */, B658BAB62B0F845D00D1F2C7 /* Localizable.xcstrings in Resources */, - B64C84DE2692D7400048FEBE /* PermissionAuthorization.storyboard in Resources */, CD2AB5C52C8222FE0019EB49 /* phishingFilterSet.json in Resources */, EA18D1CA272F0DC8006DC101 /* social_images in Resources */, BD384AC92BBC821A00EF3735 /* vpn-dark-mode.json in Resources */, @@ -16659,14 +16654,12 @@ 8449054F2E55BBEE0054F4FD /* PrivacyDashboardUITests.swift in Sources */, 844905412E54A3540054F4FD /* AddressBarUITests.swift in Sources */, 844905452E54ADD40054F4FD /* AutoconsentUITests.swift in Sources */, - EE42CBCC2BC8004700AD411C /* PermissionsTests.swift in Sources */, 4B7948192F81DFC400DA4028 /* TabNavigationMenuItemTests.swift in Sources */, 1DE28D612EEB4A7E00C07F15 /* NewPermissionViewTests.swift in Sources */, 844905432E54A3600054F4FD /* AddressBarSpoofingUITests.swift in Sources */, 844905492E55A9740054F4FD /* SearchNonexistentDomainUITests.swift in Sources */, 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */, BB0346F52CEB80B400D23E05 /* DownloadsUITests.swift in Sources */, - 8409BD742ED878B200E39FA7 /* PopupHandlingUITests.swift in Sources */, 84A03D2E2DDC275200A3685B /* BookmarksBarVisibilityTests.swift in Sources */, EEBCE6822BA444FA00B9DF00 /* XCUIElementExtension.swift in Sources */, 31DE49212EE2299500F3940A /* AIChatMultilinePasteTests.swift in Sources */, diff --git a/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests CI.xcscheme b/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests CI.xcscheme index e0e6e019a2d..e41393a501e 100644 --- a/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests CI.xcscheme +++ b/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests CI.xcscheme @@ -54,11 +54,6 @@ BlueprintName = "UI Tests" ReferencedContainer = "container:DuckDuckGo-macOS.xcodeproj"> - - - - diff --git a/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests.xcscheme b/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests.xcscheme index 93f69b06097..17a53ed2430 100644 --- a/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests.xcscheme +++ b/macOS/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS UI Tests.xcscheme @@ -70,11 +70,6 @@ BlueprintName = "UI Tests" ReferencedContainer = "container:DuckDuckGo-macOS.xcodeproj"> - - - - diff --git a/macOS/DuckDuckGo/AIChat/Sidebar/AIChatViewController.swift b/macOS/DuckDuckGo/AIChat/Sidebar/AIChatViewController.swift index 23874851c16..260cb9c19b1 100644 --- a/macOS/DuckDuckGo/AIChat/Sidebar/AIChatViewController.swift +++ b/macOS/DuckDuckGo/AIChat/Sidebar/AIChatViewController.swift @@ -493,7 +493,7 @@ final class AIChatViewController: NSViewController { private func showPermissionAuthorizationPopover(for query: PermissionAuthorizationQuery) { let popover = permissionAuthorizationPopover ?? { - let popover = PermissionAuthorizationPopover(featureFlagger: NSApp.delegateTyped.featureFlagger) + let popover = PermissionAuthorizationPopover() self.permissionAuthorizationPopover = popover return popover }() diff --git a/macOS/DuckDuckGo/Application/AppDelegate.swift b/macOS/DuckDuckGo/Application/AppDelegate.swift index e5456f6ce8e..d98ab3cdc8e 100644 --- a/macOS/DuckDuckGo/Application/AppDelegate.swift +++ b/macOS/DuckDuckGo/Application/AppDelegate.swift @@ -819,16 +819,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if AppVersion.runType.requiresEnvironment { fireproofDomains = FireproofDomains(store: FireproofDomainsStore(database: database.db, tableName: "FireproofDomains"), tld: tld) faviconManager = FaviconManager(cacheType: .standard(database.db), bookmarkManager: bookmarkManager, fireproofDomains: fireproofDomains, privacyConfigurationManager: privacyConfigurationManager) - permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db), featureFlagger: featureFlagger) + permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db)) } else { fireproofDomains = FireproofDomains(store: FireproofDomainsStore(context: nil), tld: tld) faviconManager = FaviconManager(cacheType: .inMemory, bookmarkManager: bookmarkManager, fireproofDomains: fireproofDomains, privacyConfigurationManager: privacyConfigurationManager) - permissionManager = PermissionManager(store: LocalPermissionStore(database: nil), featureFlagger: featureFlagger) + permissionManager = PermissionManager(store: LocalPermissionStore(database: nil)) } #else fireproofDomains = FireproofDomains(store: FireproofDomainsStore(database: database.db, tableName: "FireproofDomains"), tld: tld) faviconManager = FaviconManager(cacheType: .standard(database.db), bookmarkManager: bookmarkManager, fireproofDomains: fireproofDomains, privacyConfigurationManager: privacyConfigurationManager) - permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db), featureFlagger: featureFlagger) + permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db)) #endif notificationService = UserNotificationAuthorizationService() diff --git a/macOS/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/macOS/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 667826d7dde..6da959da26a 100644 --- a/macOS/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/macOS/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -84,7 +84,7 @@ final class AddressBarButtonsViewController: NSViewController { private var permissionAuthorizationPopover: PermissionAuthorizationPopover? private func permissionAuthorizationPopoverCreatingIfNeeded() -> PermissionAuthorizationPopover { return permissionAuthorizationPopover ?? { - let popover = PermissionAuthorizationPopover(featureFlagger: featureFlagger) + let popover = PermissionAuthorizationPopover() NotificationCenter.default.addObserver(self, selector: #selector(popoverDidClose), name: NSPopover.didCloseNotification, object: popover) NotificationCenter.default.addObserver(self, selector: #selector(popoverWillShow), name: NSPopover.willShowNotification, object: popover) self.permissionAuthorizationPopover = popover @@ -100,7 +100,7 @@ final class AddressBarButtonsViewController: NSViewController { private func popupBlockedPopoverCreatingIfNeeded() -> PopupBlockedPopover { return popupBlockedPopover ?? { - let popover = PopupBlockedPopover(featureFlagger: featureFlagger) + let popover = PopupBlockedPopover() popover.delegate = self self.popupBlockedPopover = popover return popover @@ -153,56 +153,7 @@ final class AddressBarButtonsViewController: NSViewController { @IBOutlet weak var privacyShieldButtonHeightConstraint: NSLayoutConstraint! @IBOutlet weak var imageButtonLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var zoomButtonHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var geolocationButtonHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var microphoneButtonHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var cameraButtonHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var popupsButtonHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var externalSchemeButtonHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var permissionButtonHeightConstraint: NSLayoutConstraint! @IBOutlet private weak var permissionButtons: NSView! - @IBOutlet weak var cameraButton: PermissionButton! { - didSet { - cameraButton.isHidden = true - cameraButton.target = self - cameraButton.action = #selector(cameraButtonAction(_:)) - } - } - @IBOutlet weak var microphoneButton: PermissionButton! { - didSet { - microphoneButton.isHidden = true - microphoneButton.target = self - microphoneButton.action = #selector(microphoneButtonAction(_:)) - } - } - @IBOutlet weak var geolocationButton: PermissionButton! { - didSet { - geolocationButton.isHidden = true - geolocationButton.target = self - geolocationButton.action = #selector(geolocationButtonAction(_:)) - } - } - @IBOutlet weak var popupsButton: PermissionButton! { - didSet { - popupsButton.isHidden = true - popupsButton.target = self - popupsButton.action = #selector(popupsButtonAction(_:)) - } - } - @IBOutlet weak var externalSchemeButton: PermissionButton! { - didSet { - externalSchemeButton.isHidden = true - externalSchemeButton.target = self - externalSchemeButton.action = #selector(externalSchemeButtonAction(_:)) - } - } - @IBOutlet weak var notificationButton: PermissionButton? { - didSet { - notificationButton?.isHidden = true - notificationButton?.target = self - notificationButton?.action = #selector(notificationButtonAction(_:)) - } - } - @IBOutlet weak var notificationButtonHeightConstraint: NSLayoutConstraint? /// Width of the left buttons container (Privacy Dashboard button, Permissions buttons…) /// Used to adjust the Passive Address Bar leading constraint @@ -388,11 +339,6 @@ final class AddressBarButtonsViewController: NSViewController { private func setupButtons() { if isInPopUpWindow { privacyDashboardButton.position = .free - cameraButton.position = .free - geolocationButton.position = .free - popupsButton.position = .free - microphoneButton.position = .free - externalSchemeButton.position = .free bookmarkButton.isHidden = true trailingButtonsContainer.isHidden = true trailingButtonsBackground.isHidden = true @@ -406,22 +352,6 @@ final class AddressBarButtonsViewController: NSViewController { (imageButton.cell as? NSButtonCell)?.highlightsBy = NSCell.StyleMask(rawValue: 0) imageButton.setAccessibilityIdentifier("AddressBarButtonsViewController.imageButton") - cameraButton.sendAction(on: .leftMouseDown) - cameraButton.setAccessibilityIdentifier("AddressBarButtonsViewController.cameraButton") - cameraButton.setAccessibilityTitle(UserText.permissionCamera) - microphoneButton.sendAction(on: .leftMouseDown) - microphoneButton.setAccessibilityIdentifier("AddressBarButtonsViewController.microphoneButton") - microphoneButton.setAccessibilityTitle(UserText.permissionMicrophone) - geolocationButton.sendAction(on: .leftMouseDown) - geolocationButton.setAccessibilityIdentifier("AddressBarButtonsViewController.geolocationButton") - geolocationButton.setAccessibilityTitle(UserText.permissionGeolocation) - popupsButton.sendAction(on: .leftMouseDown) - popupsButton.setAccessibilityTitle(UserText.permissionPopups) - popupsButton.setAccessibilityIdentifier("AddressBarButtonsViewController.popupsButton") - externalSchemeButton.sendAction(on: .leftMouseDown) - // externalSchemeButton.accessibilityTitle is set in `updatePermissionButtons` - externalSchemeButton.setAccessibilityIdentifier("AddressBarButtonsViewController.externalSchemeButton") - privacyDashboardButton.setAccessibilityRole(.button) privacyDashboardButton.setAccessibilityElement(true) privacyDashboardButton.setAccessibilityIdentifier("AddressBarButtonsViewController.privacyDashboardButton") @@ -710,7 +640,6 @@ final class AddressBarButtonsViewController: NSViewController { // update Separator on Privacy Entry Point and other buttons appearance change private func subscribeToButtonsVisibility() { privacyDashboardButton.publisher(for: \.isHidden).asVoid() - .merge(with: permissionButtons.publisher(for: \.frame).asVoid()) .merge(with: zoomButton.publisher(for: \.isHidden).asVoid()) .receive(on: DispatchQueue.main) .sink { [weak self] in @@ -750,69 +679,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func updateLegacyPermissionButtons() { - // Prevent crash if Combine subscriptions outlive view lifecycle - guard isViewLoaded else { return } - guard let tabViewModel else { return } - - guard !featureFlagger.isFeatureOn(.newPermissionView) else { - permissionButtons.isShown = false - return - } - - // Show permission buttons when there's a requested permission on NTP even if address bar is focused, - // since NTP has the address bar focused by default - let hasRequestedPermission = tabViewModel.usedPermissions.values.contains(where: { $0.isRequested }) - let shouldShowWhileFocused = (tabViewModel.tab.content == .newtab) && hasRequestedPermission - - permissionButtons.isShown = (shouldShowWhileFocused || !isTextFieldEditorFirstResponder) - && !tabViewModel.isShowingErrorPage - defer { - showOrHidePermissionPopoverIfNeeded() - } - - geolocationButton.buttonState = tabViewModel.usedPermissions.geolocation - - let (camera, microphone) = PermissionState?.combineCamera(tabViewModel.usedPermissions.camera, - withMicrophone: tabViewModel.usedPermissions.microphone) - cameraButton.buttonState = camera - microphoneButton.buttonState = microphone - - // Show pop-up button when there's a blocked pop-up (permission is requested) - if tabViewModel.usedPermissions.popups?.isRequested == true { - popupsButton.buttonState = tabViewModel.usedPermissions.popups - } else if featureFlagger.isFeatureOn(.popupBlocking) { - let pageInitiatedPopupOpened = tabViewModel.tab.popupHandling?.pageInitiatedPopupOpened ?? false - // Keep button visible (as .inactive) when a page-initiated pop-up was allowed or opened by the current page (always allowed) - popupsButton.buttonState = pageInitiatedPopupOpened ? .inactive : tabViewModel.usedPermissions.popups // .inactive or nil - } else { - popupsButton.buttonState = nil - } - externalSchemeButton.buttonState = tabViewModel.usedPermissions.externalScheme - let title = String(format: UserText.permissionExternalSchemeOpenFormat, tabViewModel.usedPermissions.first(where: { $0.key.isExternalScheme })?.key.localizedDescription ?? "") - externalSchemeButton.setAccessibilityTitle(title) - - notificationButton?.buttonState = tabViewModel.usedPermissions.notification - notificationButton?.setAccessibilityTitle(UserText.permissionNotification) - } - - private func showOrHidePermissionPopoverIfNeeded() { - guard let tabViewModel else { return } - - for permission in tabViewModel.usedPermissions.keys { - guard case .requested(let query) = tabViewModel.usedPermissions[permission] else { continue } - let permissionAuthorizationPopover = permissionAuthorizationPopoverCreatingIfNeeded() - guard !permissionAuthorizationPopover.isShown else { - if permissionAuthorizationPopover.viewController.query === query { return } - permissionAuthorizationPopover.close() - return - } - openPermissionAuthorizationPopover(for: query) - return - } - if let permissionAuthorizationPopover, permissionAuthorizationPopover.isShown { - permissionAuthorizationPopover.close() - } - + permissionButtons.isShown = false } private func updateAllPermissionButtons() { @@ -830,11 +697,6 @@ final class AddressBarButtonsViewController: NSViewController { guard isViewLoaded else { return } guard let tabViewModel else { return } - guard featureFlagger.isFeatureOn(.newPermissionView) else { - permissionCenterButton.isShown = false - return - } - // Only update icon if no authorization popover is currently shown // (icon updates during active popover are handled by openPermissionAuthorizationPopover) let isAuthorizationPopoverShown = permissionAuthorizationPopover?.isShown == true || popupBlockedPopover?.isShown == true @@ -861,7 +723,6 @@ final class AddressBarButtonsViewController: NSViewController { } private func updatePermissionCenterButtonIcon(forRequestedPermission permissionType: PermissionType? = nil) { - guard featureFlagger.isFeatureOn(.newPermissionView) else { return } permissionCenterButton.image = permissionType?.icon ?? DesignSystemImages.Glyphs.Size16.permissions } @@ -1093,9 +954,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func updateSeparator() { - separator.isShown = privacyDashboardButton.isVisible && ( - (permissionButtons.subviews.contains(where: { $0.isVisible })) || zoomButton.isVisible - ) + separator.isShown = privacyDashboardButton.isVisible && zoomButton.isVisible } // MARK: - AI Chat Action Helpers @@ -1198,7 +1057,6 @@ final class AddressBarButtonsViewController: NSViewController { askAIChatButton.setCornerRadius(cornerRadius) bookmarkButton.setCornerRadius(cornerRadius) cancelButton.setCornerRadius(cornerRadius) - permissionButtons.setCornerRadius(cornerRadius) zoomButton.setCornerRadius(cornerRadius) privacyDashboardButton.setCornerRadius(cornerRadius) permissionCenterButton.setCornerRadius(cornerRadius) @@ -1218,23 +1076,10 @@ final class AddressBarButtonsViewController: NSViewController { privacyShieldButtonWidthConstraint.constant = addressBarButtonSize privacyShieldButtonHeightConstraint.constant = addressBarButtonSize zoomButtonHeightConstraint.constant = addressBarButtonSize - geolocationButtonHeightConstraint.constant = addressBarButtonSize - microphoneButtonHeightConstraint.constant = addressBarButtonSize - cameraButtonHeightConstraint.constant = addressBarButtonSize - popupsButtonHeightConstraint.constant = addressBarButtonSize - externalSchemeButtonHeightConstraint.constant = addressBarButtonSize - permissionButtonHeightConstraint.constant = addressBarButtonSize permissionCenterButtonWidthConstraint.constant = addressBarButtonSize } private func setupButtonIcons() { - let addressBarButtonsIconsProvider = theme.iconsProvider.addressBarButtonsIconsProvider - - geolocationButton.activeImage = addressBarButtonsIconsProvider.locationSolid - geolocationButton.disabledImage = addressBarButtonsIconsProvider.locationIcon - geolocationButton.defaultImage = addressBarButtonsIconsProvider.locationIcon - externalSchemeButton.defaultImage = addressBarButtonsIconsProvider.externalSchemeIcon - popupsButton.defaultImage = addressBarButtonsIconsProvider.popupsIcon updatePermissionCenterButtonIcon() } @@ -1714,7 +1559,7 @@ final class AddressBarButtonsViewController: NSViewController { } func openPermissionAuthorizationPopover(for query: PermissionAuthorizationQuery) { - let button: AddressBarButton + guard let button = permissionCenterButton else { return } lazy var popover: NSPopover = { let popover = self.permissionAuthorizationPopoverCreatingIfNeeded() @@ -1722,43 +1567,15 @@ final class AddressBarButtonsViewController: NSViewController { return popover }() - if featureFlagger.isFeatureOn(.newPermissionView) { - button = permissionCenterButton - // Update button icon to match the permission being requested - updatePermissionCenterButtonIcon(forRequestedPermission: query.permissions.first) - if query.permissions.first?.isPopups == true { - guard !query.wasShownOnce else { return } - popover = popupBlockedPopoverCreatingIfNeeded() - } - if query.permissions.first?.isExternalScheme == true { - query.shouldShowAlwaysAllowCheckbox = true - query.shouldShowCancelInsteadOfDeny = true - } - } else { - if query.permissions.contains(.camera) - || (query.permissions.contains(.microphone) && microphoneButton.isHidden && cameraButton.isShown) { - button = cameraButton - } else { - assert(query.permissions.count == 1) - switch query.permissions.first { - case .microphone: - button = microphoneButton - case .geolocation: - button = geolocationButton - case .popups: - guard !query.wasShownOnce else { return } - button = popupsButton - popover = popupBlockedPopoverCreatingIfNeeded() - case .externalScheme: - button = externalSchemeButton - query.shouldShowAlwaysAllowCheckbox = true - query.shouldShowCancelInsteadOfDeny = true - default: - assertionFailure("Unexpected permissions") - query.handleDecision(grant: false) - return - } - } + // Update button icon to match the permission being requested + updatePermissionCenterButtonIcon(forRequestedPermission: query.permissions.first) + if query.permissions.first?.isPopups == true { + guard !query.wasShownOnce else { return } + popover = popupBlockedPopoverCreatingIfNeeded() + } + if query.permissions.first?.isExternalScheme == true { + query.shouldShowAlwaysAllowCheckbox = true + query.shouldShowCancelInsteadOfDeny = true } guard button.isVisible else { return } @@ -1899,7 +1716,6 @@ final class AddressBarButtonsViewController: NSViewController { } @IBAction func permissionCenterButtonAction(_ sender: Any) { - guard featureFlagger.isFeatureOn(.newPermissionView) else { return } guard let tabViewModel else { return } // Don't open permission center while authorization or popup blocked dialog is presented @@ -1981,145 +1797,6 @@ final class AddressBarButtonsViewController: NSViewController { popover.show(positionedBelow: permissionCenterButton.bounds.insetFromLineOfDeath(flipped: permissionCenterButton.isFlipped), in: permissionCenterButton) } - @IBAction func cameraButtonAction(_ sender: NSButton) { - guard let tabViewModel else { - assertionFailure("No selectedTabViewModel") - return - } - if case .requested(let query) = tabViewModel.usedPermissions.camera { - openPermissionAuthorizationPopover(for: query) - return - } - - var permissions = Permissions() - permissions.camera = tabViewModel.usedPermissions.camera - if microphoneButton.isHidden { - permissions.microphone = tabViewModel.usedPermissions.microphone - } - - let url = tabViewModel.tab.content.urlForWebView ?? .empty - let domain = url.isFileURL ? .localhost : (url.host ?? "") - - PermissionContextMenu(permissionManager: permissionManager, permissions: permissions.map { ($0, $1) }, domain: domain, delegate: self, featureFlagger: featureFlagger) - .popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height), in: sender) - } - - @IBAction func microphoneButtonAction(_ sender: NSButton) { - guard let tabViewModel, - let state = tabViewModel.usedPermissions.microphone - else { - Logger.general.error("Selected tab view model is nil or no microphone state") - return - } - if case .requested(let query) = state { - openPermissionAuthorizationPopover(for: query) - return - } - - let url = tabViewModel.tab.content.urlForWebView ?? .empty - let domain = url.isFileURL ? .localhost : (url.host ?? "") - - PermissionContextMenu(permissionManager: permissionManager, permissions: [(.microphone, state)], domain: domain, delegate: self, featureFlagger: featureFlagger) - .popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height), in: sender) - } - - @IBAction func geolocationButtonAction(_ sender: NSButton) { - guard let tabViewModel, - let state = tabViewModel.usedPermissions.geolocation - else { - Logger.general.error("Selected tab view model is nil or no geolocation state") - return - } - if case .requested(let query) = state { - openPermissionAuthorizationPopover(for: query) - return - } - - let url = tabViewModel.tab.content.urlForWebView ?? .empty - let domain = url.isFileURL ? .localhost : (url.host ?? "") - - PermissionContextMenu(permissionManager: permissionManager, permissions: [(.geolocation, state)], domain: domain, delegate: self, featureFlagger: featureFlagger) - .popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height), in: sender) - } - - @IBAction func popupsButtonAction(_ sender: NSButton) { - guard let tabViewModel else { - Logger.general.error("Selected tab view model is nil or has no pop-up state") - return - } - guard let state = tabViewModel.usedPermissions.popups ?? { - // If popup blocking is enabled and a page-initiated popup was opened for the current page, - // return .inactive state for the pop-up button - if featureFlagger.isFeatureOn(.popupBlocking), - tabViewModel.tab.popupHandling?.pageInitiatedPopupOpened ?? false { return .inactive } else { return nil } - }() else { - return - } - - let permissions: [(PermissionType, PermissionState)] - let domain: String - if case .requested(let query) = state { - domain = query.domain - permissions = tabViewModel.tab.permissions.authorizationQueries.reduce(into: .init()) { - guard $1.permissions.contains(.popups) else { return } - $0.append( (.popups, .requested($1)) ) - } - } else { - let url = tabViewModel.tab.content.urlForWebView ?? .empty - domain = url.isFileURL ? .localhost : (url.host ?? "") - permissions = [(.popups, state)] - } - PermissionContextMenu(permissionManager: permissionManager, - permissions: permissions, - domain: domain, - delegate: self, - featureFlagger: featureFlagger, - hasTemporaryPopupAllowance: tabViewModel.tab.popupHandling?.popupsTemporarilyAllowedForCurrentPage ?? false) - .popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height), in: sender) - } - - @IBAction func externalSchemeButtonAction(_ sender: NSButton) { - guard let tabViewModel, - let (permissionType, state) = tabViewModel.usedPermissions.first(where: { $0.key.isExternalScheme }) - else { - Logger.general.error("Selected tab view model is nil or no externalScheme state") - return - } - - let permissions: [(PermissionType, PermissionState)] - if case .requested(let query) = state { - query.wasShownOnce = false - openPermissionAuthorizationPopover(for: query) - return - } - - permissions = [(permissionType, state)] - let url = tabViewModel.tab.content.urlForWebView ?? .empty - let domain = url.isFileURL ? .localhost : (url.host ?? "") - - PermissionContextMenu(permissionManager: permissionManager, permissions: permissions, domain: domain, delegate: self, featureFlagger: featureFlagger) - .popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height), in: sender) - } - - @IBAction func notificationButtonAction(_ sender: NSButton) { - guard let tabViewModel, - let state = tabViewModel.usedPermissions.notification - else { - Logger.general.error("Selected tab view model is nil or no notification state") - return - } - if case .requested(let query) = state { - openPermissionAuthorizationPopover(for: query) - return - } - - let url = tabViewModel.tab.content.urlForWebView ?? .empty - let domain = url.isFileURL ? .localhost : (url.host ?? "") - - PermissionContextMenu(permissionManager: permissionManager, permissions: [(.notification, state)], domain: domain, delegate: self, featureFlagger: featureFlagger) - .popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height), in: sender) - } - // MARK: - Notification Animation private var animationViewCache = [String: LottieAnimationView]() @@ -2584,9 +2261,6 @@ extension AddressBarButtonsViewController: ThemeUpdateListening { let colorsProvider = theme.colorsProvider bookmarkButton.normalTintColor = colorsProvider.iconsColor - geolocationButton.normalTintColor = colorsProvider.iconsColor - cameraButton.normalTintColor = colorsProvider.iconsColor - microphoneButton.normalTintColor = colorsProvider.iconsColor } } diff --git a/macOS/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/macOS/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index c1aea564a55..54cf3904096 100644 --- a/macOS/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/macOS/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -1196,28 +1196,17 @@ - - - - - - - - - - - diff --git a/macOS/DuckDuckGo/Permissions/Model/PermissionManager.swift b/macOS/DuckDuckGo/Permissions/Model/PermissionManager.swift index cdfa52d363e..59c1670927e 100644 --- a/macOS/DuckDuckGo/Permissions/Model/PermissionManager.swift +++ b/macOS/DuckDuckGo/Permissions/Model/PermissionManager.swift @@ -19,9 +19,7 @@ import Foundation import Combine import Common -import FeatureFlags import os.log -import PrivacyConfig protocol PermissionManagerProtocol: AnyObject { @@ -46,15 +44,13 @@ protocol PermissionManagerProtocol: AnyObject { final class PermissionManager: PermissionManagerProtocol { private let store: PermissionStore - private let featureFlagger: FeatureFlagger private var permissions = [String: [PermissionType: StoredPermission]]() private let permissionSubject = PassthroughSubject() var permissionPublisher: AnyPublisher { permissionSubject.eraseToAnyPublisher() } - init(store: PermissionStore, featureFlagger: FeatureFlagger) { + init(store: PermissionStore) { self.store = store - self.featureFlagger = featureFlagger loadPermissions() } @@ -100,15 +96,10 @@ final class PermissionManager: PermissionManagerProtocol { let domain = domain.droppingWwwPrefix() // Check if permission is already stored with the same decision - // Note: permission(forDomain:...) returns .ask by default, so we also check hasPermissionPersisted - // when newPermissionView is enabled (to allow storing .ask explicitly) + // Also check hasPermissionPersisted to allow storing .ask explicitly for permission center visibility let currentDecision = self.permission(forDomain: domain, permissionType: permissionType) - if featureFlagger.isFeatureOn(.newPermissionView) { - let isAlreadyPersisted = hasPermissionPersisted(forDomain: domain, permissionType: permissionType) - guard currentDecision != decision || !isAlreadyPersisted else { return } - } else { - guard currentDecision != decision else { return } - } + let isAlreadyPersisted = hasPermissionPersisted(forDomain: domain, permissionType: permissionType) + guard currentDecision != decision || !isAlreadyPersisted else { return } defer { self.permissionSubject.send( (domain, permissionType, decision) ) diff --git a/macOS/DuckDuckGo/Permissions/Model/PermissionModel.swift b/macOS/DuckDuckGo/Permissions/Model/PermissionModel.swift index d1c3c62b14d..d6019a7759b 100644 --- a/macOS/DuckDuckGo/Permissions/Model/PermissionModel.swift +++ b/macOS/DuckDuckGo/Permissions/Model/PermissionModel.swift @@ -19,10 +19,8 @@ import AVFoundation import Combine import CoreLocation -import FeatureFlags import Foundation import Navigation -import PrivacyConfig import UserNotifications import WebKit import os.log @@ -48,7 +46,6 @@ final class PermissionModel { private let permissionManager: PermissionManagerProtocol private let geolocationService: GeolocationServiceProtocol private let systemPermissionManager: SystemPermissionManagerProtocol - private let featureFlagger: FeatureFlagger /// Holds the set of permissions the user manually removed (to avoid adding them back via updatePermissions) private var removedPermissions = Set() @@ -72,13 +69,11 @@ final class PermissionModel { init(webView: WKWebView? = nil, permissionManager: PermissionManagerProtocol, geolocationService: GeolocationServiceProtocol = GeolocationService.shared, - systemPermissionManager: SystemPermissionManagerProtocol = SystemPermissionManager(), - featureFlagger: FeatureFlagger) { + systemPermissionManager: SystemPermissionManagerProtocol = SystemPermissionManager()) { self.permissionManager = permissionManager self.geolocationService = geolocationService self.systemPermissionManager = systemPermissionManager - self.featureFlagger = featureFlagger if let webView { self.webView = webView self.subscribe(to: webView) @@ -154,10 +149,9 @@ final class PermissionModel { } else { let currentState = webView.geolocationState - // With new permission view, keep geolocation as active once it's been granted/used + // Keep geolocation as active once it's been granted/used // (.active or .inactive means it was granted or actively used) - if featureFlagger.isFeatureOn(.newPermissionView), - currentState == .none, + if currentState == .none, permissions.geolocation == .active || permissions.geolocation == .inactive { permissions.geolocation = .active } else { @@ -215,7 +209,7 @@ final class PermissionModel { let isPersisting = remember == true || persistsWhen(permission: permission, domain: domain) if isPersisting { self.permissionManager.setPermission(granted ? .allow : .deny, forDomain: domain, permissionType: permission) - } else if self.featureFlagger.isFeatureOn(.newPermissionView) { + } else { // Other permissions: one-time decisions store .ask for permission center visibility self.permissionManager.setPermission(.ask, forDomain: domain, permissionType: permission) } @@ -228,17 +222,6 @@ final class PermissionModel { // "unowned" query reference to be able to use the pointer when the callback is called on query deinit queryPtr = Unmanaged.passUnretained(query).toOpaque() - // When Geolocation queried by a website but System Permission is denied: switch to `disabled` - // Only apply this behavior when new permission view is disabled (old behavior) - // When new permission view is enabled, the dialog handles showing the two-step authorization flow - if !featureFlagger.isFeatureOn(.newPermissionView), - permissions.contains(.geolocation), - [.denied, .restricted].contains(self.geolocationService.authorizationStatus) - || !geolocationService.locationServicesEnabled() { - self.permissions.geolocation - .systemAuthorizationDenied(systemWide: !geolocationService.locationServicesEnabled()) - } - // Set state to .requested so the authorization popover can be shown permissions.forEach { self.permissions[$0].authorizationQueried(query, updateQueryIfAlreadyRequested: $0 == .popups) } query.isSystemPermissionDisabled = isSystemPermissionDisabled @@ -411,9 +394,9 @@ final class PermissionModel { for permission in permissions { var grant: PersistedPermissionDecision let stored = permissionManager.permission(forDomain: domain, permissionType: permission) - if case .allow = stored, permission.canPersistGrantedDecision(featureFlagger: featureFlagger) { + if case .allow = stored, permission.canPersistGrantedDecision { grant = .allow - } else if case .deny = stored, permission.canPersistDeniedDecision(featureFlagger: featureFlagger) { + } else if case .deny = stored, permission.canPersistDeniedDecision { grant = .deny } else if let state = self.permissions[permission] { switch state { @@ -435,7 +418,7 @@ final class PermissionModel { return false case .allow: // User has "Always Allow" stored - but check system permission first - if featureFlagger.isFeatureOn(.newPermissionView), isSystemPermissionDisabled(for: permission) { + if isSystemPermissionDisabled(for: permission) { return nil } case .ask: @@ -476,8 +459,7 @@ final class PermissionModel { // Check if this is "app=allow but system=disabled" case let isSystemDisabled: Bool = { guard let permission = permissions.first, - permission.requiresSystemPermission, - self.featureFlagger.isFeatureOn(.newPermissionView) else { return false } + permission.requiresSystemPermission else { return false } return self.permissionManager.permission(forDomain: domain, permissionType: permission) == .allow }() diff --git a/macOS/DuckDuckGo/Permissions/Model/PermissionType.swift b/macOS/DuckDuckGo/Permissions/Model/PermissionType.swift index 6333e4d01c3..894fa2e79b5 100644 --- a/macOS/DuckDuckGo/Permissions/Model/PermissionType.swift +++ b/macOS/DuckDuckGo/Permissions/Model/PermissionType.swift @@ -19,9 +19,7 @@ import AppKit import CommonObjCExtensions import DesignResourcesKitIcons -import FeatureFlags import Foundation -import PrivacyConfig import WebKit enum PermissionType: Hashable { @@ -81,37 +79,19 @@ extension PermissionType { return [.camera, .microphone, .geolocation, .notification] } - func canPersistGrantedDecision(featureFlagger: FeatureFlagger) -> Bool { - if featureFlagger.isFeatureOn(.newPermissionView) { - switch self { - case .camera, .microphone, .externalScheme, .popups, .geolocation, .notification, .autoplayPolicy: - return true - } - } else { - switch self { - case .camera, .microphone, .externalScheme, .popups, .notification, .autoplayPolicy: - return true - case .geolocation: - return false - } + var canPersistGrantedDecision: Bool { + switch self { + case .camera, .microphone, .externalScheme, .popups, .geolocation, .notification, .autoplayPolicy: + return true } } - func canPersistDeniedDecision(featureFlagger: FeatureFlagger) -> Bool { - if featureFlagger.isFeatureOn(.newPermissionView) { - switch self { - case .camera, .microphone, .geolocation, .externalScheme, .notification, .autoplayPolicy: - return true - case .popups: - return false - } - } else { - switch self { - case .camera, .microphone, .geolocation, .notification, .autoplayPolicy: - return true - case .popups, .externalScheme: - return false - } + var canPersistDeniedDecision: Bool { + switch self { + case .camera, .microphone, .geolocation, .externalScheme, .notification, .autoplayPolicy: + return true + case .popups: + return false } } diff --git a/macOS/DuckDuckGo/Permissions/View/PermissionAuthorization.storyboard b/macOS/DuckDuckGo/Permissions/View/PermissionAuthorization.storyboard deleted file mode 100644 index 6cf26ea9772..00000000000 --- a/macOS/DuckDuckGo/Permissions/View/PermissionAuthorization.storyboard +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationPopover.swift b/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationPopover.swift index 77b311ce7ed..5afd39afb50 100644 --- a/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationPopover.swift +++ b/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationPopover.swift @@ -18,20 +18,16 @@ import Cocoa import SwiftUI -import PrivacyConfig -import FeatureFlags final class PermissionAuthorizationPopover: NSPopover { @nonobjc private var didShow: Bool = false - private let featureFlagger: FeatureFlagger - init(featureFlagger: FeatureFlagger) { - self.featureFlagger = featureFlagger + override init() { super.init() behavior = .applicationDefined - setupContentController() + contentViewController = PermissionAuthorizationViewController() self.delegate = self } @@ -49,37 +45,14 @@ final class PermissionAuthorizationPopover: NSPopover { // swiftlint:disable force_cast var viewController: PermissionAuthorizationViewController { get { - // Ensure content controller is set up if contentViewController == nil { - setupContentController() + contentViewController = PermissionAuthorizationViewController() } return contentViewController as! PermissionAuthorizationViewController } } // swiftlint:enable force_cast - private func setupContentController() { - let controller: PermissionAuthorizationViewController - - if featureFlagger.isFeatureOn(.newPermissionView) { - // Create programmatically - controller = PermissionAuthorizationViewController(newPermissionView: true) - } else { - // Load from storyboard - controller = setupStoryboardController() - } - - contentViewController = controller - } - - // swiftlint:disable force_cast - private func setupStoryboardController() -> PermissionAuthorizationViewController { - let storyboard = NSStoryboard(name: "PermissionAuthorization", bundle: nil) - return storyboard - .instantiateController(withIdentifier: "PermissionAuthorizationViewController") as! PermissionAuthorizationViewController - } - // swiftlint:enable force_cast - } extension PermissionAuthorizationPopover: NSPopoverDelegate { diff --git a/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationViewController.swift b/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationViewController.swift index e2901df5e01..648a4fbf742 100644 --- a/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationViewController.swift +++ b/macOS/DuckDuckGo/Permissions/View/PermissionAuthorizationViewController.swift @@ -64,19 +64,7 @@ final class PermissionAuthorizationViewController: NSViewController { let systemPermissionManager = SystemPermissionManager() - @IBOutlet var descriptionLabel: NSTextField! - @IBOutlet var domainNameLabel: NSTextField! - @IBOutlet var alwaysAllowCheckbox: NSButton! - @IBOutlet var alwaysAllowStackView: NSStackView! - @IBOutlet var learnMoreStackView: NSStackView! - @IBOutlet var denyButton: NSButton! - @IBOutlet var buttonsBottomConstraint: NSLayoutConstraint! - @IBOutlet var learnMoreBottomConstraint: NSLayoutConstraint! - @IBOutlet weak var linkButton: LinkButton! - @IBOutlet weak var allowButton: NSButton! - private var swiftUIHostingView: NSHostingView? - private let newPermissionView: Bool /// Indicates whether the authorization flow is still in progress (user hasn't clicked Allow/Deny yet). /// This prevents the popover from being closed prematurely during two-step flows (e.g., geolocation). @@ -84,130 +72,32 @@ final class PermissionAuthorizationViewController: NSViewController { weak var query: PermissionAuthorizationQuery? { didSet { - if newPermissionView { - setupSwiftUIView() - } else { - updateText() - } + setupSwiftUIView() } } - // Programmatic initializer for SwiftUI mode - init(newPermissionView: Bool) { - self.newPermissionView = newPermissionView + init() { super.init(nibName: nil, bundle: nil) } - // Storyboard initializer + @available(*, unavailable) required init?(coder: NSCoder) { - self.newPermissionView = false - super.init(coder: coder) + fatalError("PermissionAuthorizationViewController: Use init() instead") } override func loadView() { - if newPermissionView { - // Create a simple container view for SwiftUI - view = NSView() - } else { - // Load from nib/storyboard - super.loadView() - } + view = NSView() } override func viewDidLoad() { super.viewDidLoad() - - if newPermissionView { - setupSwiftUIView() - } else { - updateText() - } - } - - override func viewWillAppear() { - guard !newPermissionView else { return } - - alwaysAllowCheckbox.state = .off - if query?.shouldShowCancelInsteadOfDeny == true { - denyButton.title = UserText.cancel - } else { - denyButton.title = UserText.permissionPopoverDenyButton - } - denyButton.setAccessibilityIdentifier("PermissionAuthorizationViewController.denyButton") - } - - private func updateText() { - guard !newPermissionView, - isViewLoaded, - let query = query, - !query.permissions.isEmpty - else { return } - - switch query.permissions[0] { - case .camera, .microphone: - descriptionLabel.stringValue = String(format: UserText.devicePermissionAuthorizationFormat, - query.domain, - query.permissions.localizedDescription.lowercased()) - case .popups: - descriptionLabel.stringValue = String(format: UserText.popupWindowsPermissionAuthorizationFormat, - query.domain, - query.permissions.localizedDescription.lowercased()) - case .notification: - descriptionLabel.stringValue = String(format: UserText.notificationPermissionAuthorizationFormat, - query.domain) - case .externalScheme where query.domain.isEmpty: - descriptionLabel.stringValue = String(format: UserText.externalSchemePermissionAuthorizationNoDomainFormat, - query.permissions.localizedDescription) - case .externalScheme: - descriptionLabel.stringValue = String(format: UserText.externalSchemePermissionAuthorizationFormat, - query.domain, - query.permissions.localizedDescription) - case .geolocation: - descriptionLabel.stringValue = String(format: UserText.locationPermissionAuthorizationFormat, query.domain) - case .autoplayPolicy: - break // Autoplay policy does not use authorization flow - } - alwaysAllowCheckbox.title = UserText.permissionAlwaysAllowOnDomainCheckbox - domainNameLabel.stringValue = query.domain.isEmpty ? "" : "“" + query.domain + "”" - alwaysAllowStackView.isHidden = !query.shouldShowAlwaysAllowCheckbox - learnMoreStackView.isHidden = !query.permissions.contains(.geolocation) - learnMoreBottomConstraint.isActive = !learnMoreStackView.isHidden - buttonsBottomConstraint.isActive = !learnMoreBottomConstraint.isActive - linkButton.title = UserText.permissionPopupLearnMoreLink - allowButton.title = UserText.permissionPopupAllowButton - allowButton.setAccessibilityIdentifier("PermissionAuthorizationViewController.allowButton") - } - - @IBAction func alwaysAllowLabelClick(_ sender: Any) { - guard !newPermissionView else { return } - alwaysAllowCheckbox.setNextState() - } - - @IBAction func grantAction(_ sender: NSButton) { - guard !newPermissionView else { return } - self.dismiss() - query?.handleDecision(grant: true, remember: query!.shouldShowAlwaysAllowCheckbox && alwaysAllowCheckbox.state == .on) - } - - @IBAction func denyAction(_ sender: NSButton) { - guard !newPermissionView else { return } - self.dismiss() - guard let query = query, - !query.shouldShowCancelInsteadOfDeny - else { return } - - query.handleDecision(grant: false) - } - - @IBAction func learnMoreAction(_ sender: NSButton) { - guard !newPermissionView else { return } - Application.appDelegate.windowControllersManager.show(url: "https://help.duckduckgo.com/privacy/device-location-services".url, source: .ui, newTab: true) + setupSwiftUIView() } // MARK: - SwiftUI View Setup private func setupSwiftUIView() { - guard newPermissionView, let query = query, !query.permissions.isEmpty else { return } + guard let query = query, !query.permissions.isEmpty else { return } // Remove all existing subviews to ensure clean state view.subviews.forEach { $0.removeFromSuperview() } @@ -277,7 +167,7 @@ final class PermissionAuthorizationViewController: NSViewController { } private func fireAuthorizationPixel(decision: PermissionPixel.AuthorizationDecision) { - guard newPermissionView, let query = query else { return } + guard let query = query else { return } // Fire pixel for each permission type in the query for permissionType in query.permissions { PixelKit.fire(PermissionPixel.authorizationDecision(permissionType: permissionType, decision: decision)) diff --git a/macOS/DuckDuckGo/Permissions/View/PermissionContextMenu.swift b/macOS/DuckDuckGo/Permissions/View/PermissionContextMenu.swift index a4530e9cfbb..99d0872b04f 100644 --- a/macOS/DuckDuckGo/Permissions/View/PermissionContextMenu.swift +++ b/macOS/DuckDuckGo/Permissions/View/PermissionContextMenu.swift @@ -170,7 +170,7 @@ final class PermissionContextMenu: NSMenu { // only show one persistence option per permission type let reduced = permissions.reduce(into: [:], { $0[$1.key] = $1.value }) for (permission, state) in reduced { - guard permission.canPersistGrantedDecision(featureFlagger: featureFlagger) || permission.canPersistDeniedDecision(featureFlagger: featureFlagger) else { continue } + guard permission.canPersistGrantedDecision || permission.canPersistDeniedDecision else { continue } if case .disabled = state { continue } addSeparator(if: numberOfItems > 0) @@ -188,11 +188,11 @@ final class PermissionContextMenu: NSMenu { let isNotifyChecked = (permission == .popups && hasTemporaryPopupAllowance) ? false : (persistedValue == .ask) addItem(.alwaysAsk(permission, on: domain, target: self, isChecked: isNotifyChecked)) - if permission.canPersistGrantedDecision(featureFlagger: featureFlagger) { + if permission.canPersistGrantedDecision { addItem(.alwaysAllow(permission, on: domain, target: self, isChecked: persistedValue == .allow)) } - if permission.canPersistDeniedDecision(featureFlagger: featureFlagger) { + if permission.canPersistDeniedDecision { addItem(.alwaysDeny(permission, on: domain, target: self, isChecked: persistedValue == .deny)) } } diff --git a/macOS/DuckDuckGo/Permissions/View/PopupBlockedPopover.swift b/macOS/DuckDuckGo/Permissions/View/PopupBlockedPopover.swift index 08bf6d82eff..f7eb9e07d6f 100644 --- a/macOS/DuckDuckGo/Permissions/View/PopupBlockedPopover.swift +++ b/macOS/DuckDuckGo/Permissions/View/PopupBlockedPopover.swift @@ -17,20 +17,15 @@ // import Cocoa -import FeatureFlags -import PrivacyConfig import SwiftUI final class PopupBlockedPopover: NSPopover { - private let featureFlagger: FeatureFlagger - - init(featureFlagger: FeatureFlagger) { - self.featureFlagger = featureFlagger + override init() { super.init() behavior = .applicationDefined - setupContentController() + contentViewController = PopupBlockedViewController() } required init?(coder: NSCoder) { @@ -48,96 +43,53 @@ final class PopupBlockedPopover: NSPopover { var viewController: PopupBlockedViewController { get { if contentViewController == nil { - setupContentController() + contentViewController = PopupBlockedViewController() } return contentViewController as! PopupBlockedViewController } } // swiftlint:enable force_cast - private func setupContentController() { - let controller: PopupBlockedViewController - - if featureFlagger.isFeatureOn(.newPermissionView) { - // Create programmatically for SwiftUI - controller = PopupBlockedViewController(newPermissionView: true) - } else { - // Load from storyboard - controller = setupStoryboardController() - } - - contentViewController = controller - } - - // swiftlint:disable force_cast - private func setupStoryboardController() -> PopupBlockedViewController { - let storyboard = NSStoryboard(name: "PermissionAuthorization", bundle: nil) - return storyboard - .instantiateController(withIdentifier: "PopupBlockedViewController") as! PopupBlockedViewController - } - // swiftlint:enable force_cast - } final class PopupBlockedViewController: NSViewController { - @IBOutlet weak var descriptionLabel: NSTextField! - private var swiftUIHostingView: NSHostingView? - private let newPermissionView: Bool private var dismissWorkItem: DispatchWorkItem? weak var query: PermissionAuthorizationQuery? { didSet { - if newPermissionView { - setupSwiftUIView() - } + setupSwiftUIView() } } - // Programmatic initializer for SwiftUI mode - init(newPermissionView: Bool) { - self.newPermissionView = newPermissionView + init() { super.init(nibName: nil, bundle: nil) } - // Storyboard initializer + @available(*, unavailable) required init?(coder: NSCoder) { - self.newPermissionView = false - super.init(coder: coder) + fatalError("PopupBlockedViewController: Use init() instead") } override func loadView() { - if newPermissionView { - // Create a simple container view for SwiftUI - view = NSView() - } else { - // Load from nib/storyboard - super.loadView() - } + view = NSView() } override func viewDidLoad() { super.viewDidLoad() - - if newPermissionView { - setupSwiftUIView() - } else { - descriptionLabel.stringValue = UserText.permissionPopupBlockedPopover - } + setupSwiftUIView() } override func viewDidAppear() { // Cancel any existing work item to prevent multiple timers dismissWorkItem?.cancel() - // New UI with Open button needs more time for user interaction - let dismissDelay: TimeInterval = newPermissionView ? 4.0 : 2.0 let workItem = DispatchWorkItem { [weak self] in self?.dismiss() } dismissWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + dismissDelay, execute: workItem) + DispatchQueue.main.asyncAfter(deadline: .now() + 4.0, execute: workItem) } override func viewWillDisappear() { @@ -147,8 +99,6 @@ final class PopupBlockedViewController: NSViewController { } private func setupSwiftUIView() { - guard newPermissionView else { return } - view.subviews.forEach { $0.removeFromSuperview() } swiftUIHostingView = nil diff --git a/macOS/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift b/macOS/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift index c850bdacbb5..3f7f7ad9a75 100644 --- a/macOS/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift +++ b/macOS/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift @@ -18,23 +18,18 @@ import Foundation import Combine -import FeatureFlags import PrivacyDashboard -import PrivacyConfig import AppKit typealias PrivacyDashboardPermissionAuthorizationState = [(permission: PermissionType, state: PermissionAuthorizationState)] final class PrivacyDashboardPermissionHandler { - init(permissionManager: PermissionManagerProtocol, - featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { + init(permissionManager: PermissionManagerProtocol) { self.permissionManager = permissionManager - self.featureFlagger = featureFlagger } private let permissionManager: PermissionManagerProtocol - private let featureFlagger: FeatureFlagger private weak var tabViewModel: TabViewModel? private var onPermissionChange: (([AllowedPermission]) -> Void)? private var cancellables = Set() @@ -67,65 +62,8 @@ final class PrivacyDashboardPermissionHandler { } private func updatePermissions() { - // Skip permission updates when new permission view is enabled - // Permission management is handled by the new Permission Center - if featureFlagger.isFeatureOn(.newPermissionView) { - onPermissionChange?([]) - return - } - - guard let usedPermissions = tabViewModel?.usedPermissions else { - assertionFailure("PrivacyDashboardViewController: tabViewModel not set") - return - } - guard let domain = tabViewModel?.tab.content.urlForWebView?.host else { - onPermissionChange?([]) - return - } - - let authorizationState: PrivacyDashboardPermissionAuthorizationState - authorizationState = permissionManager.persistedPermissionTypes.union(usedPermissions.keys) - .compactMap { permissionType in - guard permissionManager.hasPermissionPersisted(forDomain: domain, permissionType: permissionType) - || usedPermissions[permissionType] != nil - else { - return nil - } - let decision = permissionManager.permission(forDomain: domain, permissionType: permissionType) - return (permissionType, PermissionAuthorizationState(decision: decision)) - } - - var allowedPermissions: [AllowedPermission] = [] - - allowedPermissions = authorizationState.map { item in - AllowedPermission(key: item.permission.rawValue, - icon: item.permission.jsStyle, - title: item.permission.jsTitle, - permission: item.state.rawValue, - used: usedPermissions[item.permission] != nil, - paused: usedPermissions[item.permission] == .paused, - options: makeOptions(for: item, domain: domain) - ) - } - - onPermissionChange?(allowedPermissions) - } - - private func makeOptions(for item: (permission: PermissionType, state: PermissionAuthorizationState), domain: String) -> [[String: String]] { - return PermissionAuthorizationState.allCases.compactMap { decision -> [String: String]? in - // don't show Permanently Allow if can't persist Granted Decision - switch decision { - case .grant: - guard item.permission.canPersistGrantedDecision(featureFlagger: featureFlagger) else { return nil } - case .deny: - guard item.permission.canPersistDeniedDecision(featureFlagger: featureFlagger) else { return nil } - case .ask: break - } - return [ - "id": decision.rawValue, - "title": String(format: decision.localizedFormat(for: item.permission), domain) - ] - } + // Permission management is handled by the Permission Center + onPermissionChange?([]) } } diff --git a/macOS/DuckDuckGo/Statistics/PermissionPixel.swift b/macOS/DuckDuckGo/Statistics/PermissionPixel.swift index f539a65aa81..bdcf647a14d 100644 --- a/macOS/DuckDuckGo/Statistics/PermissionPixel.swift +++ b/macOS/DuckDuckGo/Statistics/PermissionPixel.swift @@ -20,8 +20,6 @@ import PixelKit /** * This enum keeps pixels related to permissions management. - * - * These pixels are only fired when the newPermissionView feature flag is enabled. */ enum PermissionPixel: PixelKitEvent { diff --git a/macOS/DuckDuckGo/Tab/Model/Tab.swift b/macOS/DuckDuckGo/Tab/Model/Tab.swift index 06ca8c73ba9..e0981d46fa7 100644 --- a/macOS/DuckDuckGo/Tab/Model/Tab.swift +++ b/macOS/DuckDuckGo/Tab/Model/Tab.swift @@ -321,8 +321,7 @@ protocol TabDelegate: ContentOverlayUserScriptDelegate { webView.setAccessibilityIdentifier("WebView") permissions = PermissionModel(permissionManager: permissionManager, - geolocationService: geolocationService, - featureFlagger: featureFlagger) + geolocationService: geolocationService) let userContentControllerPromise = Future.promise() let userScriptsPublisher = userContentControllerPromise.future diff --git a/macOS/DuckDuckGo/Tab/UserScripts/WebNotificationsHandler.swift b/macOS/DuckDuckGo/Tab/UserScripts/WebNotificationsHandler.swift index a604fd92cb9..4d340639841 100644 --- a/macOS/DuckDuckGo/Tab/UserScripts/WebNotificationsHandler.swift +++ b/macOS/DuckDuckGo/Tab/UserScripts/WebNotificationsHandler.swift @@ -256,12 +256,6 @@ final class WebNotificationsHandler: NSObject, Subfeature { return } - guard featureFlagger.isFeatureOn(.newPermissionView) else { - Logger.general.debug("WebNotificationsHandler: Blocked - newPermissionView flag disabled (ID: \(payload.id))") - await sendErrorEvent(id: payload.id, to: original.webView) - return - } - guard let url = await original.webView?.url, let domain = url.host else { Logger.general.debug("WebNotificationsHandler: Missing domain for permission check (ID: \(payload.id))") @@ -316,11 +310,6 @@ final class WebNotificationsHandler: NSObject, Subfeature { return RequestPermissionResponse(permission: Permission.denied.rawValue) } - guard featureFlagger.isFeatureOn(.newPermissionView) else { - Logger.general.debug("WebNotificationsHandler: Permission denied - newPermissionView flag disabled") - return RequestPermissionResponse(permission: Permission.denied.rawValue) - } - guard let url = await original.webView?.url, let domain = url.host, let permissionModel = permissionModel else { diff --git a/macOS/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/macOS/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 2a81ea7340e..3c810f97bac 100644 --- a/macOS/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/macOS/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -1187,15 +1187,10 @@ final class TabBarViewItem: NSCollectionViewItem { // MARK: - Active Permission Icons in Favicon private var isShowingActivePermissionIcon: Bool { - featureFlagger.isFeatureOn(.newPermissionView) && !activePermissionTypes.isEmpty + !activePermissionTypes.isEmpty } private func updateActivePermissionIcons() { - guard featureFlagger.isFeatureOn(.newPermissionView) else { - stopActivePermissionIconTimer() - return - } - // Collect all active permissions (camera, microphone, geolocation) var activeTypes: [PermissionType] = [] if usedPermissions.camera.isActive { diff --git a/macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift b/macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift index 727df5db49c..79caa1c69d1 100644 --- a/macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift +++ b/macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift @@ -210,10 +210,6 @@ public enum FeatureFlag: String, CaseIterable { /// https://app.asana.com/1/137249556945/project/414235014887631/task/1211395954816928?focus=true case webNotifications - /// New permission management view - /// https://app.asana.com/1/137249556945/project/1148564399326804/task/1211985993948718?focus=true - case newPermissionView - /// Shows a survey when quitting the app for the first time in a determined period /// https://app.asana.com/1/137249556945/project/1204006570077678/task/1212242893241885?focus=true case firstTimeQuitSurvey @@ -518,8 +514,6 @@ extension FeatureFlag: FeatureFlagDescribing { Config(source: .remoteReleasable(.feature(.popupBlocking)), category: .popupBlocking) case .webNotifications: Config(source: .remoteReleasable(.subfeature(MacOSBrowserConfigSubfeature.webNotifications)), category: .webNotifications) - case .newPermissionView: - Config(source: .remoteReleasable(.feature(.combinedPermissionView))) case .firstTimeQuitSurvey: Config(defaultValue: .enabled, source: .remoteReleasable(.subfeature(MacOSBrowserConfigSubfeature.firstTimeQuitSurvey))) case .autofillPasswordSearchPrioritizeDomain: diff --git a/macOS/PixelDefinitions/pixels/definitions/permission_pixels.json5 b/macOS/PixelDefinitions/pixels/definitions/permission_pixels.json5 index 35adbfbf734..5a642cb87eb 100644 --- a/macOS/PixelDefinitions/pixels/definitions/permission_pixels.json5 +++ b/macOS/PixelDefinitions/pixels/definitions/permission_pixels.json5 @@ -1,5 +1,4 @@ // Permission management pixels -// These pixels are fired when the newPermissionView feature flag is enabled { "m_mac_permission_authorization": { "description": "Fired when user selects an option (Allow/Deny) in the permission authorization dialog", diff --git a/macOS/UITests/NewPermissionViewTests.swift b/macOS/UITests/NewPermissionViewTests.swift index 9024936c666..e09308dc970 100644 --- a/macOS/UITests/NewPermissionViewTests.swift +++ b/macOS/UITests/NewPermissionViewTests.swift @@ -19,8 +19,8 @@ import AppKitExtensions import XCTest -/// UI Tests for the new permission authorization view and permission center (behind the newPermissionView feature flag). -/// These tests verify the SwiftUI-based permission UI that replaces the legacy storyboard-based permission UI. +/// UI Tests for the permission authorization view and permission center. +/// These tests verify the SwiftUI-based permission UI. /// Note: Restricted to macOS 26+ due to differences in system permission dialogs across macOS versions. class NewPermissionViewTests: UITestCase { @@ -52,8 +52,8 @@ class NewPermissionViewTests: UITestCase { app.resetAuthorizationStatus(for: .camera) app.resetAuthorizationStatus(for: .microphone) - // Now set up and launch the app with the newPermissionView feature flag enabled - app = XCUIApplication.setUp(featureFlags: ["newPermissionView": true]) + // Now set up and launch the app + app = XCUIApplication.setUp() addressBarTextField = app.addressBar app.enforceSingleWindow() @@ -733,13 +733,12 @@ final class NewPermissionViewPopupTests: UITestCase { try super.setUpWithError() continueAfterFailure = false - // Enable newPermissionView AND popup blocking features with reduced timeout + // Enable popup blocking feature with reduced timeout app = XCUIApplication.setUp( environment: [ "POPUP_TIMEOUT_OVERRIDE": String(PopupTimeout.testingThreshold) ], featureFlags: [ - "newPermissionView": true, "popupBlocking": true, ] ) diff --git a/macOS/UITests/PermissionsTests.swift b/macOS/UITests/PermissionsTests.swift deleted file mode 100644 index 414230182af..00000000000 --- a/macOS/UITests/PermissionsTests.swift +++ /dev/null @@ -1,618 +0,0 @@ -// -// PermissionsTests.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKitExtensions -import XCTest - -/// We are apparently in the part of the `XCUIAutomation` life cycle right now in which interruption management using `addUIInterruptionMonitor` -/// doesn't fire due to an acknowledged bug (https://forums.developer.apple.com/forums/thread/737880), so we can't test these the best way, which is -/// to create an interruption handler and wait for TCC requests to respond to (https://eclecticlight.co/2023/02/10/privacy-what-tcc-does-and-doesnt/). -/// Realistically, the best approach (in terms of robust test design) may never be a good fit for these tests, because it is always a possibility that -/// one of the targeted macOS versions is manifesting this every-few-systems bug. Therefore, these tests simply wait for the relevant -/// privacy request to click on directly, via a combination of bundle ID targeting and button targeting by number. That means that adjustments could -/// be needed in the future, in case of significant changes in this system-level interface in future macOS versions (but Apple tries not to change -/// that too frequently) or its backend (for instance, if the bundle ID for the user notification center changes). Here is a link to how to do this -/// the best way, in the event that a future macOS version stops supporting this approach, but also solves the bug with `addUIInterruptionMonitor`, -/// and you want to branch the implementations per macOS version: -/// https://stackoverflow.com/questions/56559269/adduiinterruptionmonitor-is-not-getting-called-on-macos -class PermissionsTests: UITestCase { - - private var notificationCenter: XCUIApplication! - private var addressBarTextField: XCUIElement! - private var permissionsSiteURL: URL! - - // Fire Dialog Element Accessors - private var fireDialogTitle: XCUIElement { app.fireDialogTitle } - private var fireDialogHistoryToggle: XCUIElement { app.fireDialogHistoryToggle } - private var fireDialogCookiesToggle: XCUIElement { app.fireDialogCookiesToggle } - private var fireDialogTabsToggle: XCUIElement { app.fireDialogTabsToggle } - private var fireDialogBurnButton: XCUIElement { app.fireDialogBurnButton } - - override func setUpWithError() throws { - try super.setUpWithError() - continueAfterFailure = false - - permissionsSiteURL = try XCTUnwrap(URL(string: "https://permission.site"), "It wasn't possible to unwrap a URL that the tests depend on.") - notificationCenter = XCUIApplication(bundleIdentifier: "com.apple.UserNotificationCenter") - - // Reset permissions BEFORE app launch - this is critical for TCC dialogs to appear - app = XCUIApplication() - app.resetAuthorizationStatus(for: .camera) - app.resetAuthorizationStatus(for: .microphone) - - // Now set up and launch the app - app = XCUIApplication.setUp() - addressBarTextField = app.addressBar - app.enforceSingleWindow() - - // Clear history using Fire Dialog - XCTAssertTrue( - app.historyMenu.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "History menu bar item didn't appear in a reasonable timeframe." - ) - app.historyMenu.click() - - XCTAssertTrue( - app.clearAllHistoryMenuItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Clear all history item didn't appear in a reasonable timeframe." - ) - app.clearAllHistoryMenuItem.click() - - // Fire Dialog should appear instead of old alert - XCTAssertTrue( - fireDialogTitle.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Fire dialog didn't appear in a reasonable timeframe." - ) - - // Select "Everything" scope to clear all history - app.fireDialogSegmentedControl.buttons["Everything"].click() - - // Ensure toggles are enabled - fireDialogHistoryToggle.toggleCheckboxIfNeeded(to: true, ensureHittable: { _ in }) - fireDialogCookiesToggle.toggleCheckboxIfNeeded(to: true, ensureHittable: { _ in }) - fireDialogTabsToggle.toggleCheckboxIfNeeded(to: true, ensureHittable: { _ in }) - - // Click burn button to clear history - fireDialogBurnButton.click() - - // Wait for fire animation to complete - XCTAssertTrue( - app.fakeFireButton.waitForNonExistence(timeout: UITests.Timeouts.fireAnimation), - "Fire animation didn't finish and cease existing in a reasonable timeframe." - ) - - XCTAssertTrue( - addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The address bar text field didn't become available in a reasonable timeframe before starting the test." - ) - } - - func test_cameraPermissions_withAcceptedTCCChallenge_showCorrectStateInBrowser() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let cameraButton = app.webViews.buttons["Camera"] - cameraButton.clickAfterExistenceTestSucceeds() - XCTAssert( - notificationCenter.buttons.firstMatch.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The notification center didn't appear. This can happen because the TCC setting at the start of the test wasn't correct – check the app.resetPermissions behavior." - ) - let allowButtonIndex = try XCTUnwrap(notificationCenter.indexOfSystemModelDialogButtonOnElement( - titled: "Allow", - "OK" - )) // You can add any titles you see this permission dialog using on any tested macOS versions, because it is evaluated by a variadic - // function. - let allowButton = notificationCenter.buttons.element(boundBy: allowButtonIndex) - allowButton.clickAfterExistenceTestSucceeds() // Click system camera permissions dialog - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - permissionsPopoverAllowButton.clickAfterExistenceTestSucceeds() - var websitePermissionsColorIsGreen = false - for _ in 1 ... 4 { // permission.site updates this color a bit slowly and we have no control over it, so we try a few times. - websitePermissionsColorIsGreen = try websitePermissionsButtonIsExpectedColor(cameraButton, is: .green) - if websitePermissionsColorIsGreen { - break - } - usleep(500_000) - } - XCTAssertTrue( - websitePermissionsColorIsGreen, - "After a few attempts to wait for permissions.site to update their button animation after the TCC dialog, their button has to be green." - ) - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.cameraButton"] - navigationBarViewControllerPermissionButton.clickAfterExistenceTestSucceeds() - - let permissionContextMenuAlwaysAsk = app.menuItems["PermissionContextMenu.alwaysAsk"] - XCTAssertTrue( - permissionContextMenuAlwaysAsk.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "permissionContextMenuAlwaysAsk didn't exist in a reasonable timeframe." - ) - let permissionContextMenuAlwaysAskValue = try XCTUnwrap(permissionContextMenuAlwaysAsk.value as? String) - XCTAssertEqual( - permissionContextMenuAlwaysAskValue, - "selected", - "The \"always ask\" menu item of the permission context menu has to be the selected item." - ) - } - - func test_cameraPermissions_withDeniedTCCChallenge_showCorrectStateInBrowser() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let cameraButton = app.webViews.buttons["Camera"] - cameraButton.clickAfterExistenceTestSucceeds() - XCTAssert( - notificationCenter.buttons.firstMatch.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The notification center didn't appear. This can happen because the TCC setting at the start of the test wasn't correct – check the app.resetPermissions behavior." - ) - let denyButtonIndex = try XCTUnwrap(notificationCenter.indexOfSystemModelDialogButtonOnElement( - titled: "Deny", "No", "Cancel", "Don’t Allow", "Don't Allow" - )) // You can add any titles you see this permission dialog using on any tested macOS versions, because it is evaluated by a variadic - // function. - let denyButton = notificationCenter.buttons.element(boundBy: denyButtonIndex) - denyButton.clickAfterExistenceTestSucceeds() // Click system camera permissions dialog - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - - XCTAssertTrue( // Prove that the browser's permission popover does not appear - permissionsPopoverAllowButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "The permissions popover in the browser should not appear when camera permission has been denied." - ) - - var websitePermissionsColorIsRed = false - for _ in 1 ... 4 { - websitePermissionsColorIsRed = try websitePermissionsButtonIsExpectedColor(cameraButton, is: .red) - if websitePermissionsColorIsRed { - break - } - usleep(500_000) - } - XCTAssertTrue( - websitePermissionsColorIsRed, - "After a few attempts to wait for permissions.site to update their button animation after the TCC dialog, their button has to be red." - ) - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.cameraButton"] - XCTAssertTrue( // Prove that the browser's permission button does not appear - navigationBarViewControllerPermissionButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "The permissions button in the browser should not appear when camera permission has been denied." - ) - } - - func test_cameraPermissions_withAcceptedTCCChallenge_whereAlwaysDenyIsSelected_alwaysDenies() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let cameraButton = app.webViews.buttons["Camera"] - cameraButton.clickAfterExistenceTestSucceeds() - XCTAssert( - notificationCenter.buttons.firstMatch.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The notification center didn't appear. This can happen because the TCC setting at the start of the test wasn't correct – check the app.resetPermissions behavior." - ) - let allowButtonIndex = try XCTUnwrap(notificationCenter.indexOfSystemModelDialogButtonOnElement( - titled: "Allow", - "OK" - )) // You can add any titles you see this permission dialog using on any tested macOS versions, because it is evaluated by a variadic - // function. - let allowButton = notificationCenter.buttons.element(boundBy: allowButtonIndex) - allowButton.clickAfterExistenceTestSucceeds() // Click system camera permissions dialog - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - permissionsPopoverAllowButton.clickAfterExistenceTestSucceeds() - var websitePermissionsColorIsGreen = false - for _ in 1 ... 4 { // permission.site updates this color a bit slowly and we have no control over it, so we try a few times. - websitePermissionsColorIsGreen = try websitePermissionsButtonIsExpectedColor(cameraButton, is: .green) - if websitePermissionsColorIsGreen { - break - } - usleep(500_000) - } - XCTAssertTrue( - websitePermissionsColorIsGreen, - "After a few attempts to wait for permissions.site to update their button animation after the TCC dialog, their button has to be green." - ) - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.cameraButton"] - navigationBarViewControllerPermissionButton.clickAfterExistenceTestSucceeds() - - let permissionContextMenuAlwaysAsk = app.menuItems["PermissionContextMenu.alwaysAsk"] - - XCTAssertTrue( - permissionContextMenuAlwaysAsk.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "permissionContextMenuAlwaysAsk didn't exist in a reasonable timeframe." - ) - let permissionContextMenuAlwaysAskValue = try XCTUnwrap(permissionContextMenuAlwaysAsk.value as? String) - XCTAssertEqual( - permissionContextMenuAlwaysAskValue, - "selected", - "The \"always ask\" menu item of the permission context menu has to be the selected item." - ) - let permissionContextMenuAlwaysDeny = app.menuItems["PermissionContextMenu.alwaysDeny"] - permissionContextMenuAlwaysDeny.clickAfterExistenceTestSucceeds() - app.enforceSingleWindow() - addressBarTextField.typeURL(permissionsSiteURL) - for _ in 1 ... 4 { - cameraButton.clickAfterExistenceTestSucceeds() - } - XCTAssertTrue( - try websitePermissionsButtonIsExpectedColor(cameraButton, is: .red), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the permission button will remain red" - ) - XCTAssert( - notificationCenter.buttons.firstMatch.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the TCC dialog permission alert will not be on the screen" - ) - XCTAssert( - permissionsPopoverAllowButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the permission popover will not be on the screen" - ) - } - - func test_microphonePermissions_withAcceptedTCCChallenge_showCorrectStateInBrowser() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let microphoneButton = app.webViews.buttons["Microphone"] - microphoneButton.clickAfterExistenceTestSucceeds() - XCTAssert( - notificationCenter.buttons.firstMatch.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The notification center didn't appear. This can happen because the TCC setting at the start of the test wasn't correct – check the app.resetPermissions behavior." - ) - let allowButtonIndex = try XCTUnwrap(notificationCenter.indexOfSystemModelDialogButtonOnElement( - titled: "Allow", - "OK" - )) // You can add any titles you see this permission dialog using on any tested macOS versions, because it is evaluated by a variadic - // function. - let allowButton = notificationCenter.buttons.element(boundBy: allowButtonIndex) - allowButton.clickAfterExistenceTestSucceeds() // Click system microphone permissions dialog - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - permissionsPopoverAllowButton.clickAfterExistenceTestSucceeds() - var websitePermissionsColorIsGreen = false - for _ in 1 ... 4 { // permission.site updates this color a bit slowly and we have no control over it, so we try a few times. - websitePermissionsColorIsGreen = try websitePermissionsButtonIsExpectedColor(microphoneButton, is: .green) - if websitePermissionsColorIsGreen { - break - } - usleep(500_000) - } - XCTAssertTrue( - websitePermissionsColorIsGreen, - "After a few attempts to wait for permissions.site to update their button animation after the TCC dialog, their button has to be green." - ) - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.microphoneButton"] - navigationBarViewControllerPermissionButton.clickAfterExistenceTestSucceeds() - - let permissionContextMenuAlwaysAsk = app.menuItems["PermissionContextMenu.alwaysAsk"] - XCTAssertTrue( - permissionContextMenuAlwaysAsk.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "permissionContextMenuAlwaysAsk didn't exist in a reasonable timeframe." - ) - let permissionContextMenuAlwaysAskValue = try XCTUnwrap(permissionContextMenuAlwaysAsk.value as? String) - XCTAssertEqual( - permissionContextMenuAlwaysAskValue, - "selected", - "The \"always ask\" menu item of the permission context menu has to be the selected item." - ) - } - - func test_microphonePermissions_withDeniedTCCChallenge_showCorrectStateInBrowser() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let microphoneButton = app.webViews.buttons["Microphone"] - microphoneButton.clickAfterExistenceTestSucceeds() - XCTAssert( - notificationCenter.buttons.firstMatch.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The notification center didn't appear. This can happen because the TCC setting at the start of the test wasn't correct – check the app.resetPermissions behavior." - ) - let denyButtonIndex = try XCTUnwrap(notificationCenter.indexOfSystemModelDialogButtonOnElement( - titled: "Deny", "No", "Cancel", "Don’t Allow", "Don't Allow" - )) // You can add any titles you see this permission dialog using on any tested macOS versions, because it is evaluated by a variadic - // function. - let denyButton = notificationCenter.buttons.element(boundBy: denyButtonIndex) - denyButton.clickAfterExistenceTestSucceeds() // Click system microphone permissions dialog - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - - XCTAssertTrue( // Prove that the browser's permission popover does not appear - permissionsPopoverAllowButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "The permissions popover in the browser should not appear when camera permission has been denied." - ) - - var websitePermissionsColorIsRed = false - for _ in 1 ... 4 { - websitePermissionsColorIsRed = try websitePermissionsButtonIsExpectedColor(microphoneButton, is: .red) - if websitePermissionsColorIsRed { - break - } - usleep(500_000) - } - XCTAssertTrue( - websitePermissionsColorIsRed, - "After between one and four attempts to wait for permissions.site to update their button animation after the TCC dialog, their button has to be red." - ) - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.microphoneButton"] - XCTAssertTrue( // Prove that the browser's permission button does not appear - navigationBarViewControllerPermissionButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "The permissions button in the browser should not appear when microphone permission has been denied." - ) - } - - func test_microphonePermissions_withAcceptedTCCChallenge_whereAlwaysDenyIsSelected_alwaysDenies() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let microphoneButton = app.webViews.buttons["Microphone"] - microphoneButton.clickAfterExistenceTestSucceeds() - XCTAssert( - notificationCenter.buttons.firstMatch.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "The notification center didn't appear. This can happen because the TCC setting at the start of the test wasn't correct – check the app.resetPermissions behavior." - ) - let allowButtonIndex = try XCTUnwrap(notificationCenter.indexOfSystemModelDialogButtonOnElement( - titled: "Allow", - "OK" - )) // You can add any titles you see this permission dialog using on any tested macOS versions, because it is evaluated by a variadic - // function. - let allowButton = notificationCenter.buttons.element(boundBy: allowButtonIndex) - allowButton.clickAfterExistenceTestSucceeds() // Click system microphone permissions dialog - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - permissionsPopoverAllowButton.clickAfterExistenceTestSucceeds() - var websitePermissionsColorIsGreen = false - for _ in 1 ... 4 { // permission.site updates this color a bit slowly and we have no control over it, so we try a few times. - websitePermissionsColorIsGreen = try websitePermissionsButtonIsExpectedColor(microphoneButton, is: .green) - if websitePermissionsColorIsGreen { - break - } - usleep(500_000) - } - XCTAssertTrue( - websitePermissionsColorIsGreen, - "After a few attempts to wait for permissions.site to update their button animation after the TCC dialog, their button has to be green." - ) - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.microphoneButton"] - navigationBarViewControllerPermissionButton.clickAfterExistenceTestSucceeds() - - let permissionContextMenuAlwaysAsk = app.menuItems["PermissionContextMenu.alwaysAsk"] - XCTAssertTrue( - permissionContextMenuAlwaysAsk.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "permissionContextMenuAlwaysAsk didn't exist in a reasonable timeframe." - ) - let permissionContextMenuAlwaysAskValue = try XCTUnwrap(permissionContextMenuAlwaysAsk.value as? String) - XCTAssertEqual( - permissionContextMenuAlwaysAskValue, - "selected", - "The \"always ask\" menu item of the permission context menu has to be the selected item." - ) - - let permissionContextMenuAlwaysDeny = app.menuItems["PermissionContextMenu.alwaysDeny"] - permissionContextMenuAlwaysDeny.clickAfterExistenceTestSucceeds() - app.enforceSingleWindow() - addressBarTextField.typeURL(permissionsSiteURL) - for _ in 1 ... 4 { - microphoneButton.clickAfterExistenceTestSucceeds() - } - XCTAssertTrue( - try websitePermissionsButtonIsExpectedColor(microphoneButton, is: .red), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the permission button will remain red" - ) - XCTAssert( - notificationCenter.buttons.firstMatch.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the TCC dialog permission alert will not be on the screen" - ) - XCTAssert( - permissionsPopoverAllowButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the permission popover will not be on the screen" - ) - } - - func test_locationPermissions_whenAccepted_showCorrectStateInBrowser() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let locationButton = app.webViews.buttons["Location"] - locationButton.clickAfterExistenceTestSucceeds() - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - permissionsPopoverAllowButton.clickAfterExistenceTestSucceeds() - var websitePermissionsColorIsGreen = false - for _ in 1 ... 4 { // permission.site updates this color a bit slowly and we have no control over it, so we try a few times. - websitePermissionsColorIsGreen = try websitePermissionsButtonIsExpectedColor(locationButton, is: .green) - if websitePermissionsColorIsGreen { - break - } - usleep(500_000) - } - // We would like to be able to test here that the permission.site "Location" button turns green here, but it frequently doesn't turn green - // when location permissions are granted. - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.geolocationButton"] - navigationBarViewControllerPermissionButton.clickAfterExistenceTestSucceeds() - - let permissionContextMenuAlwaysAsk = app.menuItems["PermissionContextMenu.alwaysAsk"] - XCTAssertTrue( - permissionContextMenuAlwaysAsk.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "permissionContextMenuAlwaysAsk didn't exist in a reasonable timeframe." - ) - let permissionContextMenuAlwaysAskValue = try XCTUnwrap(permissionContextMenuAlwaysAsk.value as? String) - XCTAssertEqual( - permissionContextMenuAlwaysAskValue, - "selected", - "The \"always ask\" menu item of the permission context menu has to be the selected item." - ) - } - - func test_locationPermissions_whenAlwaysDenyIsSelected_alwaysDenies() throws { - addressBarTextField.typeURLAfterExistenceTestSucceeds(permissionsSiteURL) - - let locationButton = app.webViews.buttons["Location"] - locationButton.clickAfterExistenceTestSucceeds() - let permissionsPopoverAllowButton = app.popovers.buttons["PermissionAuthorizationViewController.allowButton"] - permissionsPopoverAllowButton.clickAfterExistenceTestSucceeds() - var websitePermissionsColorIsGreen = false - for _ in 1 ... 4 { // permission.site updates this color a bit slowly and we have no control over it, so we try a few times. - websitePermissionsColorIsGreen = try websitePermissionsButtonIsExpectedColor(locationButton, is: .green) - if websitePermissionsColorIsGreen { - break - } - usleep(500_000) - } - - // We would like to be able to test here that the permission.site "Location" button turns green here, but it frequently doesn't turn green - // when location permissions are granted. - - let navigationBarViewControllerPermissionButton = app.buttons["AddressBarButtonsViewController.geolocationButton"] - navigationBarViewControllerPermissionButton.clickAfterExistenceTestSucceeds() - - let permissionContextMenuAlwaysAsk = app.menuItems["PermissionContextMenu.alwaysAsk"] - XCTAssertTrue( - permissionContextMenuAlwaysAsk.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "permissionContextMenuAlwaysAsk didn't exist in a reasonable timeframe." - ) - let permissionContextMenuAlwaysAskValue = try XCTUnwrap(permissionContextMenuAlwaysAsk.value as? String) - XCTAssertEqual( - permissionContextMenuAlwaysAskValue, - "selected", - "The \"always ask\" menu item of the permission context menu has to be the selected item." - ) - - let permissionContextMenuAlwaysDeny = app.menuItems["PermissionContextMenu.alwaysDeny"] - permissionContextMenuAlwaysDeny.clickAfterExistenceTestSucceeds() - app.enforceSingleWindow() - addressBarTextField.typeURL(permissionsSiteURL) - for _ in 1 ... 4 { - locationButton.clickAfterExistenceTestSucceeds() - } - XCTAssertTrue( // It does turn red when permission is denied. - try websitePermissionsButtonIsExpectedColor(locationButton, is: .red), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the permission button will remain red" - ) - - XCTAssert( - permissionsPopoverAllowButton.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), - "Even if we click the button for the denied resource many times, when we are on a site where we have set \"always deny\" for the resource, the permission popover will not be on the screen" - ) - } -} - -private extension PermissionsTests { - func websitePermissionsButtonIsExpectedColor(_ button: XCUIElement, is expectedColor: PredominantColor) throws -> Bool { - let buttonScreenshot = button.screenshot().image - let trimmedButton = buttonScreenshot.trim(to: CGRect( - x: 10, - y: 10, - width: 20, - height: 20 - )) // A sample of the button that we are going to analyze for its predominant color tone. - let predominantColor = try XCTUnwrap( - trimmedButton.ciImage(with: nil).predominantColor(), - "It wasn't possible to unwrap the predominant color of the website button screenshot sample" - ) - return predominantColor == expectedColor - } -} - -private extension XCUIElement { - /// We don't have as much control over what is going to appear on a modal dialogue, and it feels fragile to use Apple's accessibility IDs since I - /// don't think there is any contract for that, but we can plan some flexibility in title matching for the button names, since the button names - /// are in the test description. - /// - Parameter titled: The title or titles (if they vary across macOS versions) of a button whose index on the element we'd like to know, - /// variadic - /// - Returns: An optional Int representing the button index on the element, if a button with this title was found. - func indexOfSystemModelDialogButtonOnElement(titled: String...) -> Int? { - for buttonIndex in 0 ... 4 { // It feels unlikely that a system modal dialog will have more than five buttons - let button = self.buttons.element(boundBy: buttonIndex) - if button.exists, titled.contains(button.title) { - return buttonIndex - } - } - return nil - } -} - -/// Understand whether a webpage button is greenish or reddish when we expect one or the other, or states where we need to retry or fail -private enum PredominantColor { - case red - case green - case neither -} - -private extension NSImage { - /// Trim NSImage to sample - /// - Parameter rect: the sample size to trim to - /// - Returns: The trimmed NSImage - func trim(to rect: CGRect) -> NSImage { - let result = NSImage(size: rect.size) - result.lockFocus() - - let destRect = CGRect(origin: .zero, size: result.size) - self.draw(in: destRect, from: rect, operation: .copy, fraction: 1.0) - - result.unlockFocus() - return result - } -} - -private extension CIImage { - /// Evaluate a sample of a webpage button to see what its predominant color tone is. Assumes it is being run on a button that is expected to be - /// either green or red (otherwise we are starting to think into `https://permission.site`'s potential implementation errors or surprise cases, - /// which I don't think should be part of this test case scope which tests UIs from three responsible organizations in which the tested UIs, in - /// order of importance, should be: this browser, macOS, permission.site)). - /// - Returns: .red, .green, .neither if we get a result but it isn't helpful, or nil in the event of an error (but it will always verbosely fail - /// the test before returning nil, so in practice, if the test is still in progress, it has returned a case.) - func predominantColor() throws -> PredominantColor? { - var redValueOfSample = 0.0 - var greenValueOfSample = 0.0 - - for channel in 0 ... 1 { // We are only checking the first two channels - let extentVector = CIVector( - x: self.extent.origin.x, - y: self.extent.origin.y, - z: self.extent.size.width, - w: self.extent.size.height - ) - - guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: self, kCIInputExtentKey: extentVector]) - else { XCTFail("It wasn't possible to set the CIFilter for the predominant color channel check") - return nil - } - guard let outputImage = filter.outputImage - else { XCTFail("It wasn't possible to set the output image for the predominant color channel check") - return nil - } - - var outputBitmap = [UInt8](repeating: 0, count: 4) - let nullSingletonInstance = try XCTUnwrap(kCFNull, "Could not unwrap singleton null instance") - let outputRenderContext = CIContext(options: [.workingColorSpace: nullSingletonInstance]) - outputRenderContext.render( - outputImage, - toBitmap: &outputBitmap, - rowBytes: 4, - bounds: CGRect(x: 0, y: 0, width: 1, height: 1), - format: .RGBA8, - colorSpace: nil - ) - if channel == 0 { - redValueOfSample = Double(outputBitmap[channel]) / Double(255) - } else if channel == 1 { - greenValueOfSample = Double(outputBitmap[channel]) / Double(255) - } - } - - let tooSimilar = abs(redValueOfSample - greenValueOfSample) < 0.05 // This isn't a huge difference because these are both very light colors - if tooSimilar { - print( - "It wasn't possible to get a predominant color of the button because the two channel values of red (\(redValueOfSample)) and green (\(greenValueOfSample)) were \(redValueOfSample == greenValueOfSample ? "the same." : "too close in value.")" - ) - return .neither - } - - return max(redValueOfSample, greenValueOfSample) == redValueOfSample ? .red : .green - } -} diff --git a/macOS/UITests/PopupHandlingUITests.swift b/macOS/UITests/PopupHandlingUITests.swift deleted file mode 100644 index 47137969e8c..00000000000 --- a/macOS/UITests/PopupHandlingUITests.swift +++ /dev/null @@ -1,1011 +0,0 @@ -// -// PopupHandlingUITests.swift -// -// Copyright © 2025 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -final class PopupHandlingUITests: UITestCase { - - // MARK: - Constants - - /// User-initiated popup timeout threshold (reduced to 2s for faster testing) - /// Production default is 6s, but tests override to 2s in setUp - private enum PopupTimeout { - static let testingThreshold: TimeInterval = 2.0 - } - - private enum AccessibilityIdentifiers { - static let popupsButton = "AddressBarButtonsViewController.popupsButton" - static let alwaysAllow = "PermissionContextMenu.alwaysAllow" - static let alwaysAsk = "PermissionContextMenu.alwaysAsk" // "Notify" for popups - static let allowPopupsForPage = "PermissionContextMenu.allowPopupsForPage" // "Only allow pop-ups for this visit" - } - - private var addressBarTextField: XCUIElement! - private var serviceNowURL: URL! - private var popupDelayedURL: URL! - private var popupLinksURL: URL! - - override func setUpWithError() throws { - try super.setUpWithError() - continueAfterFailure = false - - // Enable all popup blocking features and set reduced timeout for faster testing - app = XCUIApplication.setUp( - environment: [ - "POPUP_TIMEOUT_OVERRIDE": String(PopupTimeout.testingThreshold) // Reduce from 6s to 2s for faster tests - ], - featureFlags: [ - "newPermissionView": false, // Disabled until UI tests can handle the new permission view - "popupBlocking": true, - ] - ) - - // Load test HTML files from HTTP test server (not file:// URLs) - serviceNowURL = URL.testsServer.appendingPathComponent("popup-servicenow.html") - popupDelayedURL = URL.testsServer.appendingPathComponent("popup-delayed.html") - popupLinksURL = URL.testsServer.appendingPathComponent("popup-links.html") - - addressBarTextField = app.addressBar - app.enforceSingleWindow() - } - - override func tearDown() { - // Burn all data to clear permissions between tests - app.fireButton.click() - app.fireDialogSegmentedControl.buttons["Everything"].click() - app.fireDialogTabsToggle.toggleCheckboxIfNeeded(to: true, ensureHittable: { _ in }) - app.fireDialogHistoryToggle.toggleCheckboxIfNeeded(to: true, ensureHittable: { _ in }) - app.fireDialogCookiesToggle.toggleCheckboxIfNeeded(to: true, ensureHittable: { _ in }) - app.fireDialogBurnButton.click() - - // Wait for fire animation to complete - _ = app.fakeFireButton.waitForNonExistence(timeout: UITests.Timeouts.fireAnimation) - - super.tearDown() - } - - // MARK: - Tests - - // MARK: User-Initiated Popup Behavior - - /// Tests that user-initiated popups (triggered immediately on button click) open without requiring permission - func testUserClickOpensPopupsWithoutPermission() throws { - // Load test page - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Click button (user-initiated) - should open without permission - let button = webView.links["Open Popup (Immediate)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Wait for popup window to appear and verify content - let popupWindow = app.windows["Immediate Popup"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should open without permission prompt" - ) - - // Verify popup content loaded correctly - let popupContent = popupWindow.staticTexts["User-Initiated Popup Opened"] - XCTAssertTrue( - popupContent.exists, - "Popup content should be visible, verifying no breakage" - ) - - // Verify no popup blocked button appears - verifyNoPopupBlockedButton() - } - - /// Tests that popups delayed within the user interaction timeout window (1s < 2s threshold) are allowed without permission - func testExtendedTimeoutWindowAllowsDelayedPopups() throws { - // Load test page - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Click on page to establish user interaction - webView.click() - - // Click button for popup within timeout window - let button = webView.links["Open Popup (Within Timeout)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Wait for popup window to appear (should open within threshold) - let popupWindow = app.windows["Delayed User Popup"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should open without permission within \(PopupTimeout.testingThreshold)s timeout window" - ) - - // Verify popup content loaded correctly - let popupContent = popupWindow.staticTexts["Delayed User-Initiated Popup"] - XCTAssertTrue( - popupContent.exists, - "Popup content should be visible, verifying no breakage" - ) - - // Verify no permission prompt appeared - verifyNoPopupBlockedButton() - } - - /// Tests that popups delayed beyond the timeout (3s > 2s threshold) are blocked and must be manually opened from the menu - /// Verifies both empty (about:blank) and cross-domain popups, where empty URLs don't appear in the menu - func testExpiredTimeoutRequiresPermissionAndManualOpen() throws { - // Load test page - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // First: Trigger blocked about:blank popup (3s delay) - let blankButton = webView.links["Trigger Blocked about:blank"] - XCTAssertTrue(blankButton.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - blankButton.click() - - // Validate first popup blocked button appears (wait for 3s delay + block to happen) - // about:blank blocked - empty URLs don't appear in menu - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0, timeout: UITests.Timeouts.elementExistence) - - // Second: Click delayed cross-domain popup button (also 3s delay) - let beyondButton = webView.links["Open Popup (Beyond Timeout)"] - XCTAssertTrue(beyondButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - beyondButton.click() - - // Wait for status to change again (indicating second popup was blocked) - let secondBlockedStatus = webView.staticTexts["Blocked"] - XCTAssertTrue( - secondBlockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Status should show second popup was blocked" - ) - - // Verify 2 blocked popups (1 about:blank + 1 example.com) - // Only example.com should have a menu item (about:blank suppressed) - verifyPopupBlockedButton(count: 2, expectedOpenItems: 1, closeMenu: false) - - // Choose example.com (cross-domain popup) from the menu - let blockedPopupMenuItem = app.menuItems.containing(\.title, containing: "example.com").firstMatch - XCTAssertTrue( - blockedPopupMenuItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Blocked cross-domain popup (example.com) should appear in menu" - ) - blockedPopupMenuItem.click() - - // Validate popup opened with content - let popupWindow = app.windows["Example Domain"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.navigation), - "Cross-domain popup should open when selected from menu" - ) - - // Verify popup navigated to cross-domain URL and loaded content - let popupWebView = popupWindow.webViews["Example Domain"] - XCTAssertTrue( - popupWebView.waitForExistence(timeout: UITests.Timeouts.navigation), - "Cross-domain popup content should load successfully" - ) - - // Verify window count: main + manually opened example.com (about:blank was blocked, not opened) - XCTAssertEqual(app.windows.count, 2, "Should have main window + manually opened popup") - } - - // MARK: Permission Persistence Across Tabs - - /// Tests that popup permissions ("Always Allow", "Notify") persist across tabs and app restarts - /// Verifies that the popup blocked button remains visible (per PR #2641) and shows the correct permission state - func testAlwaysAllowPersistsAcrossTabs() throws { - // Tab 1: Load page and trigger blocked popup (beyond timeout) - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - let button = webView.links["Open Popup (Beyond Timeout)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1, closeMenu: false) - - // Select "Always Allow" - this immediately opens the blocked popup - app.menuItems[AccessibilityIdentifiers.alwaysAllow].click() - - // Verify the blocked popup opened immediately after granting permission - let popupWindow = app.windows["Example Domain"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.navigation), - "Blocked popup should open immediately after 'Always Allow'" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened") - - // Close it - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - // Tab 2: Open new tab and load same page - app.openNewTab() - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - - // Verify button is NOT visible initially in new tab (no blocked popups yet) - verifyNoPopupBlockedButton() - - // Click button - popup should now open (permission already granted) - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Wait for popup window to open - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should open (permission already granted)" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened – 2") - - // Close popup window so it doesn't cover the popup blocked button - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - // The popup button from Tab 1 should still be visible (button persists per PR #2641) - // Open its menu to change permission back to Notify - verifyPopupBlockedButton(count: 0, closeMenu: false) - - // Verify "Always Allow" is checked (persisted from Tab 1) - let alwaysAllowMenuItem = app.menuItems[AccessibilityIdentifiers.alwaysAllow] - XCTAssertEqual(alwaysAllowMenuItem.value as? String, "selected", "Always Allow should be selected in Tab 2") - - // Change to "Notify" - app.menuItems[AccessibilityIdentifiers.alwaysAsk].click() - - // Try opening another popup in Tab 2 - should be blocked now - button.click() - - let blockedStatus = webView.staticTexts.containing(\.value, containing: "Blocked").firstMatch - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked in Tab 2 after changing to Notify" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1) - try app.closeTab() - - // Open Tab 3 and verify Notify persists across tabs - app.openNewTab() - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Wait for popup to be blocked - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked (Notify permission persisted across tabs)" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1, closeMenu: false) - - // Verify "Notify" is checked (persisted from Tab 2) - let notifyMenuItem = app.menuItems[AccessibilityIdentifiers.alwaysAsk] - XCTAssertEqual(notifyMenuItem.value as? String, "selected", "Notify should be selected in Tab 3") - - // Set "Always Allow" again - this immediately opens the blocked popup - app.menuItems[AccessibilityIdentifiers.alwaysAllow].click() - - // Verify the blocked popup opened immediately - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Blocked popup should open immediately after 'Always Allow'" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened – 3") - - // Close it before restart - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - // Restart app: Verify permission persists after restart - app.terminate() - app.launch() - - addressBarTextField = app.addressBar - app.enforceSingleWindow() - - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Wait for popup window to open again - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should still open (permission persisted after restart)" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened – 4") - - // Close popup and verify permission menu still accessible - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - verifyPopupBlockedButton(count: 0, closeMenu: false) - - // Verify "Always Allow" is still checked after restart - let alwaysAllowAfterRestart = app.menuItems[AccessibilityIdentifiers.alwaysAllow] - XCTAssertEqual(alwaysAllowAfterRestart.value as? String, "selected", "Always Allow should still be selected after restart") - - // Close menu - app.typeKey(.escape, modifierFlags: []) - } - - // MARK: Popup Button State - - /// Tests that the popup blocked button appears only for page-initiated blocked popups, not for user-initiated actions - /// Validates page-initiated popups (where first consumes interaction, second is blocked) vs Cmd+click link navigation - func testPopupButtonAppearsOnlyForPageInitiatedPopups() throws { - // Load page and trigger page-initiated popup (first consumes interaction, second blocked) - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Trigger page popup: opens 2 popups (first allowed, second blocked) - let button = webView.links["Trigger 2 Page Popups (1st Allowed, 2nd Blocked)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Verify first popup opened - let popupWindow = app.windows["First Popup Allowed"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "First popup should exist" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened (main + 1)") - - let popupContent = popupWindow.staticTexts["First Popup Allowed"] - XCTAssertTrue( - popupContent.exists, - "First popup content should be visible, verifying no breakage" - ) - - // Close popup window before navigating - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - // Only 1 popup should be blocked (first one consumes user interaction) - // Blocked popup is about:blank - empty URLs don't appear in menu - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0) - - // Now test user-initiated popup - app.activateAddressBar() - app.openURL(popupLinksURL, waitForWebViewAccessibilityLabel: "Popup Links Test") - let linksWebView = app.webViews["Popup Links Test"] - - // ⌘-click link (user-initiated) - let link = linksWebView.links["Normal Link to Example"] - XCUIElement.perform(withKeyModifiers: [.command]) { - link.click() - } - - // Wait for tab to open - let newTab = app.tabs["Example Domain"] - XCTAssertTrue( - newTab.waitForExistence(timeout: UITests.Timeouts.navigation), - "New tab should open" - ) - - // Button should NOT appear for user-initiated - verifyNoPopupBlockedButton(webViewTitle: "Popup Links Test") - } - - /// Tests that the popup blocked button correctly accumulates and displays the count of multiple blocked popups - func testPopupButtonCountAccumulatesMultipleBlockedPopups() throws { - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Trigger 6 popups: first opens (consumes interaction), remaining 5 blocked - let button = webView.links["Trigger 6 Page Popups (1st Allowed, 5 Blocked)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Verify first popup opened - let popupWindow = app.windows["First of Multiple"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "First popup should exist" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened (main + 1)") - - let popupContent = popupWindow.staticTexts["First Popup Allowed"] - XCTAssertTrue( - popupContent.exists, - "First popup content should be visible, verifying no breakage" - ) - - // Close popup window - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - // Verify button shows count of 5 blocked (first popup consumes interaction) - // All 5 are empty URLs - don't appear in menu - verifyPopupBlockedButton(count: 5, expectedOpenItems: 0) - } - - /// Tests that the popup blocked button clears when navigating to a different page - func testPopupButtonClearsOnNavigation() throws { - // Block popup and verify button appears - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - let button = webView.links["Open Popup (Beyond Timeout)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - let blockedStatus = webView.staticTexts.containing(\.value, containing: "Blocked").firstMatch - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1) - - // Navigate to different page - app.activateAddressBar() - app.openURL(URL(string: "https://example.com")!, waitForWebViewAccessibilityLabel: "Example Domain") - - // Button should clear - verifyNoPopupBlockedButton(webViewTitle: "Example Domain") - } - - // MARK: Empty/About:blank URL Suppression - - /// Tests that empty/about:blank blocked popups don't appear in the menu and are suppressed from the blocked list - /// Verifies that after "Always Allow", empty popups are suppressed while normal popups open correctly - func testEmptyURLsSuppressedAfterPermissionApproval() throws { - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Click button that opens about:blank (blocked because beyond timeout) - let blockedButton = webView.links["Trigger Blocked about:blank"] - XCTAssertTrue(blockedButton.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - blockedButton.click() - - // about:blank blocked - empty URLs don't appear in menu - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0, closeMenu: false) - - // Grant "Always Allow" permission (shouldn't open empty popup since empty URLs are suppressed) - app.menuItems[AccessibilityIdentifiers.alwaysAllow].click() - - // Verify no popup opened (empty URL suppressed) - XCTAssertEqual(app.windows.count, 1, "Empty popup should be suppressed") - - // Click button that opens about:blank with "Expected to be opened" - should be allowed - let expectedButton = webView.links["Open about:blank (Expected to be opened)"] - expectedButton.click() - - // Popup should open (permission granted) - let expectedPopup = app.windows["Expected Popup"] - XCTAssertTrue( - expectedPopup.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "about:blank popup should open (permission granted)" - ) - XCTAssertEqual(app.windows.count, 2, "Exactly one popup should have opened") - - // Close popup - expectedPopup.buttons[XCUIIdentifierCloseWindow].click() - - // Popups allowed - blocked count cleared - verifyPopupBlockedButton(count: 0, expectedOpenItems: 0) - } - - /// Tests that temporary popup allowance ("Only allow pop-ups for this visit") clears on reload, navigation, and back/forward navigation - /// Verifies allowance is cleared on: page reload, forward/back navigation, and navigating to a different page - func testTemporaryAllowanceClearsOnNavigation() throws { - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - let button = webView.links["Trigger Blocked about:blank"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0, closeMenu: false) - - // Select "Only allow pop-ups for this visit" (temporary) - app.menuItems[AccessibilityIdentifiers.allowPopupsForPage].click() - - // Click button that opens popup that should be allowed - let expectedButton = webView.links["Open about:blank (Expected to be opened)"] - expectedButton.click() - - let popupWindow = app.windows["Expected Popup"] - XCTAssertTrue( - popupWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "about:blank popup should open (temporary allowance active)" - ) - - // Close popup - popupWindow.buttons[XCUIIdentifierCloseWindow].click() - - // Reload page - temporary allowance should be cleared - app.reloadButton.click() - - // Click again after reload - popup should be blocked (temporary allowance cleared on reload) - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - let blockedStatus = webView.staticTexts.containing(\.value, containing: "Blocked").firstMatch - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked (temporary allowance cleared after reload)" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0) - - // Navigate away - app.activateAddressBar() - app.openURL(URL(string: "https://example.com")!, waitForWebViewAccessibilityLabel: "Example Domain") - - // Navigate back - app.activateAddressBar() - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - - // Popup should be blocked again (temporary allowance cleared on navigation) - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked (temporary allowance cleared)" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0) - - // Navigate back twice (double-back) - app.backButton.click() - app.backButton.click() - - // Click after double-back - should still be blocked - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked after double-back navigation" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0) - - // Navigate forward twice (double-forward) - app.forwardButton.click() - app.forwardButton.click() - - // Click after double-forward - should still be blocked - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked after double-forward navigation" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 0) - } - - // MARK: Popup Without Window Features (Opens as Tab) - - /// Tests that blocked popups opened via window.open() without window features (width/height) - /// are properly blocked and, when manually opened from the menu, open as tabs instead of popup windows - func testDelayedPopupWithoutWindowFeaturesOpensAsTab() throws { - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Click button that opens popup without window features (should open as tab, not window) - let button = webView.links["Open Popup as Tab (Beyond Timeout)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - let blockedStatus = webView.staticTexts.containing(\.value, containing: "Blocked").firstMatch - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup without window features should be blocked" - ) - - // Verify blocked popup appears in menu (cross-domain) - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1, closeMenu: false) - - // Open the blocked popup - let blockedPopupMenuItem = app.menuItems.containing(\.title, containing: "example.com").firstMatch - XCTAssertTrue( - blockedPopupMenuItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Blocked popup should appear in menu" - ) - blockedPopupMenuItem.click() - - // Verify it opened as a tab, not a window - let newTab = app.tabs["Example Domain"] - XCTAssertTrue( - newTab.waitForExistence(timeout: UITests.Timeouts.navigation), - "Popup without window features should open as tab" - ) - - XCTAssertEqual(app.windows.count, 1, "Should have only 1 window (no popup window created)") - XCTAssertEqual(app.tabs.count, 2, "Should have 2 tabs (original + opened)") - } - - // MARK: Multiple Cross-Domain Blocked Popups - - /// Tests that multiple blocked popups from different cross-domain sources (example.com, duckduckgo.com) - /// are tracked separately, appear as individual menu items, can be opened independently, - /// and the blocked count decrements properly until the menu is empty - func testMultipleCrossDomainBlockedPopups() throws { - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView = app.webViews["Popup Delayed Test"] - - // Block first popup (example.com) - let exampleButton = webView.links["Open Popup (Beyond Timeout)"] - XCTAssertTrue(exampleButton.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - exampleButton.click() - - let blockedStatus = webView.staticTexts.containing(\.value, containing: "Blocked").firstMatch - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "First popup (example.com) should be blocked" - ) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1) - - // Block second popup (duckduckgo.com) - let duckduckgoButton = webView.links["Open Popup Alt Domain (Beyond Timeout)"] - XCTAssertTrue(duckduckgoButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - duckduckgoButton.click() - - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Second popup (duckduckgo.com) should be blocked" - ) - - // Verify 2 blocked popups, both with menu items - verifyPopupBlockedButton(count: 2, expectedOpenItems: 2, closeMenu: false) - - // Open first blocked popup (example.com) - let exampleComItem = app.menuItems.containing(\.title, containing: "example.com").firstMatch - XCTAssertTrue( - exampleComItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "example.com menu item should exist" - ) - exampleComItem.click() - - let examplePopup = app.windows["Example Domain"] - XCTAssertTrue( - examplePopup.waitForExistence(timeout: UITests.Timeouts.navigation), - "example.com popup should open" - ) - XCTAssertEqual(app.windows.count, 2, "Should have main window + 1 popup") - - // Close example.com popup - examplePopup.buttons[XCUIIdentifierCloseWindow].click() - - // Verify count decremented to 1 - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1, closeMenu: false) - - // Open second blocked popup (duckduckgo.com) - let duckduckgoItem = app.menuItems.containing(\.title, containing: "duckduckgo.com").firstMatch - XCTAssertTrue( - duckduckgoItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "duckduckgo.com menu item should exist" - ) - duckduckgoItem.click() - - let duckduckgoPopup = app.windows.element(matching: \.title, containing: "DuckDuckGo") - XCTAssertTrue( - duckduckgoPopup.waitForExistence(timeout: UITests.Timeouts.navigation), - "duckduckgo.com popup should open" - ) - XCTAssertEqual(app.windows.count, 2, "Should have main window + 1 popup") - - // Close duckduckgo.com popup - duckduckgoPopup.buttons[XCUIIdentifierCloseWindow].click() - - // Verify button shows count 0 with no menu items (both popups opened) - verifyPopupBlockedButton(count: 0, expectedOpenItems: 0) - } - - // MARK: Multiple Windows with Blocked Popups - - /// Tests that blocked popup state is maintained independently per window: - /// - Blocked popups in one window don't affect other windows - /// - Opening a blocked popup in one window doesn't affect the other window's blocked list - /// - Each window has its own popup blocked button with its own state - func testBlockedPopupsInMultipleWindows() throws { - // Window 1: Block popup - addressBarTextField.pasteURL(popupDelayedURL, pressingEnter: true) - let webView1 = app.webViews["Popup Delayed Test"] - - let button = webView1.links["Open Popup (Beyond Timeout)"] - XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button.click() - - // Open second window immediately after clicking (while popup is being delayed) - app.openNewWindow() - - // Wait for window count to become 2 - XCTAssertTrue( - app.windows.wait(for: \.count, equals: 2, timeout: UITests.Timeouts.elementExistence), - "Window count should become 2" - ) - - let blockedStatus = webView1.staticTexts.containing(\.value, containing: "Blocked").firstMatch - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked in window 1" - ) - - // Verify no button in the new (second) window yet - verifyNoPopupBlockedButton(webViewTitle: "New Tab") - - // Focus back to the first window - app.typeKey("`", modifierFlags: [.command]) - - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1, closeMenu: false) - - // Open the blocked popup - let exampleComItem = app.menuItems.containing(\.title, containing: "example.com").firstMatch - exampleComItem.click() - - let popup = app.windows.element(matching: \.title, containing: "Example Domain") - XCTAssertTrue( - popup.waitForExistence(timeout: UITests.Timeouts.navigation), - "Popup should open" - ) - - // Close popup - popup.buttons[XCUIIdentifierCloseWindow].click() - - // Verify button state changed in window 1 (count 0) - verifyPopupBlockedButton(count: 0, expectedOpenItems: 0) - - // Switch to window 2 and verify no button there - app.typeKey("`", modifierFlags: [.command]) - verifyNoPopupBlockedButton(webViewTitle: "New Tab") - - // Window 2: Load same page and block popup - let addressBar2 = app.addressBar - addressBar2.pasteURL(popupDelayedURL, pressingEnter: true) - let webView2 = app.webViews["Popup Delayed Test"].firstMatch - - let button2 = webView2.links["Open Popup (Beyond Timeout)"] - XCTAssertTrue(button2.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - button2.click() - - XCTAssertTrue( - blockedStatus.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "Popup should be blocked in window 2" - ) - - // Verify button in window 2 shows blocked popup - verifyPopupBlockedButton(count: 1, expectedOpenItems: 1) - - // Switch back to window 1 and verify its button still shows blocked popup - app.typeKey("`", modifierFlags: [.command]) - verifyPopupBlockedButton(count: 0, expectedOpenItems: 0) - } - - // MARK: ServiceNow Scenarios - - /// Tests ServiceNow-like scenario where Cmd+click on a link that also triggers window.open() calls - /// Verifies that only one tab opens (event consumption prevents extra tabs) and extra window.open() calls are blocked - func testServiceNowCommandClickOnlyOpensOneTab() throws { - addressBarTextField.pasteURL(serviceNowURL, pressingEnter: true) - let webView = app.webViews["Simulate ServiceNow Cmd+Click Bug"] - - // ⌘-click first link (with extra tabs) - let link = webView.links["Incident INC001 (with extra tabs)"] - XCTAssertTrue(link.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - - XCUIElement.perform(withKeyModifiers: [.command]) { - link.click() - } - - // Wait for new tab - let newTab = app.tabs["Example Domain"] - XCTAssertTrue( - newTab.waitForExistence(timeout: UITests.Timeouts.navigation), - "Only one new tab should open" - ) - - // Verify only 2 tabs total (original + 1 new) - XCTAssertEqual( - app.tabs.count, - 2, - "Should have exactly 2 tabs (original + 1 new), event consumption prevented extra tabs" - ) - - // Verify popup button shows 2 blocked (the 2 window.open() calls) - // Both are empty URLs - don't appear in menu - verifyPopupBlockedButton(count: 2, webViewTitle: "Simulate ServiceNow Cmd+Click Bug", expectedOpenItems: 0) - } - - /// Tests that middle-clicking a ServiceNow-like link bypasses the JavaScript handler entirely, - /// opening only the intended tab without triggering or blocking any window.open() calls - func testServiceNowMiddleClickOnlyOpensOneTab() throws { - guard #available(macOS 15, *) else { throw XCTSkip("WebKit issue fixed in macOS 15, matches Safari behavior") } - - addressBarTextField.pasteURL(serviceNowURL, pressingEnter: true) - let webView = app.webViews["Simulate ServiceNow Cmd+Click Bug"] - - // Middle-click first link (bypasses JavaScript handler) - let link = webView.links["Incident INC001 (with extra tabs)"] - XCTAssertTrue(link.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - link.middleClick() - - // Wait for new tab - let newTab = app.tabs["Example Domain"] - XCTAssertTrue( - newTab.waitForExistence(timeout: UITests.Timeouts.navigation), - "New tab did not open" - ) - - // Verify only 2 tabs total - XCTAssertEqual( - app.tabs.count, - 2, - "Should have exactly 2 tabs" - ) - - // Middle-click bypasses JavaScript, so no popups are triggered - verifyNoPopupBlockedButton(webViewTitle: "Simulate ServiceNow Cmd+Click Bug") - } - - /// Tests that using "Open Link in New Tab" from the context menu bypasses the JavaScript handler, - /// opening only the intended tab without triggering or blocking any window.open() calls - func testServiceNowContextMenuOpenInNewTab() throws { - addressBarTextField.pasteURL(serviceNowURL, pressingEnter: true) - let webView = app.webViews["Simulate ServiceNow Cmd+Click Bug"] - - // Right-click and select "Open Link in New Tab" (bypasses JavaScript handler) - let link = webView.links["Incident INC001 (with extra tabs)"] - XCTAssertTrue(link.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - link.rightClick() - - app.menuItems["Open Link in New Tab"].click() - - // Wait for new tab - let newTab = app.tabs["Example Domain"] - XCTAssertTrue( - newTab.waitForExistence(timeout: UITests.Timeouts.navigation), - "Only one new tab should open" - ) - - // Verify only 2 tabs total - XCTAssertEqual( - app.tabs.count, - 2, - "Should have exactly 2 tabs" - ) - - // Context menu bypasses JavaScript, so no popups are triggered - verifyNoPopupBlockedButton(webViewTitle: "Simulate ServiceNow Cmd+Click Bug") - } - - /// Tests that Cmd+clicking a clean link (without extra window.open() calls) opens normally without any blocked popups - func testServiceNowCleanLinkOpensNormally() throws { - addressBarTextField.pasteURL(serviceNowURL, pressingEnter: true) - let webView = app.webViews["Simulate ServiceNow Cmd+Click Bug"] - - // ⌘-click clean link (no extra window.open() calls) - let link = webView.links["Incident INC002 (clean)"] - XCTAssertTrue(link.waitForExistence(timeout: UITests.Timeouts.localTestServer)) - - XCUIElement.perform(withKeyModifiers: [.command]) { - link.click() - } - - // Wait for new tab - let newTab = app.tabs["Example Domain"] - XCTAssertTrue( - newTab.waitForExistence(timeout: UITests.Timeouts.navigation), - "New tab should open normally" - ) - - // Verify 2 tabs total - XCTAssertEqual(app.tabs.count, 2, "Should have 2 tabs") - - // No popup button should appear (clean link has no extra popups) - verifyNoPopupBlockedButton(webViewTitle: "Simulate ServiceNow Cmd+Click Bug") - } - - // MARK: - Helpers - - private func mainWindow(titled webViewTitle: String) -> XCUIElement { - // Find main window by its webview content (test page title) - // Popup windows don't have these test pages - app.windows.element(matching: \.title, equalTo: webViewTitle).firstMatch - } - - private func popupBlockedButton(inWindowWith webViewTitle: String = "Popup Delayed Test") -> XCUIElement { - // Get button from main window with specified webview title - let mainWindow = mainWindow(titled: webViewTitle) - XCTAssertTrue( - mainWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence), - "mainWindow should be available" - ) - - return mainWindow.buttons[AccessibilityIdentifiers.popupsButton] - } - - private func verifyPopupBlockedButton(count: Int, webViewTitle: String = "Popup Delayed Test", expectedOpenItems: Int? = nil, timeout: TimeInterval = UITests.Timeouts.elementExistence, closeMenu: Bool = true, file: StaticString = #file, line: UInt = #line) { - let button = popupBlockedButton(inWindowWith: webViewTitle) - XCTAssertTrue( - button.waitForExistence(timeout: timeout), - "Popup blocked button should appear", - file: file, - line: line - ) - - // Wait for the "Pop-Up Blocked" to disappear - let blockedPopover = app.popovers.containing(.staticText, where: .keyPath(\.value, equalTo: "Pop-Up Blocked")).element - _=blockedPopover.waitForExistence(timeout: 1) // give some time for the popover to appear - XCTAssertTrue(blockedPopover.waitForNonExistence(timeout: UITests.Timeouts.elementExistence)) - // wait for the animation to complete - sleep(2) - - // Click button to open context menu and verify count - button.click() - - // Get the context menu - let contextMenu = button.children(matching: .menu).firstMatch - - // Only check for "Blocked X pop-ups" header if count > 0 - if count > 0 { - let expectedText = "Blocked \(count) pop-up" - let menuItem = contextMenu.menuItems.containing(\.title, containing: expectedText).firstMatch - XCTAssertTrue( - menuItem.exists, - "Menu should show '\(expectedText)s', but menu item not found", - file: file, - line: line - ) - - // Verify the number of "Open..." menu items if specified - if let expectedOpenItems = expectedOpenItems { - // Count menu items for blocked popups (empty/about:blank are suppressed and won't appear) - let openItems = contextMenu.menuItems.containing( - .keyPath(\.isEnabled, equalTo: true) - .and(.or(.keyPath(\.title, contains: "\""), - .keyPath(\.title, contains: "“"), - .keyPath(\.title, contains: "about:blank") - )) - ).allElementsBoundByIndex - - XCTAssertEqual( - openItems.count, - expectedOpenItems, - "Should have exactly \(expectedOpenItems) 'Open...' menu item(s) for blocked popups, but found \(openItems)", - file: file, - line: line - ) - } - } else { - // When count is 0, verify no "Blocked" header - let blockedHeader = contextMenu.menuItems.containing(\.title, containing: "Blocked").firstMatch - XCTAssertFalse( - blockedHeader.exists, - "Should not show 'Blocked X pop-ups' header when count is 0", - file: file, - line: line - ) - } - - // Close menu by pressing Escape (unless told to keep it open) - if closeMenu { - app.typeKey(.escape, modifierFlags: []) - } - } - - private func verifyNoPopupBlockedButton(webViewTitle: String = "Popup Delayed Test", file: StaticString = #file, line: UInt = #line) { - XCTAssertFalse( - popupBlockedButton(inWindowWith: webViewTitle).exists, - "Popup blocked button should not appear for user-initiated popups", - file: file, - line: line - ) - } - -} diff --git a/macOS/UITests/UI Tests.xctestplan b/macOS/UITests/UI Tests.xctestplan index abaa54a793c..9d578d2405c 100644 --- a/macOS/UITests/UI Tests.xctestplan +++ b/macOS/UITests/UI Tests.xctestplan @@ -41,8 +41,7 @@ "BookmarkSearchTests\/testShowInFolderFunctionalityOnBookmarksPanel()", "FireWindowTests\/testCrendentialsAreAutoFilledInFireWindows()", "NewPermissionViewTests\/test_locationPermissions_whenAccepted_showCorrectStateInBrowser()", - "NewPermissionViewTests\/test_locationPermissions_whenNeverAllowIsSelected_alwaysDenies()", - "PermissionsTests" + "NewPermissionViewTests\/test_locationPermissions_whenNeverAllowIsSelected_alwaysDenies()" ], "target" : { "containerPath" : "container:DuckDuckGo-macOS.xcodeproj", diff --git a/macOS/UnitTests/Permissions/PermissionManagerTests.swift b/macOS/UnitTests/Permissions/PermissionManagerTests.swift index a574106b2f1..64473d6e4da 100644 --- a/macOS/UnitTests/Permissions/PermissionManagerTests.swift +++ b/macOS/UnitTests/Permissions/PermissionManagerTests.swift @@ -23,20 +23,17 @@ import XCTest final class PermissionManagerTests: XCTestCase { var store: PermissionStoreMock! - var featureFlagger: MockFeatureFlagger! lazy var manager: PermissionManager! = { - PermissionManager(store: store, featureFlagger: featureFlagger) + PermissionManager(store: store) }() override func setUp() { store = PermissionStoreMock() - featureFlagger = MockFeatureFlagger() } override func tearDown() { manager = nil store = nil - featureFlagger = nil } func testWhenPermissionManagerInitializedThenPermissionsAreLoaded() { diff --git a/macOS/UnitTests/Permissions/PermissionModelTests.swift b/macOS/UnitTests/Permissions/PermissionModelTests.swift index dadd9db72a4..3708a4ba5f4 100644 --- a/macOS/UnitTests/Permissions/PermissionModelTests.swift +++ b/macOS/UnitTests/Permissions/PermissionModelTests.swift @@ -19,7 +19,7 @@ import AVFoundation import Combine import CommonObjCExtensions -import FeatureFlags + import Foundation import OSLog import PrivacyConfig @@ -36,7 +36,6 @@ final class PermissionModelTests: XCTestCase { var geolocationServiceMock: GeolocationServiceMock! var geolocationProviderMock: GeolocationProviderMock! var systemPermissionManagerMock: SystemPermissionManagerMock! - var featureFlaggerMock: MockFeatureFlagger! static var processPool: WKProcessPool! var webView: WebViewMock! var model: PermissionModel! @@ -65,7 +64,6 @@ final class PermissionModelTests: XCTestCase { permissionManagerMock = PermissionManagerMock() geolocationServiceMock = GeolocationServiceMock() systemPermissionManagerMock = SystemPermissionManagerMock() - featureFlaggerMock = MockFeatureFlagger() let configuration = WKWebViewConfiguration(processPool: Self.processPool) webView = WebViewMock(frame: NSRect(x: 0, y: 0, width: 50, height: 50), configuration: configuration) @@ -76,8 +74,7 @@ final class PermissionModelTests: XCTestCase { model = PermissionModel(webView: webView, permissionManager: permissionManagerMock, geolocationService: geolocationServiceMock, - systemPermissionManager: systemPermissionManagerMock, - featureFlagger: featureFlaggerMock) + systemPermissionManager: systemPermissionManagerMock) AVCaptureDeviceMock.authorizationStatuses = nil } @@ -88,7 +85,6 @@ final class PermissionModelTests: XCTestCase { permissionManagerMock = nil geolocationServiceMock = nil systemPermissionManagerMock = nil - featureFlaggerMock = nil pixelKit = nil geolocationProviderMock = nil model = nil @@ -149,12 +145,13 @@ final class PermissionModelTests: XCTestCase { .camera: .inactive]) } - func testWhenLocationIsDeactivatedThenStateChangesToInactive() { + func testWhenLocationIsDeactivatedThenStateStaysActive() { geolocationServiceMock.authorizationStatus = .authorized geolocationProviderMock.isActive = true geolocationProviderMock.isActive = false - XCTAssertEqual(model.permissions, [.geolocation: .inactive]) + // Geolocation stays .active once granted/used (for permission center visibility) + XCTAssertEqual(model.permissions, [.geolocation: .active]) } func testWhenPermissionIsQueriedThenQueryIsPublished() { @@ -344,7 +341,8 @@ final class PermissionModelTests: XCTestCase { withExtendedLifetime(c) { waitForExpectations(timeout: 1) } - XCTAssertEqual(model.permissions, [:]) + // After navigation reset, geolocation transitions to .reloading (awaiting deactivation) + XCTAssertEqual(model.permissions, [.geolocation: .reloading]) } func testWhenExternalSchemePermissionQueryIsResetThenItTriggersDecisionHandler() { @@ -475,7 +473,7 @@ final class PermissionModelTests: XCTestCase { XCTAssertEqual(model.permissions, [:]) } - func testWhenSystemLocationIsDisabledAndLocationQueriedThenStateIsDisabled() { + func testWhenSystemLocationIsDisabledAndLocationQueriedThenQueryIsShownForTwoStepFlow() { geolocationServiceMock.authorizationStatus = .denied // Wait for authorizationQuery to be set by async Task @@ -499,11 +497,14 @@ final class PermissionModelTests: XCTestCase { } wait(for: [queryExpectation], timeout: 1) - XCTAssertEqual(model.permissions, [.geolocation: .disabled(systemWide: false)]) + // The two-step authorization dialog handles system permission state, + // so geolocation stays in .requested state (not immediately .disabled) + XCTAssertEqual(model.permissions, [.geolocation: .requested(model.authorizationQuery!)]) e = expectation(description: "permission granted") geolocationServiceMock.authorizationStatus = .authorizedAlways - XCTAssertEqual(model.permissions, [.geolocation: .requested(model.authorizationQuery!)]) + // System authorization granted triggers updatePermissions() which transitions from .requested to .inactive + XCTAssertEqual(model.permissions, [.geolocation: .inactive]) model.authorizationQuery!.handleDecision(grant: true) withExtendedLifetime(c) { waitForExpectations(timeout: 1) @@ -1321,12 +1322,9 @@ final class PermissionModelTests: XCTestCase { XCTAssertFalse(model.isPermissionGranted(.notification, forDomain: domain)) } - // MARK: - System Permission Disabled Tests (New Permission View) - - func testWhenNewPermissionViewEnabledAndSystemPermissionDeniedThenQueryIsShown() { - // Enable new permission view feature flag - featureFlaggerMock.featuresStub[FeatureFlag.newPermissionView.rawValue] = true + // MARK: - System Permission Disabled Tests + func testWhenSystemPermissionDeniedThenQueryIsShown() { // Set system permission as denied systemPermissionManagerMock.authorizationStates[.geolocation] = .denied @@ -1352,10 +1350,7 @@ final class PermissionModelTests: XCTestCase { XCTAssertEqual(model.permissions.geolocation, .requested(model.authorizationQuery!)) } - func testWhenNewPermissionViewEnabledAndSystemPermissionRestrictedThenQueryIsShown() { - // Enable new permission view feature flag - featureFlaggerMock.featuresStub[FeatureFlag.newPermissionView.rawValue] = true - + func testWhenSystemPermissionRestrictedThenQueryIsShown() { // Set system permission as restricted systemPermissionManagerMock.authorizationStates[.geolocation] = .restricted @@ -1379,10 +1374,7 @@ final class PermissionModelTests: XCTestCase { XCTAssertNotNil(model.authorizationQuery) } - func testWhenNewPermissionViewEnabledAndSystemPermissionDisabledSystemWideThenQueryIsShown() { - // Enable new permission view feature flag - featureFlaggerMock.featuresStub[FeatureFlag.newPermissionView.rawValue] = true - + func testWhenSystemPermissionDisabledSystemWideThenQueryIsShown() { // Set system permission as system disabled (Location Services off) systemPermissionManagerMock.authorizationStates[.geolocation] = .systemDisabled @@ -1406,10 +1398,7 @@ final class PermissionModelTests: XCTestCase { XCTAssertNotNil(model.authorizationQuery) } - func testWhenNewPermissionViewEnabledAndSystemPermissionAuthorizedThenStoredPermissionIsUsed() { - // Enable new permission view feature flag - featureFlaggerMock.featuresStub[FeatureFlag.newPermissionView.rawValue] = true - + func testWhenSystemPermissionAuthorizedThenStoredPermissionIsUsed() { // Set system permission as authorized systemPermissionManagerMock.authorizationStates[.geolocation] = .authorized @@ -1445,10 +1434,7 @@ final class PermissionModelTests: XCTestCase { XCTAssertFalse(queryShown) } - func testWhenNewPermissionViewEnabledAndSystemPermissionDeniedThenStoredAllowDeniesAndShowsInfoPopover() { - // Enable new permission view feature flag - featureFlaggerMock.featuresStub[FeatureFlag.newPermissionView.rawValue] = true - + func testWhenSystemPermissionDeniedThenStoredAllowDeniesAndShowsInfoPopover() { // Set system permission as denied systemPermissionManagerMock.authorizationStates[.geolocation] = .denied @@ -1487,10 +1473,7 @@ final class PermissionModelTests: XCTestCase { XCTAssertEqual(receivedPermissionType, .geolocation) } - func testWhenNewPermissionViewEnabledAndSystemPermissionDeniedButUserSetNeverAllowThenDenyDirectly() { - // Enable new permission view feature flag - featureFlaggerMock.featuresStub[FeatureFlag.newPermissionView.rawValue] = true - + func testWhenSystemPermissionDeniedButUserSetNeverAllowThenDenyDirectly() { // Set system permission as denied systemPermissionManagerMock.authorizationStates[.geolocation] = .denied diff --git a/macOS/UnitTests/Tab/UserScripts/WebNotificationsHandlerTests.swift b/macOS/UnitTests/Tab/UserScripts/WebNotificationsHandlerTests.swift index c0e0b2e6179..3f45f652d10 100644 --- a/macOS/UnitTests/Tab/UserScripts/WebNotificationsHandlerTests.swift +++ b/macOS/UnitTests/Tab/UserScripts/WebNotificationsHandlerTests.swift @@ -172,7 +172,7 @@ final class WebNotificationsHandlerTests: XCTestCase { mockIconFetcher = MockNotificationIconFetcher() mockPermissionModel = MockWebNotificationPermissionModel() mockFeatureFlagger = MockFeatureFlagger() - mockFeatureFlagger.enableFeatures([.webNotifications, .newPermissionView]) + mockFeatureFlagger.enableFeatures([.webNotifications]) handler = WebNotificationsHandler( tabUUID: testTabUUID, notificationService: mockNotificationService, diff --git a/macOS/UnitTests/TabExtensionsTests/PopupHandlingTabExtensionTests.swift b/macOS/UnitTests/TabExtensionsTests/PopupHandlingTabExtensionTests.swift index 48717ff5e17..d85b1521217 100644 --- a/macOS/UnitTests/TabExtensionsTests/PopupHandlingTabExtensionTests.swift +++ b/macOS/UnitTests/TabExtensionsTests/PopupHandlingTabExtensionTests.swift @@ -46,8 +46,7 @@ final class PopupHandlingTabExtensionTests: XCTestCase { mockFeatureFlagger = MockFeatureFlagger() mockPopupBlockingConfig = MockPopupBlockingConfiguration() testPermissionManager = TestPermissionManager() - mockPermissionModel = PermissionModel(permissionManager: testPermissionManager, - featureFlagger: mockFeatureFlagger) + mockPermissionModel = PermissionModel(permissionManager: testPermissionManager) webView = WebView(featureFlagger: mockFeatureFlagger) configuration = WKWebViewConfiguration() windowFeatures = WKWindowFeatures()