| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/chrome_browser_application_mac.h" |
| |
| #import "base/apple/foundation_util.h" |
| #import "base/apple/scoped_objc_class_swizzler.h" |
| #import "base/mac/mac_util.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "content/public/browser/browser_accessibility_state.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_test.h" |
| |
| namespace { |
| BOOL g_voice_over_enabled = NO; |
| } |
| |
| @interface NSWorkspace (Extras) |
| - (BOOL)voiceOverEnabled; |
| - (void)setVoiceOverEnabled:(BOOL)flag; |
| @end |
| |
| @implementation NSWorkspace (Extras) |
| |
| - (BOOL)voiceOverEnabled { |
| return g_voice_over_enabled; |
| } |
| |
| // It seems NSWorkspace notifies of changes to voiceOverEnabled via KVO, |
| // but doesn't implement this method. We add it so we can test our KVO |
| // monitoring code. |
| - (void)setVoiceOverEnabled:(BOOL)flag { |
| g_voice_over_enabled = flag; |
| } |
| |
| @end |
| |
| @interface NSApplication (ChromeBrowserApplicationMacBrowserTestSwizzle) |
| @end |
| |
| @implementation NSApplication (ChromeBrowserApplicationMacBrowserTestSwizzle) |
| |
| - (void)testObserveValueForKeyPath:(NSString*)keyPath |
| ofObject:(id)object |
| change: |
| (NSDictionary<NSKeyValueChangeKey, id>*)change |
| context:(void*)context { |
| if (context) { |
| *static_cast<bool*>(context) = true; |
| } |
| } |
| |
| @end |
| |
| class ChromeBrowserAppMacBrowserTest : public InProcessBrowserTest { |
| public: |
| ChromeBrowserAppMacBrowserTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kSonomaAccessibilityActivationRefinements); |
| |
| br_cr_app_ = base::apple::ObjCCast<BrowserCrApplication>(NSApp); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| SetVoiceOverEnabled(VoiceOverEnabledAtStartUp()); |
| } |
| |
| // Whether or not we simulate VoiceOver active before the test runs. |
| virtual BOOL VoiceOverEnabledAtStartUp() { return NO; } |
| |
| BOOL VoiceOverEnabled() { return [br_cr_app_ voiceOverStateForTesting]; } |
| |
| // Simulates the user activating or deactivating VoiceOver. |
| void SetVoiceOverEnabled(BOOL enabled) { |
| NSString* kVoiceOverKVOKeyPath = @"voiceOverEnabled"; |
| [[NSWorkspace sharedWorkspace] setValue:[NSNumber numberWithBool:enabled] |
| forKey:kVoiceOverKVOKeyPath]; |
| } |
| |
| void WaitThreeSeconds() { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&base::RunLoop::Quit, base::Unretained(&run_loop)), |
| base::Seconds(3)); |
| run_loop.Run(); |
| } |
| |
| bool BrowserIsInAccessibilityMode(ui::AXMode mode) { |
| content::BrowserAccessibilityState* accessibility_state = |
| content::BrowserAccessibilityState::GetInstance(); |
| return accessibility_state->GetAccessibilityMode() == mode; |
| } |
| |
| bool BrowserIsInCompleteAccessibilityMode() { |
| return BrowserIsInAccessibilityMode(ui::kAXModeComplete); |
| } |
| |
| bool BrowserIsInBasicAccessibilityMode() { |
| return BrowserIsInAccessibilityMode(ui::kAXModeBasic); |
| } |
| |
| bool BrowserIsInNativeAPIAccessibilityMode() { |
| return BrowserIsInAccessibilityMode(ui::AXMode::kNativeAPIs); |
| } |
| |
| bool BrowserAccessibilityDisabled() { |
| return BrowserIsInAccessibilityMode(ui::AXMode()); |
| } |
| |
| void RequestAppAccessibilityRole() { [br_cr_app_ accessibilityRole]; } |
| |
| void EnableEnhancedUserInterface(BOOL enable) { |
| // We need to call -accessibilitySetValue:forAttribute: on br_cr_app_, but |
| // the compiler complains it's deprecated API. It's right, but it's the API |
| // BrowserCrApplication is currently using. Silence the error. |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| [br_cr_app_ accessibilitySetValue:[NSNumber numberWithBool:enable] |
| forAttribute:@"AXEnhancedUserInterface"]; |
| #pragma clang diagnostic pop |
| } |
| |
| private: |
| BrowserCrApplication* br_cr_app_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Ensures that overrides to the application's |
| // observeValueForKeyPath:ofObject:change:context: method call super on |
| // unrecognized key paths. |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserTest, |
| KVOObservationCallsSuper) { |
| base::apple::ScopedObjCClassSwizzler swizzler( |
| [NSApplication class], |
| @selector(observeValueForKeyPath:ofObject:change:context:), |
| @selector(testObserveValueForKeyPath:ofObject:change:context:)); |
| |
| bool super_was_called = false; |
| |
| [NSApp observeValueForKeyPath:@"testKeyPath" |
| ofObject:nil |
| change:nil |
| context:&super_was_called]; |
| |
| EXPECT_TRUE(super_was_called); |
| } |
| |
| // Tests how BrowserCrApplication responds to VoiceOver activations. |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserTest, |
| RespondToVoiceOverStateChanges) { |
| // We could perform this check in SetUp(), but in theory, this browser |
| // test file will contain more that just these Sonoma tests. |
| if (base::mac::MacOSVersion() < 14'00'00) { |
| GTEST_SKIP(); |
| } |
| |
| EXPECT_FALSE(VoiceOverEnabled()); |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| SetVoiceOverEnabled(YES); |
| EXPECT_TRUE(VoiceOverEnabled()); |
| EXPECT_TRUE(BrowserIsInCompleteAccessibilityMode()); |
| |
| SetVoiceOverEnabled(NO); |
| EXPECT_FALSE(VoiceOverEnabled()); |
| |
| // Turning VoiceOver off disables accessibility support, but not immediately. |
| // Chrome waits a couple seconds in case there's a fast-follow call to enable |
| // it. Wait a bit for the change to take before proceeding. |
| WaitThreeSeconds(); |
| |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| } |
| |
| // Tests how BrowserCrApplication responds to AXEnhancedUserInterface requests |
| // from Assistive Technology (AT). |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserTest, |
| RespondToAXEnhancedUserInterfaceRequests) { |
| if (base::mac::MacOSVersion() < 14'00'00) { |
| GTEST_SKIP(); |
| } |
| |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| // Requesting AX enhanced UI should have no immediate effect. |
| EnableEnhancedUserInterface(YES); |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| // If we suddenly turn off support and wait a bit, there should be no change |
| // in accessibility support. We're ensuring that sudden on/off changes are |
| // ignored. |
| EnableEnhancedUserInterface(NO); |
| WaitThreeSeconds(); |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| // If we turn it on and wait, the code should assume it's not a spurious |
| // request. |
| EnableEnhancedUserInterface(YES); |
| WaitThreeSeconds(); |
| EXPECT_TRUE(BrowserIsInCompleteAccessibilityMode()); |
| |
| // Turn it off (and wait a bit). |
| EnableEnhancedUserInterface(NO); |
| WaitThreeSeconds(); |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| } |
| |
| // Tests how BrowserCrApplication responds to mismatched |
| // AXEnhancedUserInterface requests. |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserTest, |
| HandleMismatchedAXEnhancedUserInterfaceRequests) { |
| if (base::mac::MacOSVersion() < 14'00'00) { |
| GTEST_SKIP(); |
| } |
| |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| // The code uses a counter to track requests. Ensure that it can't go |
| // negative. |
| EnableEnhancedUserInterface(YES); |
| EnableEnhancedUserInterface(NO); |
| EnableEnhancedUserInterface(NO); |
| EnableEnhancedUserInterface(NO); |
| |
| WaitThreeSeconds(); |
| |
| EnableEnhancedUserInterface(YES); |
| WaitThreeSeconds(); |
| EXPECT_TRUE(BrowserIsInCompleteAccessibilityMode()); |
| } |
| |
| // Tests that BrowserCrApplication ignores requests from ATs to disable AX |
| // support if VoiceOver is active. |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserTest, |
| IgnoreAXEnhancedUserInterfaceDisableRequests) { |
| if (base::mac::MacOSVersion() < 14'00'00) { |
| GTEST_SKIP(); |
| } |
| |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| // Simulate an AT requesting AX enhanced UI. |
| EnableEnhancedUserInterface(YES); |
| |
| WaitThreeSeconds(); |
| |
| // The user activates VoiceOver. |
| SetVoiceOverEnabled(YES); |
| EXPECT_TRUE(BrowserIsInCompleteAccessibilityMode()); |
| |
| // When the AT is done, make sure it can't disable AX support (VoiceOver is |
| // using it). |
| EnableEnhancedUserInterface(NO); |
| WaitThreeSeconds(); |
| EXPECT_FALSE(BrowserAccessibilityDisabled()); |
| } |
| |
| // Tests that accessibility role requests to the application enable native |
| // accessibility support. |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserTest, |
| RespondToAccessibilityRoleRequests) { |
| if (base::mac::MacOSVersion() < 14'00'00) { |
| GTEST_SKIP(); |
| } |
| |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| RequestAppAccessibilityRole(); |
| EXPECT_TRUE(BrowserIsInNativeAPIAccessibilityMode()); |
| |
| // The user activates VoiceOver. |
| SetVoiceOverEnabled(YES); |
| |
| // Requests for AccessibilityRole when VoiceOver is active should not |
| // downgrade the AX level. |
| RequestAppAccessibilityRole(); |
| EXPECT_TRUE(BrowserIsInCompleteAccessibilityMode()); |
| |
| SetVoiceOverEnabled(NO); |
| WaitThreeSeconds(); |
| EXPECT_TRUE(BrowserAccessibilityDisabled()); |
| |
| EnableEnhancedUserInterface(YES); |
| WaitThreeSeconds(); |
| |
| // Requests for AccessibilityRole when the AX mode is complete should not |
| // downgrade the AX level. |
| RequestAppAccessibilityRole(); |
| EXPECT_TRUE(BrowserIsInCompleteAccessibilityMode()); |
| } |
| |
| // A test class where VoiceOver is "enabled" when its tests start. |
| class ChromeBrowserAppMacBrowserMacVoiceOverEnabledTest |
| : public ChromeBrowserAppMacBrowserTest { |
| public: |
| BOOL VoiceOverEnabledAtStartUp() override { return YES; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ChromeBrowserAppMacBrowserMacVoiceOverEnabledTest, |
| DetectVoiceOverStateOnStartUp) { |
| if (base::mac::MacOSVersion() < 14'00'00) { |
| GTEST_SKIP(); |
| } |
| |
| content::BrowserAccessibilityState* accessibility_state = |
| content::BrowserAccessibilityState::GetInstance(); |
| |
| // Enable VoiceOver. |
| EXPECT_TRUE(VoiceOverEnabled()); |
| EXPECT_EQ(accessibility_state->GetAccessibilityMode(), ui::kAXModeComplete); |
| } |