blob: 5c729fcde948dcfe154bec0ca7d496ea42185d10 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/views_bridge_mac/bridged_native_widget_impl.h"
#import <Cocoa/Cocoa.h>
#include <objc/runtime.h>
#include <memory>
#import "base/mac/foundation_util.h"
#import "base/mac/mac_util.h"
#import "base/mac/scoped_objc_class_swizzler.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/macros.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_task_environment.h"
#import "testing/gtest_mac.h"
#import "ui/base/cocoa/window_size_constants.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/material_design/material_design_controller.h"
#import "ui/base/test/cocoa_helper.h"
#include "ui/events/test/cocoa_test_event_utils.h"
#import "ui/gfx/mac/coordinate_conversion.h"
#import "ui/views/cocoa/bridged_native_widget_host_impl.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/controls/textfield/textfield_controller.h"
#include "ui/views/controls/textfield/textfield_model.h"
#include "ui/views/test/test_views_delegate.h"
#include "ui/views/view.h"
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#import "ui/views_bridge_mac/bridged_content_view.h"
#import "ui/views_bridge_mac/native_widget_mac_nswindow.h"
#import "ui/views_bridge_mac/views_nswindow_delegate.h"
using base::ASCIIToUTF16;
using base::SysNSStringToUTF8;
using base::SysNSStringToUTF16;
using base::SysUTF8ToNSString;
using base::SysUTF16ToNSString;
#define EXPECT_EQ_RANGE(a, b) \
EXPECT_EQ(a.location, b.location); \
EXPECT_EQ(a.length, b.length);
// Helpers to verify an expectation against both the actual toolkit-views
// behaviour and the Cocoa behaviour.
#define EXPECT_NSEQ_3(expected_literal, expected_cocoa, actual_views) \
EXPECT_NSEQ(expected_literal, actual_views); \
EXPECT_NSEQ(expected_cocoa, actual_views);
#define EXPECT_EQ_RANGE_3(expected_literal, expected_cocoa, actual_views) \
EXPECT_EQ_RANGE(expected_literal, actual_views); \
EXPECT_EQ_RANGE(expected_cocoa, actual_views);
#define EXPECT_EQ_3(expected_literal, expected_cocoa, actual_views) \
EXPECT_EQ(expected_literal, actual_views); \
EXPECT_EQ(expected_cocoa, actual_views);
namespace {
// Implemented NSResponder action messages for use in tests.
NSArray* const kMoveActions = @[
@"moveForward:",
@"moveRight:",
@"moveBackward:",
@"moveLeft:",
@"moveUp:",
@"moveDown:",
@"moveWordForward:",
@"moveWordBackward:",
@"moveToBeginningOfLine:",
@"moveToEndOfLine:",
@"moveToBeginningOfParagraph:",
@"moveToEndOfParagraph:",
@"moveToEndOfDocument:",
@"moveToBeginningOfDocument:",
@"pageDown:",
@"pageUp:",
@"moveWordRight:",
@"moveWordLeft:",
@"moveToLeftEndOfLine:",
@"moveToRightEndOfLine:"
];
NSArray* const kSelectActions = @[
@"moveBackwardAndModifySelection:",
@"moveForwardAndModifySelection:",
@"moveWordForwardAndModifySelection:",
@"moveWordBackwardAndModifySelection:",
@"moveUpAndModifySelection:",
@"moveDownAndModifySelection:",
@"moveToBeginningOfLineAndModifySelection:",
@"moveToEndOfLineAndModifySelection:",
@"moveToBeginningOfParagraphAndModifySelection:",
@"moveToEndOfParagraphAndModifySelection:",
@"moveToEndOfDocumentAndModifySelection:",
@"moveToBeginningOfDocumentAndModifySelection:",
@"pageDownAndModifySelection:",
@"pageUpAndModifySelection:",
@"moveParagraphForwardAndModifySelection:",
@"moveParagraphBackwardAndModifySelection:",
@"moveRightAndModifySelection:",
@"moveLeftAndModifySelection:",
@"moveWordRightAndModifySelection:",
@"moveWordLeftAndModifySelection:",
@"moveToLeftEndOfLineAndModifySelection:",
@"moveToRightEndOfLineAndModifySelection:"
];
NSArray* const kDeleteActions = @[
@"deleteForward:", @"deleteBackward:", @"deleteWordForward:",
@"deleteWordBackward:", @"deleteToBeginningOfLine:", @"deleteToEndOfLine:",
@"deleteToBeginningOfParagraph:", @"deleteToEndOfParagraph:"
];
// This omits @"insertText:":. See BridgedNativeWidgetTest.NilTextInputClient.
NSArray* const kMiscActions = @[ @"cancelOperation:", @"transpose:", @"yank:" ];
// Empty range shortcut for readibility.
NSRange EmptyRange() {
return NSMakeRange(NSNotFound, 0);
}
// Sets |composition_text| as the composition text with caret placed at
// |caret_pos| and updates |caret_range|.
void SetCompositionText(ui::TextInputClient* client,
const base::string16& composition_text,
const int caret_pos,
NSRange* caret_range) {
ui::CompositionText composition;
composition.selection = gfx::Range(caret_pos);
composition.text = composition_text;
client->SetCompositionText(composition);
if (caret_range)
*caret_range = NSMakeRange(caret_pos, 0);
}
// Returns a zero width rectangle corresponding to current caret position.
gfx::Rect GetCaretBounds(const ui::TextInputClient* client) {
gfx::Rect caret_bounds = client->GetCaretBounds();
caret_bounds.set_width(0);
return caret_bounds;
}
// Returns a zero width rectangle corresponding to caret bounds when it's placed
// at |caret_pos| and updates |caret_range|.
gfx::Rect GetCaretBoundsForPosition(ui::TextInputClient* client,
const base::string16& composition_text,
const int caret_pos,
NSRange* caret_range) {
SetCompositionText(client, composition_text, caret_pos, caret_range);
return GetCaretBounds(client);
}
// Returns the expected boundary rectangle for characters of |composition_text|
// within the |query_range|.
gfx::Rect GetExpectedBoundsForRange(ui::TextInputClient* client,
const base::string16& composition_text,
NSRange query_range) {
gfx::Rect left_caret = GetCaretBoundsForPosition(
client, composition_text, query_range.location, nullptr);
gfx::Rect right_caret = GetCaretBoundsForPosition(
client, composition_text, query_range.location + query_range.length,
nullptr);
// The expected bounds correspond to the area between the left and right caret
// positions.
return gfx::Rect(left_caret.x(), left_caret.y(),
right_caret.x() - left_caret.x(), left_caret.height());
}
// Uses the NSTextInputClient protocol to extract a substring from |view|.
NSString* GetViewStringForRange(NSView<NSTextInputClient>* view,
NSRange range) {
return [[view attributedSubstringForProposedRange:range actualRange:nullptr]
string];
}
// The behavior of NSTextView for RTL strings is buggy for some move and select
// commands, but only when the command is received when there is a selection
// active. E.g. moveRight: moves a cursor right in an RTL string, but it moves
// to the left-end of a selection. See TestEditingCommands() for specifics.
// This is filed as rdar://27863290.
bool IsRTLMoveBuggy(SEL sel) {
return sel == @selector(moveWordRight:) || sel == @selector(moveWordLeft:) ||
sel == @selector(moveRight:) || sel == @selector(moveLeft:);
}
bool IsRTLSelectBuggy(SEL sel) {
return sel == @selector(moveWordRightAndModifySelection:) ||
sel == @selector(moveWordLeftAndModifySelection:) ||
sel == @selector(moveRightAndModifySelection:) ||
sel == @selector(moveLeftAndModifySelection:);
}
// Used by InterpretKeyEventsDonorForNSView to simulate IME behavior.
using InterpretKeyEventsCallback = base::RepeatingCallback<void(id)>;
InterpretKeyEventsCallback* g_fake_interpret_key_events = nullptr;
// Used by UpdateWindowsDonorForNSApp to hook -[NSApp updateWindows].
base::RepeatingClosure* g_update_windows_closure = nullptr;
// Used to provide a return value for +[NSTextInputContext currentInputContext].
NSTextInputContext* g_fake_current_input_context = nullptr;
} // namespace
// Subclass of BridgedContentView with an override of interpretKeyEvents:. Note
// the size of the class must match BridgedContentView since the method table
// is swapped out at runtime. This is basically a mock, but mocks are banned
// under ui/views. Method swizzling causes these tests to flake when
// parallelized in the same process.
@interface InterpretKeyEventMockedBridgedContentView : BridgedContentView
@end
@implementation InterpretKeyEventMockedBridgedContentView
- (void)interpretKeyEvents:(NSArray<NSEvent*>*)eventArray {
ASSERT_TRUE(g_fake_interpret_key_events);
g_fake_interpret_key_events->Run(self);
}
@end
@interface UpdateWindowsDonorForNSApp : NSApplication
@end
@implementation UpdateWindowsDonorForNSApp
- (void)updateWindows {
ASSERT_TRUE(g_update_windows_closure);
g_update_windows_closure->Run();
}
@end
@interface CurrentInputContextDonorForNSTextInputContext : NSTextInputContext
@end
@implementation CurrentInputContextDonorForNSTextInputContext
+ (NSTextInputContext*)currentInputContext {
return g_fake_current_input_context;
}
@end
// Class to override -[NSWindow toggleFullScreen:] to a no-op. This simulates
// NSWindow's behavior when attempting to toggle fullscreen state again, when
// the last attempt failed but Cocoa has not yet sent
// windowDidFailToEnterFullScreen:.
@interface BridgedNativeWidgetTestWindow : NativeWidgetMacNSWindow {
@private
BOOL ignoreToggleFullScreen_;
int ignoredToggleFullScreenCount_;
}
@property(assign, nonatomic) BOOL ignoreToggleFullScreen;
@property(readonly, nonatomic) int ignoredToggleFullScreenCount;
@end
@implementation BridgedNativeWidgetTestWindow
@synthesize ignoreToggleFullScreen = ignoreToggleFullScreen_;
@synthesize ignoredToggleFullScreenCount = ignoredToggleFullScreenCount_;
- (void)performSelector:(SEL)aSelector
withObject:(id)anArgument
afterDelay:(NSTimeInterval)delay {
// This is used in simulations without a message loop. Don't start a message
// loop since that would expose the tests to system notifications and
// potential flakes. Instead, just pretend the message loop is flushed here.
if (ignoreToggleFullScreen_ && aSelector == @selector(toggleFullScreen:))
[self toggleFullScreen:anArgument];
else
[super performSelector:aSelector withObject:anArgument afterDelay:delay];
}
- (void)toggleFullScreen:(id)sender {
if (ignoreToggleFullScreen_)
++ignoredToggleFullScreenCount_;
else
[super toggleFullScreen:sender];
}
@end
namespace views {
namespace test {
// Provides the |parent| argument to construct a BridgedNativeWidgetImpl.
class MockNativeWidgetMac : public NativeWidgetMac {
public:
explicit MockNativeWidgetMac(internal::NativeWidgetDelegate* delegate)
: NativeWidgetMac(delegate) {}
using NativeWidgetMac::bridge_impl;
using NativeWidgetMac::bridge_host;
// internal::NativeWidgetPrivate:
void InitNativeWidget(const Widget::InitParams& params) override {
ownership_ = params.ownership;
base::scoped_nsobject<NativeWidgetMacNSWindow> window(
[[BridgedNativeWidgetTestWindow alloc]
initWithContentRect:ui::kWindowSizeDeterminedLater
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO]);
bridge_host()->CreateLocalBridge(window);
if (auto* parent =
BridgedNativeWidgetHostImpl::GetFromNativeView(params.parent)) {
bridge_host()->SetParent(parent);
}
bridge_host()->InitWindow(params);
// Usually the bridge gets initialized here. It is skipped to run extra
// checks in tests, and so that a second window isn't created.
delegate()->OnNativeWidgetCreated(true);
// To allow events to dispatch to a view, it needs a way to get focus.
bridge_host()->SetFocusManager(GetWidget()->GetFocusManager());
}
void ReorderNativeViews() override {
// Called via Widget::Init to set the content view. No-op in these tests.
}
private:
DISALLOW_COPY_AND_ASSIGN(MockNativeWidgetMac);
};
// Helper test base to construct a BridgedNativeWidgetImpl with a valid parent.
class BridgedNativeWidgetTestBase : public ui::CocoaTest {
public:
struct SkipInitialization {};
BridgedNativeWidgetTestBase()
: widget_(new Widget),
native_widget_mac_(new MockNativeWidgetMac(widget_.get())) {}
explicit BridgedNativeWidgetTestBase(SkipInitialization tag)
: native_widget_mac_(nullptr) {}
BridgedNativeWidgetImpl* bridge() {
return native_widget_mac_->bridge_impl();
}
BridgedNativeWidgetHostImpl* bridge_host() {
return native_widget_mac_->bridge_host();
}
// Generate an autoreleased KeyDown NSEvent* in |widget_| for pressing the
// corresponding |key_code|.
NSEvent* VkeyKeyDown(ui::KeyboardCode key_code) {
return cocoa_test_event_utils::SynthesizeKeyEvent(
widget_->GetNativeWindow().GetNativeNSWindow(), true /* keyDown */,
key_code, 0);
}
// Generate an autoreleased KeyDown NSEvent* using the given keycode, and
// representing the first unicode character of |chars|.
NSEvent* UnicodeKeyDown(int key_code, NSString* chars) {
return cocoa_test_event_utils::KeyEventWithKeyCode(
key_code, [chars characterAtIndex:0], NSKeyDown, 0);
}
// Overridden from testing::Test:
void SetUp() override {
ui::CocoaTest::SetUp();
ui::MaterialDesignController::Initialize();
init_params_.native_widget = native_widget_mac_;
// Use a frameless window, otherwise Widget will try to center the window
// before the tests covering the Init() flow are ready to do that.
init_params_.type = Widget::InitParams::TYPE_WINDOW_FRAMELESS;
// To control the lifetime without an actual window that must be closed,
// tests in this file need to use WIDGET_OWNS_NATIVE_WIDGET.
init_params_.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
// Opacity defaults to "infer" which is usually updated by ViewsDelegate.
init_params_.opacity = Widget::InitParams::OPAQUE_WINDOW;
init_params_.bounds = gfx::Rect(100, 100, 100, 100);
if (native_widget_mac_)
native_widget_mac_->GetWidget()->Init(init_params_);
}
void TearDown() override {
// ui::CocoaTest::TearDown will wait until all NSWindows are destroyed, so
// be sure to destroy the widget (which will destroy its NSWindow)
// beforehand.
widget_.reset();
ui::CocoaTest::TearDown();
}
NSWindow* bridge_window() const {
if (native_widget_mac_->bridge_impl())
return native_widget_mac_->bridge_impl()->ns_window();
return nil;
}
protected:
std::unique_ptr<Widget> widget_;
MockNativeWidgetMac* native_widget_mac_; // Weak. Owned by |widget_|.
// Make the InitParams available to tests to cover initialization codepaths.
Widget::InitParams init_params_;
private:
TestViewsDelegate test_views_delegate_;
DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTestBase);
};
class BridgedNativeWidgetTest : public BridgedNativeWidgetTestBase,
public TextfieldController {
public:
using HandleKeyEventCallback =
base::RepeatingCallback<bool(Textfield*, const ui::KeyEvent& key_event)>;
BridgedNativeWidgetTest();
~BridgedNativeWidgetTest() override;
// Install a textfield with input type |text_input_type| in the view hierarchy
// and make it the text input client. Also initializes |dummy_text_view_|.
Textfield* InstallTextField(
const base::string16& text,
ui::TextInputType text_input_type = ui::TEXT_INPUT_TYPE_TEXT);
Textfield* InstallTextField(const std::string& text);
// Returns the actual current text for |ns_view_|, or the selected substring.
NSString* GetActualText();
NSString* GetActualSelectedText();
// Returns the expected current text from |dummy_text_view_|, or the selected
// substring.
NSString* GetExpectedText();
NSString* GetExpectedSelectedText();
// Returns the actual selection range for |ns_view_|.
NSRange GetActualSelectionRange();
// Returns the expected selection range from |dummy_text_view_|.
NSRange GetExpectedSelectionRange();
// Set the selection range for the installed textfield and |dummy_text_view_|.
void SetSelectionRange(NSRange range);
// Perform command |sel| on |ns_view_| and |dummy_text_view_|.
void PerformCommand(SEL sel);
// Make selection from |start| to |end| on installed views::Textfield and
// |dummy_text_view_|. If |start| > |end|, extend selection to left from
// |start|.
void MakeSelection(int start, int end);
// Helper method to set the private |keyDownEvent_| field on |ns_view_|.
void SetKeyDownEvent(NSEvent* event);
// Sets a callback to run on the next HandleKeyEvent().
void SetHandleKeyEventCallback(HandleKeyEventCallback callback);
// testing::Test:
void SetUp() override;
void TearDown() override;
// TextfieldController:
bool HandleKeyEvent(Textfield* sender,
const ui::KeyEvent& key_event) override;
protected:
// Test delete to beginning of line or paragraph based on |sel|. |sel| can be
// either deleteToBeginningOfLine: or deleteToBeginningOfParagraph:.
void TestDeleteBeginning(SEL sel);
// Test delete to end of line or paragraph based on |sel|. |sel| can be
// either deleteToEndOfLine: or deleteToEndOfParagraph:.
void TestDeleteEnd(SEL sel);
// Test editing commands in |selectors| against the expectations set by
// |dummy_text_view_|. This is done by selecting every substring within a set
// of test strings (both RTL and non-RTL by default) and performing every
// selector on both the NSTextView and the BridgedContentView hosting a
// focused views::TextField to ensure the resulting text and selection ranges
// match. |selectors| is an NSArray of NSStrings. |cases| determines whether
// RTL strings are to be tested.
void TestEditingCommands(NSArray* selectors);
std::unique_ptr<views::View> view_;
// Weak. Owned by bridge().
BridgedContentView* ns_view_;
// An NSTextView which helps set the expectations for our tests.
base::scoped_nsobject<NSTextView> dummy_text_view_;
HandleKeyEventCallback handle_key_event_callback_;
base::test::ScopedTaskEnvironment scoped_task_environment_;
private:
DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTest);
};
// Class that counts occurrences of a VKEY_RETURN accelerator, marking them
// processed.
class EnterAcceleratorView : public View {
public:
EnterAcceleratorView() { AddAccelerator({ui::VKEY_RETURN, 0}); }
int count() const { return count_; }
// View:
bool AcceleratorPressed(const ui::Accelerator& accelerator) override {
++count_;
return true;
}
private:
int count_ = 0;
};
BridgedNativeWidgetTest::BridgedNativeWidgetTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::UI) {}
BridgedNativeWidgetTest::~BridgedNativeWidgetTest() {
}
Textfield* BridgedNativeWidgetTest::InstallTextField(
const base::string16& text,
ui::TextInputType text_input_type) {
Textfield* textfield = new Textfield();
textfield->SetText(text);
textfield->SetTextInputType(text_input_type);
textfield->set_controller(this);
view_->RemoveAllChildViews(true);
view_->AddChildView(textfield);
textfield->SetBoundsRect(init_params_.bounds);
// Request focus so the InputMethod can dispatch events to the RootView, and
// have them delivered to the textfield. Note that focusing a textfield
// schedules a task to flash the cursor, so this requires |message_loop_|.
textfield->RequestFocus();
[ns_view_ setTextInputClient:textfield];
// Initialize the dummy text view. Initializing this with NSZeroRect causes
// weird NSTextView behavior on OSX 10.9.
dummy_text_view_.reset(
[[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]);
[dummy_text_view_ setString:SysUTF16ToNSString(text)];
return textfield;
}
Textfield* BridgedNativeWidgetTest::InstallTextField(const std::string& text) {
return InstallTextField(base::ASCIIToUTF16(text));
}
NSString* BridgedNativeWidgetTest::GetActualText() {
return GetViewStringForRange(ns_view_, NSMakeRange(0, NSUIntegerMax));
}
NSString* BridgedNativeWidgetTest::GetActualSelectedText() {
return GetViewStringForRange(ns_view_, [ns_view_ selectedRange]);
}
NSString* BridgedNativeWidgetTest::GetExpectedText() {
return GetViewStringForRange(dummy_text_view_, NSMakeRange(0, NSUIntegerMax));
}
NSString* BridgedNativeWidgetTest::GetExpectedSelectedText() {
return GetViewStringForRange(dummy_text_view_,
[dummy_text_view_ selectedRange]);
}
NSRange BridgedNativeWidgetTest::GetActualSelectionRange() {
return [ns_view_ selectedRange];
}
NSRange BridgedNativeWidgetTest::GetExpectedSelectionRange() {
return [dummy_text_view_ selectedRange];
}
void BridgedNativeWidgetTest::SetSelectionRange(NSRange range) {
ui::TextInputClient* client = [ns_view_ textInputClient];
client->SetEditableSelectionRange(gfx::Range(range));
[dummy_text_view_ setSelectedRange:range];
}
void BridgedNativeWidgetTest::PerformCommand(SEL sel) {
[ns_view_ doCommandBySelector:sel];
[dummy_text_view_ doCommandBySelector:sel];
}
void BridgedNativeWidgetTest::MakeSelection(int start, int end) {
ui::TextInputClient* client = [ns_view_ textInputClient];
const gfx::Range range(start, end);
// Although a gfx::Range is directed, the underlying model will not choose an
// affinity until the cursor is moved.
client->SetEditableSelectionRange(range);
// Set the range without an affinity. The first @selector sent to the text
// field determines the affinity. Note that Range::ToNSRange() may discard
// the direction since NSRange has no direction.
[dummy_text_view_ setSelectedRange:range.ToNSRange()];
}
void BridgedNativeWidgetTest::SetKeyDownEvent(NSEvent* event) {
[ns_view_ setValue:event forKey:@"keyDownEvent_"];
}
void BridgedNativeWidgetTest::SetHandleKeyEventCallback(
HandleKeyEventCallback callback) {
handle_key_event_callback_ = std::move(callback);
}
void BridgedNativeWidgetTest::SetUp() {
BridgedNativeWidgetTestBase::SetUp();
view_.reset(new views::internal::RootView(widget_.get()));
base::scoped_nsobject<NSWindow> window([bridge_window() retain]);
// The delegate should exist before setting the root view.
EXPECT_TRUE([window delegate]);
bridge_host()->SetRootView(view_.get());
bridge()->CreateContentView(bridge_host()->GetRootViewNSViewId(),
view_->bounds());
ns_view_ = bridge()->ns_view();
// Pretend it has been shown via NativeWidgetMac::Show().
[window orderFront:nil];
[window makeFirstResponder:bridge()->ns_view()];
}
void BridgedNativeWidgetTest::TearDown() {
// Clear kill buffer so that no state persists between tests.
TextfieldModel::ClearKillBuffer();
if (bridge_host()) {
bridge_host()->SetRootView(nullptr);
bridge()->DestroyContentView();
}
view_.reset();
BridgedNativeWidgetTestBase::TearDown();
}
bool BridgedNativeWidgetTest::HandleKeyEvent(Textfield* sender,
const ui::KeyEvent& key_event) {
if (handle_key_event_callback_)
return handle_key_event_callback_.Run(sender, key_event);
return false;
}
void BridgedNativeWidgetTest::TestDeleteBeginning(SEL sel) {
InstallTextField("foo bar baz");
EXPECT_EQ_RANGE_3(NSMakeRange(11, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move the caret to the beginning of the line.
SetSelectionRange(NSMakeRange(0, 0));
// Verify no deletion takes place.
PerformCommand(sel);
EXPECT_NSEQ_3(@"foo bar baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move the caret as- "foo| bar baz".
SetSelectionRange(NSMakeRange(3, 0));
PerformCommand(sel);
// Verify state is "| bar baz".
EXPECT_NSEQ_3(@" bar baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Make a selection as- " bar |baz|".
SetSelectionRange(NSMakeRange(5, 3));
PerformCommand(sel);
// Verify only the selection is deleted so that the state is " bar |".
EXPECT_NSEQ_3(@" bar ", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(5, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Verify yanking inserts the deleted text.
PerformCommand(@selector(yank:));
EXPECT_NSEQ_3(@" bar baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(8, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
void BridgedNativeWidgetTest::TestDeleteEnd(SEL sel) {
InstallTextField("foo bar baz");
EXPECT_EQ_RANGE_3(NSMakeRange(11, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Caret is at the end of the line. Verify no deletion takes place.
PerformCommand(sel);
EXPECT_NSEQ_3(@"foo bar baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(11, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move the caret as- "foo bar| baz".
SetSelectionRange(NSMakeRange(7, 0));
PerformCommand(sel);
// Verify state is "foo bar|".
EXPECT_NSEQ_3(@"foo bar", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(7, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Make a selection as- "|foo |bar".
SetSelectionRange(NSMakeRange(0, 4));
PerformCommand(sel);
// Verify only the selection is deleted so that the state is "|bar".
EXPECT_NSEQ_3(@"bar", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Verify yanking inserts the deleted text.
PerformCommand(@selector(yank:));
EXPECT_NSEQ_3(@"foo bar", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(4, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
void BridgedNativeWidgetTest::TestEditingCommands(NSArray* selectors) {
struct {
base::string16 test_string;
bool is_rtl;
} test_cases[] = {
{base::WideToUTF16(L"ab c"), false},
{base::WideToUTF16(L"\x0634\x0632 \x064A"), true},
};
for (const auto& test_case : test_cases) {
for (NSString* selector_string in selectors) {
SEL sel = NSSelectorFromString(selector_string);
const int len = test_case.test_string.length();
for (int i = 0; i <= len; i++) {
for (int j = 0; j <= len; j++) {
SCOPED_TRACE(base::StringPrintf(
"Testing range [%d-%d] for case %s and selector %s\n", i, j,
base::UTF16ToUTF8(test_case.test_string).c_str(),
base::SysNSStringToUTF8(selector_string).c_str()));
InstallTextField(test_case.test_string);
MakeSelection(i, j);
// Sanity checks for MakeSelection().
EXPECT_NSEQ(GetExpectedSelectedText(), GetActualSelectedText());
EXPECT_EQ_RANGE_3(NSMakeRange(std::min(i, j), std::abs(i - j)),
GetExpectedSelectionRange(),
GetActualSelectionRange());
// Bail out early for selection-modifying commands that are buggy in
// Cocoa, since the selected text will not match.
if (test_case.is_rtl && i != j && IsRTLSelectBuggy(sel))
continue;
PerformCommand(sel);
EXPECT_NSEQ(GetExpectedSelectedText(), GetActualSelectedText());
EXPECT_NSEQ(GetExpectedText(), GetActualText());
// Spot-check some Cocoa RTL bugs. These only manifest when there is a
// selection (i != j), not for regular cursor moves.
if (test_case.is_rtl && i != j && IsRTLMoveBuggy(sel)) {
if (sel == @selector(moveRight:)) {
// Surely moving right with an rtl string moves to the start of a
// range (i.e. min). But Cocoa moves to the end.
EXPECT_EQ_RANGE(NSMakeRange(std::max(i, j), 0),
GetExpectedSelectionRange());
EXPECT_EQ_RANGE(NSMakeRange(std::min(i, j), 0),
GetActualSelectionRange());
} else if (sel == @selector(moveLeft:)) {
EXPECT_EQ_RANGE(NSMakeRange(std::min(i, j), 0),
GetExpectedSelectionRange());
EXPECT_EQ_RANGE(NSMakeRange(std::max(i, j), 0),
GetActualSelectionRange());
}
continue;
}
EXPECT_EQ_RANGE(GetExpectedSelectionRange(),
GetActualSelectionRange());
}
}
}
}
}
// The TEST_VIEW macro expects the view it's testing to have a superview. In
// these tests, the NSView bridge is a contentView, at the root. These mimic
// what TEST_VIEW usually does.
TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewAddRemove) {
base::scoped_nsobject<BridgedContentView> view([bridge()->ns_view() retain]);
base::scoped_nsobject<NSWindow> window([bridge_window() retain]);
EXPECT_NSEQ([window contentView], view);
EXPECT_NSEQ(window, [view window]);
// The superview of a contentView is an NSNextStepFrame.
EXPECT_TRUE([view superview]);
EXPECT_TRUE([view bridge]);
// Ensure the tracking area to propagate mouseMoved: events to the RootView is
// installed.
EXPECT_EQ(1u, [[view trackingAreas] count]);
// Closing the window should tear down the C++ bridge, remove references to
// any C++ objects in the ObjectiveC object, and remove it from the hierarchy.
[window close];
EXPECT_FALSE([view bridge]);
EXPECT_FALSE([view superview]);
EXPECT_FALSE([view window]);
EXPECT_EQ(0u, [[view trackingAreas] count]);
EXPECT_FALSE([window contentView]);
EXPECT_FALSE([window delegate]);
}
TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewDisplay) {
[bridge()->ns_view() display];
}
// Test that resizing the window resizes the root view appropriately.
TEST_F(BridgedNativeWidgetTest, ViewSizeTracksWindow) {
const int kTestNewWidth = 400;
const int kTestNewHeight = 300;
// |bridge_window()| is borderless, so these should align.
NSSize window_size = [bridge_window() frame].size;
EXPECT_EQ(view_->width(), static_cast<int>(window_size.width));
EXPECT_EQ(view_->height(), static_cast<int>(window_size.height));
// Make sure a resize actually occurs.
EXPECT_NE(kTestNewWidth, view_->width());
EXPECT_NE(kTestNewHeight, view_->height());
[bridge_window() setFrame:NSMakeRect(0, 0, kTestNewWidth, kTestNewHeight)
display:NO];
EXPECT_EQ(kTestNewWidth, view_->width());
EXPECT_EQ(kTestNewHeight, view_->height());
}
TEST_F(BridgedNativeWidgetTest, GetInputMethodShouldNotReturnNull) {
EXPECT_TRUE(bridge_host()->GetInputMethod());
}
// A simpler test harness for testing initialization flows.
class BridgedNativeWidgetInitTest : public BridgedNativeWidgetTestBase {
public:
BridgedNativeWidgetInitTest()
: BridgedNativeWidgetTestBase(SkipInitialization()) {}
// Prepares a new |window_| and |widget_| for a call to PerformInit().
void CreateNewWidgetToInit() {
widget_.reset(new Widget);
native_widget_mac_ = new MockNativeWidgetMac(widget_.get());
init_params_.native_widget = native_widget_mac_;
}
void PerformInit() {
widget_->Init(init_params_);
}
private:
DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetInitTest);
};
// Test that BridgedNativeWidgetImpl remains sane if Init() is never called.
TEST_F(BridgedNativeWidgetInitTest, InitNotCalled) {
// Don't use a Widget* as the delegate. ~Widget() checks for Widget::
// |native_widget_destroyed_| being set to true. That can only happen with a
// non-null WidgetDelegate, which is only set in Widget::Init(). Then, since
// neither Widget nor NativeWidget take ownership, use a unique_ptr.
std::unique_ptr<MockNativeWidgetMac> native_widget(
new MockNativeWidgetMac(nullptr));
native_widget_mac_ = native_widget.get();
EXPECT_FALSE(bridge());
EXPECT_FALSE(bridge_host()->GetLocalNSWindow());
}
// Tests the shadow type given in InitParams.
TEST_F(BridgedNativeWidgetInitTest, ShadowType) {
// Verify Widget::InitParam defaults and arguments added from SetUp().
EXPECT_EQ(Widget::InitParams::TYPE_WINDOW_FRAMELESS, init_params_.type);
EXPECT_EQ(Widget::InitParams::OPAQUE_WINDOW, init_params_.opacity);
EXPECT_EQ(Widget::InitParams::SHADOW_TYPE_DEFAULT, init_params_.shadow_type);
CreateNewWidgetToInit();
EXPECT_FALSE(
[bridge_window() hasShadow]); // Default for NSBorderlessWindowMask.
PerformInit();
// Borderless is 0, so isn't really a mask. Check that nothing is set.
EXPECT_EQ(NSBorderlessWindowMask, [bridge_window() styleMask]);
EXPECT_TRUE(
[bridge_window() hasShadow]); // SHADOW_TYPE_DEFAULT means a shadow.
CreateNewWidgetToInit();
init_params_.shadow_type = Widget::InitParams::SHADOW_TYPE_NONE;
PerformInit();
EXPECT_FALSE([bridge_window() hasShadow]); // Preserves lack of shadow.
// Default for Widget::InitParams::TYPE_WINDOW.
CreateNewWidgetToInit();
PerformInit();
EXPECT_FALSE(
[bridge_window() hasShadow]); // SHADOW_TYPE_NONE removes shadow.
init_params_.shadow_type = Widget::InitParams::SHADOW_TYPE_DEFAULT;
CreateNewWidgetToInit();
PerformInit();
EXPECT_TRUE([bridge_window() hasShadow]); // Preserves shadow.
widget_.reset();
}
// Ensure a nil NSTextInputContext is returned when the ui::TextInputClient is
// not editable, a password field, or unset.
TEST_F(BridgedNativeWidgetTest, InputContext) {
const base::string16 test_string = base::ASCIIToUTF16("test_str");
InstallTextField(test_string, ui::TEXT_INPUT_TYPE_PASSWORD);
EXPECT_FALSE([ns_view_ inputContext]);
InstallTextField(test_string, ui::TEXT_INPUT_TYPE_TEXT);
EXPECT_TRUE([ns_view_ inputContext]);
[ns_view_ setTextInputClient:nil];
EXPECT_FALSE([ns_view_ inputContext]);
InstallTextField(test_string, ui::TEXT_INPUT_TYPE_NONE);
EXPECT_FALSE([ns_view_ inputContext]);
}
// Test getting complete string using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_GetCompleteString) {
const std::string test_string = "foo bar baz";
InstallTextField(test_string);
NSRange range = NSMakeRange(0, test_string.size());
NSRange actual_range;
NSAttributedString* actual_text =
[ns_view_ attributedSubstringForProposedRange:range
actualRange:&actual_range];
NSRange expected_range;
NSAttributedString* expected_text =
[dummy_text_view_ attributedSubstringForProposedRange:range
actualRange:&expected_range];
EXPECT_NSEQ_3(@"foo bar baz", [expected_text string], [actual_text string]);
EXPECT_EQ_RANGE_3(range, expected_range, actual_range);
}
// Test getting middle substring using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_GetMiddleSubstring) {
const std::string test_string = "foo bar baz";
InstallTextField(test_string);
NSRange range = NSMakeRange(4, 3);
NSRange actual_range;
NSAttributedString* actual_text =
[ns_view_ attributedSubstringForProposedRange:range
actualRange:&actual_range];
NSRange expected_range;
NSAttributedString* expected_text =
[dummy_text_view_ attributedSubstringForProposedRange:range
actualRange:&expected_range];
EXPECT_NSEQ_3(@"bar", [expected_text string], [actual_text string]);
EXPECT_EQ_RANGE_3(range, expected_range, actual_range);
}
// Test getting ending substring using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_GetEndingSubstring) {
const std::string test_string = "foo bar baz";
InstallTextField(test_string);
NSRange range = NSMakeRange(8, 100);
NSRange actual_range;
NSAttributedString* actual_text =
[ns_view_ attributedSubstringForProposedRange:range
actualRange:&actual_range];
NSRange expected_range;
NSAttributedString* expected_text =
[dummy_text_view_ attributedSubstringForProposedRange:range
actualRange:&expected_range];
EXPECT_NSEQ_3(@"baz", [expected_text string], [actual_text string]);
EXPECT_EQ_RANGE_3(NSMakeRange(8, 3), expected_range, actual_range);
}
// Test getting empty substring using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_GetEmptySubstring) {
const std::string test_string = "foo bar baz";
InstallTextField(test_string);
// Try with EmptyRange(). This behaves specially and should return the
// complete string and the corresponding text range.
NSRange range = EmptyRange();
NSRange actual_range = EmptyRange();
NSRange expected_range = EmptyRange();
NSAttributedString* actual_text =
[ns_view_ attributedSubstringForProposedRange:range
actualRange:&actual_range];
NSAttributedString* expected_text =
[dummy_text_view_ attributedSubstringForProposedRange:range
actualRange:&expected_range];
EXPECT_NSEQ_3(@"foo bar baz", [expected_text string], [actual_text string]);
EXPECT_EQ_RANGE_3(NSMakeRange(0, test_string.length()), expected_range,
actual_range);
// Try with a valid empty range.
range = NSMakeRange(2, 0);
actual_range = EmptyRange();
expected_range = EmptyRange();
actual_text = [ns_view_ attributedSubstringForProposedRange:range
actualRange:&actual_range];
expected_text =
[dummy_text_view_ attributedSubstringForProposedRange:range
actualRange:&expected_range];
EXPECT_NSEQ_3(nil, [expected_text string], [actual_text string]);
EXPECT_EQ_RANGE_3(range, expected_range, actual_range);
// Try with an out of bounds empty range.
range = NSMakeRange(20, 0);
actual_range = EmptyRange();
expected_range = EmptyRange();
actual_text = [ns_view_ attributedSubstringForProposedRange:range
actualRange:&actual_range];
expected_text =
[dummy_text_view_ attributedSubstringForProposedRange:range
actualRange:&expected_range];
EXPECT_NSEQ_3(nil, [expected_text string], [actual_text string]);
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), expected_range, actual_range);
}
// Test inserting text using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_InsertText) {
const std::string test_string = "foo";
InstallTextField(test_string);
[ns_view_ insertText:SysUTF8ToNSString(test_string)
replacementRange:EmptyRange()];
[dummy_text_view_ insertText:SysUTF8ToNSString(test_string)
replacementRange:EmptyRange()];
EXPECT_NSEQ_3(@"foofoo", GetExpectedText(), GetActualText());
}
// Test replacing text using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_ReplaceText) {
const std::string test_string = "foo bar";
InstallTextField(test_string);
[ns_view_ insertText:@"baz" replacementRange:NSMakeRange(4, 3)];
[dummy_text_view_ insertText:@"baz" replacementRange:NSMakeRange(4, 3)];
EXPECT_NSEQ_3(@"foo baz", GetExpectedText(), GetActualText());
}
// Test IME composition using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_Compose) {
const std::string test_string = "foo ";
InstallTextField(test_string);
EXPECT_EQ_3(NO, [dummy_text_view_ hasMarkedText], [ns_view_ hasMarkedText]);
// As per NSTextInputClient documentation, markedRange should return
// {NSNotFound, 0} iff hasMarked returns false. However, NSTextView returns
// {text_length, text_length} for this case. We maintain consistency with the
// documentation, hence the EXPECT_FALSE check.
EXPECT_FALSE(
NSEqualRanges([dummy_text_view_ markedRange], [ns_view_ markedRange]));
EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]);
// Start composition.
NSString* compositionText = @"bar";
NSUInteger compositionLength = [compositionText length];
[ns_view_ setMarkedText:compositionText
selectedRange:NSMakeRange(0, 2)
replacementRange:EmptyRange()];
[dummy_text_view_ setMarkedText:compositionText
selectedRange:NSMakeRange(0, 2)
replacementRange:EmptyRange()];
EXPECT_EQ_3(YES, [dummy_text_view_ hasMarkedText], [ns_view_ hasMarkedText]);
EXPECT_EQ_RANGE_3(NSMakeRange(test_string.size(), compositionLength),
[dummy_text_view_ markedRange], [ns_view_ markedRange]);
EXPECT_EQ_RANGE_3(NSMakeRange(test_string.size(), 2),
GetExpectedSelectionRange(), GetActualSelectionRange());
// Confirm composition.
[ns_view_ unmarkText];
[dummy_text_view_ unmarkText];
EXPECT_EQ_3(NO, [dummy_text_view_ hasMarkedText], [ns_view_ hasMarkedText]);
EXPECT_FALSE(
NSEqualRanges([dummy_text_view_ markedRange], [ns_view_ markedRange]));
EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]);
EXPECT_NSEQ_3(@"foo bar", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange([GetActualText() length], 0),
GetExpectedSelectionRange(), GetActualSelectionRange());
}
// Test IME composition for accented characters.
TEST_F(BridgedNativeWidgetTest, TextInput_AccentedCharacter) {
InstallTextField("abc");
// Simulate action messages generated when the key 'a' is pressed repeatedly
// and leads to the showing of an IME candidate window. To simulate an event,
// set the private keyDownEvent field on the BridgedContentView.
// First an insertText: message with key 'a' is generated.
SetKeyDownEvent(cocoa_test_event_utils::SynthesizeKeyEvent(
widget_->GetNativeWindow().GetNativeNSWindow(), true, ui::VKEY_A, 0));
[ns_view_ insertText:@"a" replacementRange:EmptyRange()];
[dummy_text_view_ insertText:@"a" replacementRange:EmptyRange()];
SetKeyDownEvent(nil);
EXPECT_EQ_3(NO, [dummy_text_view_ hasMarkedText], [ns_view_ hasMarkedText]);
EXPECT_NSEQ_3(@"abca", GetExpectedText(), GetActualText());
// Next the IME popup appears. On selecting the accented character using arrow
// keys, setMarkedText action message is generated which replaces the earlier
// inserted 'a'.
SetKeyDownEvent(cocoa_test_event_utils::SynthesizeKeyEvent(
widget_->GetNativeWindow().GetNativeNSWindow(), true, ui::VKEY_RIGHT, 0));
[ns_view_ setMarkedText:@"à"
selectedRange:NSMakeRange(0, 1)
replacementRange:NSMakeRange(3, 1)];
[dummy_text_view_ setMarkedText:@"à"
selectedRange:NSMakeRange(0, 1)
replacementRange:NSMakeRange(3, 1)];
SetKeyDownEvent(nil);
EXPECT_EQ_3(YES, [dummy_text_view_ hasMarkedText], [ns_view_ hasMarkedText]);
EXPECT_EQ_RANGE_3(NSMakeRange(3, 1), [dummy_text_view_ markedRange],
[ns_view_ markedRange]);
EXPECT_EQ_RANGE_3(NSMakeRange(3, 1), GetExpectedSelectionRange(),
GetActualSelectionRange());
EXPECT_NSEQ_3(@"abcà", GetExpectedText(), GetActualText());
// On pressing enter, the marked text is confirmed.
SetKeyDownEvent(cocoa_test_event_utils::SynthesizeKeyEvent(
widget_->GetNativeWindow().GetNativeNSWindow(), true, ui::VKEY_RETURN,
0));
[ns_view_ insertText:@"à" replacementRange:EmptyRange()];
[dummy_text_view_ insertText:@"à" replacementRange:EmptyRange()];
SetKeyDownEvent(nil);
EXPECT_EQ_3(NO, [dummy_text_view_ hasMarkedText], [ns_view_ hasMarkedText]);
EXPECT_EQ_RANGE_3(NSMakeRange(4, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
EXPECT_NSEQ_3(@"abcà", GetExpectedText(), GetActualText());
}
// Test moving the caret left and right using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_MoveLeftRight) {
InstallTextField("foo");
EXPECT_EQ_RANGE_3(NSMakeRange(3, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move right not allowed, out of range.
PerformCommand(@selector(moveRight:));
EXPECT_EQ_RANGE_3(NSMakeRange(3, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move left.
PerformCommand(@selector(moveLeft:));
EXPECT_EQ_RANGE_3(NSMakeRange(2, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move right.
PerformCommand(@selector(moveRight:));
EXPECT_EQ_RANGE_3(NSMakeRange(3, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
// Test backward delete using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteBackward) {
InstallTextField("a");
EXPECT_EQ_RANGE_3(NSMakeRange(1, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Delete one character.
PerformCommand(@selector(deleteBackward:));
EXPECT_NSEQ_3(nil, GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Verify that deletion did not modify the kill buffer.
PerformCommand(@selector(yank:));
EXPECT_NSEQ_3(nil, GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Try to delete again on an empty string.
PerformCommand(@selector(deleteBackward:));
EXPECT_NSEQ_3(nil, GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
// Test forward delete using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteForward) {
InstallTextField("a");
EXPECT_EQ_RANGE_3(NSMakeRange(1, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// At the end of the string, can't delete forward.
PerformCommand(@selector(deleteForward:));
EXPECT_NSEQ_3(@"a", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(1, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Should succeed after moving left first.
PerformCommand(@selector(moveLeft:));
PerformCommand(@selector(deleteForward:));
EXPECT_NSEQ_3(nil, GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Verify that deletion did not modify the kill buffer.
PerformCommand(@selector(yank:));
EXPECT_NSEQ_3(nil, GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
// Test forward word deletion using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteWordForward) {
InstallTextField("foo bar baz");
EXPECT_EQ_RANGE_3(NSMakeRange(11, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Caret is at the end of the line. Verify no deletion takes place.
PerformCommand(@selector(deleteWordForward:));
EXPECT_NSEQ_3(@"foo bar baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(11, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move the caret as- "foo b|ar baz".
SetSelectionRange(NSMakeRange(5, 0));
PerformCommand(@selector(deleteWordForward:));
// Verify state is "foo b| baz"
EXPECT_NSEQ_3(@"foo b baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(5, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Make a selection as- "|fo|o b baz".
SetSelectionRange(NSMakeRange(0, 2));
PerformCommand(@selector(deleteWordForward:));
// Verify only the selection is deleted and state is "|o b baz".
EXPECT_NSEQ_3(@"o b baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Verify that deletion did not modify the kill buffer.
PerformCommand(@selector(yank:));
EXPECT_NSEQ_3(@"o b baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
// Test backward word deletion using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteWordBackward) {
InstallTextField("foo bar baz");
EXPECT_EQ_RANGE_3(NSMakeRange(11, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move the caret to the beginning of the line.
SetSelectionRange(NSMakeRange(0, 0));
// Verify no deletion takes place.
PerformCommand(@selector(deleteWordBackward:));
EXPECT_NSEQ_3(@"foo bar baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(0, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Move the caret as- "foo ba|r baz".
SetSelectionRange(NSMakeRange(6, 0));
PerformCommand(@selector(deleteWordBackward:));
// Verify state is "foo |r baz".
EXPECT_NSEQ_3(@"foo r baz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(4, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Make a selection as- "f|oo r b|az".
SetSelectionRange(NSMakeRange(1, 6));
PerformCommand(@selector(deleteWordBackward:));
// Verify only the selection is deleted and state is "f|az"
EXPECT_NSEQ_3(@"faz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(1, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
// Verify that deletion did not modify the kill buffer.
PerformCommand(@selector(yank:));
EXPECT_NSEQ_3(@"faz", GetExpectedText(), GetActualText());
EXPECT_EQ_RANGE_3(NSMakeRange(1, 0), GetExpectedSelectionRange(),
GetActualSelectionRange());
}
// Test deleting to beginning/end of line/paragraph using text input protocol.
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteToBeginningOfLine) {
TestDeleteBeginning(@selector(deleteToBeginningOfLine:));
}
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteToEndOfLine) {
TestDeleteEnd(@selector(deleteToEndOfLine:));
}
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteToBeginningOfParagraph) {
TestDeleteBeginning(@selector(deleteToBeginningOfParagraph:));
}
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteToEndOfParagraph) {
TestDeleteEnd(@selector(deleteToEndOfParagraph:));
}
// Test move commands against expectations set by |dummy_text_view_|.
TEST_F(BridgedNativeWidgetTest, TextInput_MoveEditingCommands) {
TestEditingCommands(kMoveActions);
}
// Test move and select commands against expectations set by |dummy_text_view_|.
TEST_F(BridgedNativeWidgetTest, TextInput_MoveAndSelectEditingCommands) {
TestEditingCommands(kSelectActions);
}
// Test delete commands against expectations set by |dummy_text_view_|.
TEST_F(BridgedNativeWidgetTest, TextInput_DeleteCommands) {
TestEditingCommands(kDeleteActions);
}
// Test that we don't crash during an action message even if the TextInputClient
// is nil. Regression test for crbug.com/615745.
TEST_F(BridgedNativeWidgetTest, NilTextInputClient) {
[ns_view_ setTextInputClient:nil];
NSMutableArray* selectors = [NSMutableArray array];
[selectors addObjectsFromArray:kMoveActions];
[selectors addObjectsFromArray:kSelectActions];
[selectors addObjectsFromArray:kDeleteActions];
// -insertText: is omitted from this list to avoid a DCHECK in
// doCommandBySelector:. AppKit never passes -insertText: to
// doCommandBySelector: (it calls -insertText: directly instead).
[selectors addObjectsFromArray:kMiscActions];
for (NSString* selector in selectors)
[ns_view_ doCommandBySelector:NSSelectorFromString(selector)];
[ns_view_ insertText:@""];
}
// Test transpose command against expectations set by |dummy_text_view_|.
TEST_F(BridgedNativeWidgetTest, TextInput_Transpose) {
TestEditingCommands(@[ @"transpose:" ]);
}
// Test firstRectForCharacterRange:actualRange for cases where query range is
// empty or outside composition range.
TEST_F(BridgedNativeWidgetTest, TextInput_FirstRectForCharacterRange_Caret) {
InstallTextField("");
ui::TextInputClient* client = [ns_view_ textInputClient];
// No composition. Ensure bounds and range corresponding to the current caret
// position are returned.
// Initially selection range will be [0, 0].
NSRange caret_range = NSMakeRange(0, 0);
NSRange query_range = NSMakeRange(1, 1);
NSRange actual_range;
NSRect rect = [ns_view_ firstRectForCharacterRange:query_range
actualRange:&actual_range];
EXPECT_EQ(GetCaretBounds(client), gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ_RANGE(caret_range, actual_range);
// Set composition with caret before second character ('e').
const base::string16 test_string = base::ASCIIToUTF16("test_str");
const size_t kTextLength = 8;
SetCompositionText(client, test_string, 1, &caret_range);
// Test bounds returned for empty ranges within composition range. As per
// Apple's documentation for firstRectForCharacterRange:actualRange:, for an
// empty query range, the returned rectangle should coincide with the
// insertion point and have zero width. However in our implementation, if the
// empty query range lies within the composition range, we return a zero width
// rectangle corresponding to the query range location.
// Test bounds returned for empty range before second character ('e') are same
// as caret bounds when placed before second character.
query_range = NSMakeRange(1, 0);
rect = [ns_view_ firstRectForCharacterRange:query_range
actualRange:&actual_range];
EXPECT_EQ(GetCaretBoundsForPosition(client, test_string, 1, &caret_range),
gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ_RANGE(query_range, actual_range);
// Test bounds returned for empty range after the composition text are same as
// caret bounds when placed after the composition text.
query_range = NSMakeRange(kTextLength, 0);
rect = [ns_view_ firstRectForCharacterRange:query_range
actualRange:&actual_range];
EXPECT_NE(GetCaretBoundsForPosition(client, test_string, 1, &caret_range),
gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ(
GetCaretBoundsForPosition(client, test_string, kTextLength, &caret_range),
gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ_RANGE(query_range, actual_range);
// Query outside composition range. Ensure bounds and range corresponding to
// the current caret position are returned.
query_range = NSMakeRange(kTextLength + 1, 0);
rect = [ns_view_ firstRectForCharacterRange:query_range
actualRange:&actual_range];
EXPECT_EQ(GetCaretBounds(client), gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ_RANGE(caret_range, actual_range);
// Make sure not crashing by passing null pointer instead of actualRange.
rect = [ns_view_ firstRectForCharacterRange:query_range actualRange:nullptr];
}
// Test firstRectForCharacterRange:actualRange for non-empty query ranges within
// the composition range.
TEST_F(BridgedNativeWidgetTest, TextInput_FirstRectForCharacterRange) {
InstallTextField("");
ui::TextInputClient* client = [ns_view_ textInputClient];
const base::string16 test_string = base::ASCIIToUTF16("test_str");
const size_t kTextLength = 8;
SetCompositionText(client, test_string, 1, nullptr);
// Query bounds for the whole composition string.
NSRange query_range = NSMakeRange(0, kTextLength);
NSRange actual_range;
NSRect rect = [ns_view_ firstRectForCharacterRange:query_range
actualRange:&actual_range];
EXPECT_EQ(GetExpectedBoundsForRange(client, test_string, query_range),
gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ_RANGE(query_range, actual_range);
// Query bounds for the substring "est_".
query_range = NSMakeRange(1, 4);
rect = [ns_view_ firstRectForCharacterRange:query_range
actualRange:&actual_range];
EXPECT_EQ(GetExpectedBoundsForRange(client, test_string, query_range),
gfx::ScreenRectFromNSRect(rect));
EXPECT_EQ_RANGE(query_range, actual_range);
}
// Test simulated codepaths for IMEs that do not always "mark" text. E.g.
// phonetic languages such as Korean and Vietnamese.
TEST_F(BridgedNativeWidgetTest, TextInput_SimulatePhoneticIme) {
Textfield* textfield = InstallTextField("");
EXPECT_TRUE([ns_view_ textInputClient]);
object_setClass(ns_view_, [InterpretKeyEventMockedBridgedContentView class]);
// Sequence of calls (and corresponding keyDown events) obtained via tracing
// with 2-Set Korean IME and pressing q, o, then Enter on the keyboard.
NSEvent* q_in_ime = UnicodeKeyDown(12, @"ㅂ");
InterpretKeyEventsCallback handle_q_in_ime = base::BindRepeating([](id view) {
[view insertText:@"ㅂ" replacementRange:NSMakeRange(NSNotFound, 0)];
});
NSEvent* o_in_ime = UnicodeKeyDown(31, @"ㅐ");
InterpretKeyEventsCallback handle_o_in_ime = base::BindRepeating([](id view) {
[view insertText:@"배" replacementRange:NSMakeRange(0, 1)];
});
InterpretKeyEventsCallback handle_return_in_ime =
base::BindRepeating([](id view) {
// When confirming the composition, AppKit repeats itself.
[view insertText:@"배" replacementRange:NSMakeRange(0, 1)];
[view insertText:@"배" replacementRange:NSMakeRange(0, 1)];
[view doCommandBySelector:@selector(insertNewLine:)];
});
// Add a hook for the KeyEvent being received by the TextfieldController. E.g.
// this is where the Omnibox would start to search when Return is pressed.
bool saw_vkey_return = false;
SetHandleKeyEventCallback(base::BindRepeating(
[](bool* saw_return, Textfield* textfield, const ui::KeyEvent& event) {
if (event.key_code() == ui::VKEY_RETURN) {
EXPECT_FALSE(*saw_return);
*saw_return = true;
EXPECT_EQ(base::SysNSStringToUTF16(@"배"), textfield->text());
}
return false;
},
&saw_vkey_return));
EXPECT_EQ(base::UTF8ToUTF16(""), textfield->text());
g_fake_interpret_key_events = &handle_q_in_ime;
[ns_view_ keyDown:q_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"ㅂ"), textfield->text());
EXPECT_FALSE(saw_vkey_return);
g_fake_interpret_key_events = &handle_o_in_ime;
[ns_view_ keyDown:o_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"배"), textfield->text());
EXPECT_FALSE(saw_vkey_return);
// Note the "Enter" should not replace the replacement range, even though a
// replacement range was set.
g_fake_interpret_key_events = &handle_return_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_RETURN)];
EXPECT_EQ(base::SysNSStringToUTF16(@"배"), textfield->text());
// VKEY_RETURN should be seen by via the unhandled key event handler (but not
// via -insertText:.
EXPECT_TRUE(saw_vkey_return);
g_fake_interpret_key_events = nullptr;
}
// Test simulated codepaths for typing 'm', 'o', 'o', Enter in the Telex IME.
// This IME does not mark text, but, unlike 2-set Korean, it re-inserts the
// entire word on each keypress, even though only the last character in the word
// can be modified. This prevents the keypress being treated as a "character"
// event (which is unavoidably unfortunate for the Undo buffer), but also led to
// a codepath that suppressed a VKEY_RETURN when it should not, since there is
// no candidate IME window to dismiss for this IME.
TEST_F(BridgedNativeWidgetTest, TextInput_SimulateTelexMoo) {
Textfield* textfield = InstallTextField("");
EXPECT_TRUE([ns_view_ textInputClient]);
EnterAcceleratorView* enter_view = new EnterAcceleratorView();
textfield->parent()->AddChildView(enter_view);
// Sequence of calls (and corresponding keyDown events) obtained via tracing
// with Telex IME and pressing 'm', 'o', 'o', then Enter on the keyboard.
// Note that without the leading 'm', only one character changes, which could
// allow the keypress to be treated as a character event, which would not
// produce the bug.
NSEvent* m_in_ime = UnicodeKeyDown(46, @"m");
InterpretKeyEventsCallback handle_m_in_ime = base::BindRepeating([](id view) {
[view insertText:@"m" replacementRange:NSMakeRange(NSNotFound, 0)];
});
// Note that (unlike Korean IME), Telex generates a latin "o" for both events:
// it doesn't associate a unicode character on the second NSEvent.
NSEvent* o_in_ime = UnicodeKeyDown(31, @"o");
InterpretKeyEventsCallback handle_first_o_in_ime =
base::BindRepeating([](id view) {
// Note the whole word is replaced, not just the last character.
[view insertText:@"mo" replacementRange:NSMakeRange(0, 1)];
});
InterpretKeyEventsCallback handle_second_o_in_ime =
base::BindRepeating([](id view) {
[view insertText:@"mô" replacementRange:NSMakeRange(0, 2)];
});
InterpretKeyEventsCallback handle_return_in_ime =
base::BindRepeating([](id view) {
// Note the previous -insertText: repeats, even though it is unchanged.
// But the IME also follows with an -insertNewLine:.
[view insertText:@"mô" replacementRange:NSMakeRange(0, 2)];
[view doCommandBySelector:@selector(insertNewLine:)];
});
EXPECT_EQ(base::UTF8ToUTF16(""), textfield->text());
EXPECT_EQ(0, enter_view->count());
object_setClass(ns_view_, [InterpretKeyEventMockedBridgedContentView class]);
g_fake_interpret_key_events = &handle_m_in_ime;
[ns_view_ keyDown:m_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"m"), textfield->text());
EXPECT_EQ(0, enter_view->count());
g_fake_interpret_key_events = &handle_first_o_in_ime;
[ns_view_ keyDown:o_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"mo"), textfield->text());
EXPECT_EQ(0, enter_view->count());
g_fake_interpret_key_events = &handle_second_o_in_ime;
[ns_view_ keyDown:o_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"mô"), textfield->text());
EXPECT_EQ(0, enter_view->count());
g_fake_interpret_key_events = &handle_return_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_RETURN)];
EXPECT_EQ(base::SysNSStringToUTF16(@"mô"), textfield->text()); // No change.
EXPECT_EQ(1, enter_view->count()); // Now we see the accelerator.
}
// Simulate 'a', Enter in Hiragana. This should just insert "あ", suppressing
// accelerators.
TEST_F(BridgedNativeWidgetTest, TextInput_NoAcceleratorEnterComposition) {
Textfield* textfield = InstallTextField("");
EXPECT_TRUE([ns_view_ textInputClient]);
EnterAcceleratorView* enter_view = new EnterAcceleratorView();
textfield->parent()->AddChildView(enter_view);
// Sequence of calls (and corresponding keyDown events) obtained via tracing
// with Hiragana IME and pressing 'a', then Enter on the keyboard.
// Note 0 is the actual keyCode for 'a', not a placeholder.
NSEvent* a_in_ime = UnicodeKeyDown(0, @"a");
InterpretKeyEventsCallback handle_a_in_ime = base::BindRepeating([](id view) {
// TODO(crbug/612675): |text| should be an NSAttributedString.
[view setMarkedText:@"あ"
selectedRange:NSMakeRange(1, 0)
replacementRange:NSMakeRange(NSNotFound, 0)];
});
InterpretKeyEventsCallback handle_first_return_in_ime =
base::BindRepeating([](id view) {
[view insertText:@"あ" replacementRange:NSMakeRange(NSNotFound, 0)];
// Note there is no call to -insertNewLine: here.
});
InterpretKeyEventsCallback handle_second_return_in_ime = base::BindRepeating(
[](id view) { [view doCommandBySelector:@selector(insertNewLine:)]; });
EXPECT_EQ(base::UTF8ToUTF16(""), textfield->text());
EXPECT_EQ(0, enter_view->count());
object_setClass(ns_view_, [InterpretKeyEventMockedBridgedContentView class]);
g_fake_interpret_key_events = &handle_a_in_ime;
[ns_view_ keyDown:a_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"あ"), textfield->text());
EXPECT_EQ(0, enter_view->count());
g_fake_interpret_key_events = &handle_first_return_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_RETURN)];
EXPECT_EQ(base::SysNSStringToUTF16(@"あ"), textfield->text());
EXPECT_EQ(0, enter_view->count()); // Not seen as an accelerator.
g_fake_interpret_key_events = &handle_second_return_in_ime;
[ns_view_
keyDown:VkeyKeyDown(ui::VKEY_RETURN)]; // Sanity check: send Enter again.
EXPECT_EQ(base::SysNSStringToUTF16(@"あ"), textfield->text()); // No change.
EXPECT_EQ(1, enter_view->count()); // Now we see the accelerator.
}
// Simulate 'a', Tab, Enter, Enter in Hiragana. This should just insert "a",
// suppressing accelerators.
TEST_F(BridgedNativeWidgetTest, TextInput_NoAcceleratorTabEnterComposition) {
Textfield* textfield = InstallTextField("");
EXPECT_TRUE([ns_view_ textInputClient]);
EnterAcceleratorView* enter_view = new EnterAcceleratorView();
textfield->parent()->AddChildView(enter_view);
// Sequence of calls (and corresponding keyDown events) obtained via tracing
// with Hiragana IME and pressing 'a', Tab, then Enter on the keyboard.
NSEvent* a_in_ime = UnicodeKeyDown(0, @"a");
InterpretKeyEventsCallback handle_a_in_ime = base::BindRepeating([](id view) {
// TODO(crbug/612675): |text| should have an underline.
[view setMarkedText:@"あ"
selectedRange:NSMakeRange(1, 0)
replacementRange:NSMakeRange(NSNotFound, 0)];
});
InterpretKeyEventsCallback handle_tab_in_ime =
base::BindRepeating([](id view) {
// TODO(crbug/612675): |text| should be an NSAttributedString (now with
// a different underline color).
[view setMarkedText:@"a"
selectedRange:NSMakeRange(0, 1)
replacementRange:NSMakeRange(NSNotFound, 0)];
// Note there is no -insertTab: generated.
});
InterpretKeyEventsCallback handle_first_return_in_ime =
base::BindRepeating([](id view) {
// Do *nothing*. Enter does not confirm nor change the composition, it
// just dismisses the IME window, leaving the text marked.
});
InterpretKeyEventsCallback handle_second_return_in_ime =
base::BindRepeating([](id view) {
// The second return will confirm the composition.
[view insertText:@"a" replacementRange:NSMakeRange(NSNotFound, 0)];
});
InterpretKeyEventsCallback handle_third_return_in_ime =
base::BindRepeating([](id view) {
// Only the third return will generate -insertNewLine:.
[view doCommandBySelector:@selector(insertNewLine:)];
});
EXPECT_EQ(base::UTF8ToUTF16(""), textfield->text());
EXPECT_EQ(0, enter_view->count());
object_setClass(ns_view_, [InterpretKeyEventMockedBridgedContentView class]);
g_fake_interpret_key_events = &handle_a_in_ime;
[ns_view_ keyDown:a_in_ime];
EXPECT_EQ(base::SysNSStringToUTF16(@"あ"), textfield->text());
EXPECT_EQ(0, enter_view->count());
g_fake_interpret_key_events = &handle_tab_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_TAB)];
// Tab will switch to a Romanji (Latin) character.
EXPECT_EQ(base::SysNSStringToUTF16(@"a"), textfield->text());
EXPECT_EQ(0, enter_view->count());
g_fake_interpret_key_events = &handle_first_return_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_RETURN)];
// Enter just dismisses the IME window. The composition is still active.
EXPECT_EQ(base::SysNSStringToUTF16(@"a"), textfield->text());
EXPECT_EQ(0, enter_view->count()); // Not seen as an accelerator.
g_fake_interpret_key_events = &handle_second_return_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_RETURN)];
// Enter now confirms the composition (unmarks text). Note there is still no
// IME window visible but, since there is marked text, IME is still active.
EXPECT_EQ(base::SysNSStringToUTF16(@"a"), textfield->text());
EXPECT_EQ(0, enter_view->count()); // Not seen as an accelerator.
g_fake_interpret_key_events = &handle_third_return_in_ime;
[ns_view_ keyDown:VkeyKeyDown(ui::VKEY_RETURN)]; // Send Enter a third time.
EXPECT_EQ(base::SysNSStringToUTF16(@"a"), textfield->text()); // No change.
EXPECT_EQ(1, enter_view->count()); // Now we see the accelerator.
}
// Test a codepath that could hypothetically cause [NSApp updateWindows] to be
// called recursively due to IME dismissal during teardown triggering a focus
// change. Twice.
TEST_F(BridgedNativeWidgetTest, TextInput_RecursiveUpdateWindows) {
Textfield* textfield = InstallTextField("");
EXPECT_TRUE([ns_view_ textInputClient]);
object_setClass(ns_view_, [InterpretKeyEventMockedBridgedContentView class]);
base::mac::ScopedObjCClassSwizzler update_windows_swizzler(
[NSApplication class], [UpdateWindowsDonorForNSApp class],
@selector(updateWindows));
base::mac::ScopedObjCClassSwizzler current_input_context_swizzler(
[NSTextInputContext class],
[CurrentInputContextDonorForNSTextInputContext class],
@selector(currentInputContext));
int vkey_return_count = 0;
// Everything happens with this one event.
NSEvent* return_with_fake_ime = cocoa_test_event_utils::SynthesizeKeyEvent(
widget_->GetNativeWindow().GetNativeNSWindow(), true, ui::VKEY_RETURN, 0);
InterpretKeyEventsCallback generate_return_and_fake_ime = base::BindRepeating(
[](int* saw_return_count, id view) {
EXPECT_EQ(0, *saw_return_count);
// First generate the return to simulate an input context change.
[view insertText:@"\r" replacementRange:NSMakeRange(NSNotFound, 0)];
EXPECT_EQ(1, *saw_return_count);
},
&vkey_return_count);
bool saw_update_windows = false;
base::RepeatingClosure update_windows_closure = base::BindRepeating(
[](bool* saw_update_windows, BridgedContentView* view,
Textfield* textfield) {
// Ensure updateWindows is not invoked recursively.
EXPECT_FALSE(*saw_update_windows);
*saw_update_windows = true;
// Inside updateWindows, assume the IME got dismissed and wants to
// insert its last bit of text for the old input context.
[view insertText:@"배" replacementRange:NSMakeRange(0, 1)];
// This is triggered by the setTextInputClient:nullptr in
// SetHandleKeyEventCallback(), so -inputContext should also be nil.
EXPECT_FALSE([view inputContext]);
// Ensure we can't recursively call updateWindows. A TextInputClient
// reacting to InsertChar could theoretically do this, but toolkit-views
// DCHECKs if there is recursive event dispatch, so call
// setTextInputClient directly.
[view setTextInputClient:textfield];
// Finally simulate what -[NSApp updateWindows] should _actually_ do,
// which is to update the input context (from the first responder).
g_fake_current_input_context = [view inputContext];
// Now, the |textfield| set above should have been set again.
EXPECT_TRUE(g_fake_current_input_context);
},
&saw_update_windows, ns_view_, textfield);
SetHandleKeyEventCallback(base::BindRepeating(
[](int* saw_return_count, BridgedContentView* view, Textfield* textfield,
const ui::KeyEvent& event) {
if (event.key_code() == ui::VKEY_RETURN) {
*saw_return_count += 1;
// Simulate Textfield::OnBlur() by clearing the input method.
// Textfield needs to be in a Widget to do this normally.
[view setTextInputClient:nullptr];
}
return false;
},
&vkey_return_count, ns_view_));
// Starting text (just insert it).
[ns_view_ insertText:@"ㅂ" replacementRange:NSMakeRange(NSNotFound, 0)];
EXPECT_EQ(base::SysNSStringToUTF16(@"ㅂ"), textfield->text());
g_fake_interpret_key_events = &generate_return_and_fake_ime;
g_update_windows_closure = &update_windows_closure;
g_fake_current_input_context = [ns_view_ inputContext];
EXPECT_TRUE(g_fake_current_input_context);
[ns_view_ keyDown:return_with_fake_ime];
// We should see one VKEY_RETURNs and one updateWindows. In particular, note
// that there is not a second VKEY_RETURN seen generated by keyDown: thinking
// the event has been unhandled. This is because it was handled when the fake
// IME sent \r.
EXPECT_TRUE(saw_update_windows);
EXPECT_EQ(1, vkey_return_count);
// The text inserted during updateWindows should have been inserted, even
// though we were trying to change the input context.
EXPECT_EQ(base::SysNSStringToUTF16(@"배"), textfield->text());
EXPECT_TRUE(g_fake_current_input_context);
g_fake_current_input_context = nullptr;
g_fake_interpret_key_events = nullptr;
g_update_windows_closure = nullptr;
}
typedef BridgedNativeWidgetTestBase BridgedNativeWidgetSimulateFullscreenTest;
// Simulate the notifications that AppKit would send out if a fullscreen
// operation begins, and then fails and must abort. This notification sequence
// was determined by posting delayed tasks to toggle fullscreen state and then
// mashing Ctrl+Left/Right to keep OSX in a transition between Spaces to cause
// the fullscreen transition to fail.
TEST_F(BridgedNativeWidgetSimulateFullscreenTest, FailToEnterAndExit) {
BridgedNativeWidgetTestWindow* window =
base::mac::ObjCCastStrict<BridgedNativeWidgetTestWindow>(
widget_->GetNativeWindow().GetNativeNSWindow());
[window setIgnoreToggleFullScreen:YES];
widget_->Show();
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
EXPECT_FALSE(bridge()->target_fullscreen_state());
// Simulate an initial toggleFullScreen: (user- or Widget-initiated).
[center postNotificationName:NSWindowWillEnterFullScreenNotification
object:window];
// On a failure, Cocoa starts by sending an unexpected *exit* fullscreen, and
// BridgedNativeWidgetImpl will think it's just a delayed transition and try
// to go back into fullscreen but get ignored by Cocoa.
EXPECT_EQ(0, [window ignoredToggleFullScreenCount]);
EXPECT_TRUE(bridge()->target_fullscreen_state());
[center postNotificationName:NSWindowDidExitFullScreenNotification
object:window];
EXPECT_EQ(1, [window ignoredToggleFullScreenCount]);
EXPECT_FALSE(bridge()->target_fullscreen_state());
// Cocoa follows up with a failure message sent to the NSWindowDelegate (there
// is no equivalent notification for failure).
ViewsNSWindowDelegate* window_delegate =
base::mac::ObjCCast<ViewsNSWindowDelegate>([window delegate]);
[window_delegate windowDidFailToEnterFullScreen:window];
EXPECT_FALSE(bridge()->target_fullscreen_state());
// Now perform a successful fullscreen operation.
[center postNotificationName:NSWindowWillEnterFullScreenNotification
object:window];
EXPECT_TRUE(bridge()->target_fullscreen_state());
[center postNotificationName:NSWindowDidEnterFullScreenNotification
object:window];
EXPECT_TRUE(bridge()->target_fullscreen_state());
// And try to get out.
[center postNotificationName:NSWindowWillExitFullScreenNotification
object:window];
EXPECT_FALSE(bridge()->target_fullscreen_state());
// On a failure, Cocoa sends a failure message, but then just dumps the window
// out of fullscreen anyway (in that order).
[window_delegate windowDidFailToExitFullScreen:window];
EXPECT_FALSE(bridge()->target_fullscreen_state());
[center postNotificationName:NSWindowDidExitFullScreenNotification
object:window];
EXPECT_EQ(1, [window ignoredToggleFullScreenCount]); // No change.
EXPECT_FALSE(bridge()->target_fullscreen_state());
widget_->CloseNow();
}
} // namespace test
} // namespace views