chromium/content/browser/renderer_host/render_widget_host_view_mac_browsertest.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/render_widget_host_view_mac.h"

#include <string>

#include "base/functional/bind.h"
#import "base/mac/mac_util.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#import "content/app_shim_remote_cocoa/render_widget_host_view_cocoa.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "third_party/blink/public/platform/web_text_input_type.h"
#include "ui/events/test/cocoa_test_event_utils.h"

@interface TextInputFlagChangeWaiter : NSObject
@end

@implementation TextInputFlagChangeWaiter {
  RenderWidgetHostViewCocoa* _rwhv_cocoa;
  std::unique_ptr<base::RunLoop> _run_loop;
}

- (instancetype)initWithRenderWidgetHostViewCocoa:
    (RenderWidgetHostViewCocoa*)rwhv_cocoa {
  if ((self = [super init])) {
    _rwhv_cocoa = rwhv_cocoa;
    [_rwhv_cocoa addObserver:self
                  forKeyPath:@"textInputFlags"
                     options:NSKeyValueObservingOptionNew
                     context:nullptr];
    [self reset];
  }
  return self;
}

- (void)dealloc {
  [_rwhv_cocoa removeObserver:self forKeyPath:@"textInputFlags"];
}

- (void)reset {
  _run_loop = std::make_unique<base::RunLoop>();
}

- (void)wait {
  _run_loop->Run();

  [self reset];
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id>*)change
                       context:(void*)context {
  _run_loop->Quit();
}
@end

namespace content {

namespace {

class TextCallbackWaiter {
 public:
  TextCallbackWaiter() = default;

  TextCallbackWaiter(const TextCallbackWaiter&) = delete;
  TextCallbackWaiter& operator=(const TextCallbackWaiter&) = delete;

  void Wait() { run_loop_.Run(); }

  const std::u16string& text() const { return text_; }

  void GetText(const std::u16string& text) {
    text_ = text;
    run_loop_.Quit();
  }

 private:
  std::u16string text_;
  base::RunLoop run_loop_;
};

class TextSelectionWaiter : public TextInputManager::Observer {
 public:
  explicit TextSelectionWaiter(RenderWidgetHostViewMac* rwhv) {
    rwhv->GetTextInputManager()->AddObserver(this);
  }

  void OnTextSelectionChanged(TextInputManager* text_input_manager,
                              RenderWidgetHostViewBase* updated_view) override {
    text_input_manager->RemoveObserver(this);
    run_loop_.Quit();
  }

  void Wait() { run_loop_.Run(); }

 private:
  base::RunLoop run_loop_;
};

}  // namespace

class RenderWidgetHostViewMacTest : public ContentBrowserTest {
 public:
  RenderWidgetHostViewMacTest() {
    scoped_feature_list_.InitAndEnableFeature(
        features::kSonomaAccessibilityActivationRefinements);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewMacTest, GetPageTextForSpeech) {
  GURL url(
      "data:text/html,<span>Hello</span>"
      "<span style='display:none'>Goodbye</span>"
      "<span>World</span>");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderWidgetHostView* rwhv =
      shell()->web_contents()->GetPrimaryMainFrame()->GetView();
  RenderWidgetHostViewMac* rwhv_mac =
      static_cast<RenderWidgetHostViewMac*>(rwhv);

  TextCallbackWaiter waiter;
  rwhv_mac->GetPageTextForSpeech(
      base::BindOnce(&TextCallbackWaiter::GetText, base::Unretained(&waiter)));
  waiter.Wait();

  EXPECT_EQ(u"Hello\nWorld", waiter.text());
}

// Test that -firstRectForCharacterRange:actualRange: works when the range
// isn't in the active selection, which requires a sync IPC to the renderer.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewMacTest,
                       GetFirstRectForCharacterRangeUncached) {
  GURL url("data:text/html,Hello");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  auto* web_contents_impl =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  auto* root = web_contents_impl->GetPrimaryFrameTree().root();
  web_contents_impl->GetPrimaryFrameTree().SetFocusedFrame(
      root, root->current_frame_host()->GetSiteInstance()->group());

  RenderWidgetHostView* rwhv =
      shell()->web_contents()->GetPrimaryMainFrame()->GetView();
  RenderWidgetHostViewMac* rwhv_mac =
      static_cast<RenderWidgetHostViewMac*>(rwhv);

  NSRect rect = [rwhv_mac->GetInProcessNSView()
      firstRectForCharacterRange:NSMakeRange(2, 1)
                     actualRange:nullptr];
  EXPECT_GT(NSMinX(rect), 0);
  EXPECT_GT(NSWidth(rect), 0);
  EXPECT_GT(NSHeight(rect), 0);
}

IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewMacTest, UpdateInputFlags) {
  class InputMethodObserver {};

  GURL url("data:text/html,<!doctype html><textarea id=ta></textarea>");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderWidgetHostView* rwhv =
      shell()->web_contents()->GetPrimaryMainFrame()->GetView();
  RenderWidgetHostViewMac* rwhv_mac =
      static_cast<RenderWidgetHostViewMac*>(rwhv);
  RenderWidgetHostViewCocoa* rwhv_cocoa = rwhv_mac->GetInProcessNSView();
  TextInputFlagChangeWaiter* flag_change_waiter =
      [[TextInputFlagChangeWaiter alloc]
          initWithRenderWidgetHostViewCocoa:rwhv_cocoa];

  EXPECT_TRUE(ExecJs(shell(), "ta.focus();"));
  [flag_change_waiter wait];
  EXPECT_FALSE(rwhv_cocoa.textInputFlags &
               blink::kWebTextInputFlagAutocorrectOff);

  EXPECT_TRUE(ExecJs(
      shell(),
      "ta.setAttribute('autocorrect', 'off'); console.log(ta.outerHTML);"));
  [flag_change_waiter wait];
  EXPECT_TRUE(rwhv_cocoa.textInputFlags &
              blink::kWebTextInputFlagAutocorrectOff);
}

IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewMacTest,
                       InputTextPreventedSyncsCursorLocation) {
  class InputMethodObserver {};

  GURL url("data:text/html,<!doctype html><textarea id=ta></textarea>");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderWidgetHostView* rwhv =
      shell()->web_contents()->GetPrimaryMainFrame()->GetView();
  RenderWidgetHostViewMac* rwhv_mac =
      static_cast<RenderWidgetHostViewMac*>(rwhv);
  RenderWidgetHostViewCocoa* rwhv_cocoa = rwhv_mac->GetInProcessNSView();

  EXPECT_TRUE(ExecJs(
      shell(),
      "ta.addEventListener('keypress', (evt) => {evt.preventDefault();}); "
      "ta.focus();"));

  // Before typing anything, we're at position 0.
  EXPECT_EQ(0lu, [rwhv_cocoa selectedRange].location);

  TextSelectionWaiter waiter(rwhv_mac);
  NSEvent* key_a = cocoa_test_event_utils::KeyEventWithKeyCode(
      'a', 'a', NSEventTypeKeyDown, 0);
  [rwhv_cocoa keyEvent:key_a];

  // After typing 'a', the browser process assumes that the text was entered and
  // we are at position 1.
  EXPECT_EQ(1lu, [rwhv_cocoa selectedRange].location);

  // Wait for the renderer to sync the selection to the browser.
  waiter.Wait();

  // The synced selection updates the selected position back to 0.
  EXPECT_EQ(0lu, [rwhv_cocoa selectedRange].location);
}

// Tests that accessibility role requests sent to the web contents enable
// basic (native + web contents) accessibility support.
IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewMacTest,
                       RespondToAccessibilityRoleRequestsOnWebContent) {
  if (base::mac::MacOSVersion() < 14'00'00) {
    GTEST_SKIP();
  }

  // Load some content.
  GURL url("data:text/html,<!doctype html><textarea id=ta></textarea>");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  content::BrowserAccessibilityState* accessibility_state =
      content::BrowserAccessibilityState::GetInstance();

  // No accessibility support enabled at this time.
  EXPECT_EQ(accessibility_state->GetAccessibilityMode(), ui::AXMode());

  // An AT descending the AX tree calls -accessibilityRole on the nodes as it
  // goes. Simulate an AT calling -accessibilityRole on the web contents.
  RenderWidgetHostView* rwhv =
      shell()->web_contents()->GetPrimaryMainFrame()->GetView();
  RenderWidgetHostViewMac* rwhv_mac =
      static_cast<RenderWidgetHostViewMac*>(rwhv);
  RenderWidgetHostViewCocoa* rwhv_cocoa = rwhv_mac->GetInProcessNSView();

  [rwhv_cocoa accessibilityRole];

  // Calling -accessibilityRole on the RenderWidgetHostViewCocoa should have
  // activated basic accessibility support.
  EXPECT_EQ(accessibility_state->GetAccessibilityMode(), ui::kAXModeBasic);
}

}  // namespace content