| // Copyright (c) 2012 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. |
| |
| #include "content/browser/renderer_host/render_widget_host_view_mac.h" |
| |
| #include <Cocoa/Cocoa.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <tuple> |
| |
| #include "base/command_line.h" |
| #include "base/containers/queue.h" |
| #include "base/mac/mac_util.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/mac/sdk_forward_declarations.h" |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "content/browser/browser_thread_impl.h" |
| #include "content/browser/compositor/test/no_transport_image_transport_factory.h" |
| #include "content/browser/frame_host/render_widget_host_view_guest.h" |
| #include "content/browser/gpu/compositor_util.h" |
| #include "content/browser/renderer_host/render_widget_host_delegate.h" |
| #include "content/browser/renderer_host/text_input_manager.h" |
| #include "content/common/input_messages.h" |
| #include "content/common/text_input_state.h" |
| #include "content/common/view_messages.h" |
| #include "content/public/browser/notification_types.h" |
| #include "content/public/browser/render_widget_host_view_mac_delegate.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/mock_render_process_host.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/test/mock_widget_impl.h" |
| #include "content/test/test_render_view_host.h" |
| #include "gpu/ipc/common/gpu_messages.h" |
| #include "gpu/ipc/service/image_transport_surface.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/gtest_mac.h" |
| #import "third_party/ocmock/OCMock/OCMock.h" |
| #import "third_party/ocmock/ocmock_extensions.h" |
| #import "ui/base/test/scoped_fake_nswindow_focus.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/blink/blink_features.h" |
| #include "ui/events/blink/web_input_event_traits.h" |
| #include "ui/events/test/cocoa_test_event_utils.h" |
| #import "ui/gfx/test/ui_cocoa_test_helper.h" |
| #include "ui/latency/latency_info.h" |
| |
| // Helper class with methods used to mock -[NSEvent phase], used by |
| // |MockScrollWheelEventWithPhase()|. |
| @interface MockPhaseMethods : NSObject { |
| } |
| |
| - (NSEventPhase)phaseNone; |
| - (NSEventPhase)phaseBegan; |
| - (NSEventPhase)phaseChanged; |
| - (NSEventPhase)phaseEnded; |
| @end |
| |
| @implementation MockPhaseMethods |
| |
| - (NSEventPhase)phaseNone { |
| return NSEventPhaseNone; |
| } |
| - (NSEventPhase)phaseBegan { |
| return NSEventPhaseBegan; |
| } |
| - (NSEventPhase)phaseChanged { |
| return NSEventPhaseChanged; |
| } |
| - (NSEventPhase)phaseEnded { |
| return NSEventPhaseEnded; |
| } |
| |
| @end |
| |
| @interface MockRenderWidgetHostViewMacDelegate |
| : NSObject<RenderWidgetHostViewMacDelegate> { |
| BOOL unhandledWheelEventReceived_; |
| } |
| |
| @property(nonatomic) BOOL unhandledWheelEventReceived; |
| |
| @end |
| |
| @implementation MockRenderWidgetHostViewMacDelegate |
| |
| @synthesize unhandledWheelEventReceived = unhandledWheelEventReceived_; |
| |
| - (void)rendererHandledWheelEvent:(const blink::WebMouseWheelEvent&)event |
| consumed:(BOOL)consumed { |
| if (!consumed) |
| unhandledWheelEventReceived_ = true; |
| } |
| |
| - (void)rendererHandledGestureScrollEvent:(const blink::WebGestureEvent&)event |
| consumed:(BOOL)consumed { |
| if (!consumed && |
| event.GetType() == blink::WebInputEvent::kGestureScrollUpdate) |
| unhandledWheelEventReceived_ = true; |
| } |
| |
| - (void)touchesBeganWithEvent:(NSEvent*)event {} |
| - (void)touchesMovedWithEvent:(NSEvent*)event {} |
| - (void)touchesCancelledWithEvent:(NSEvent*)event {} |
| - (void)touchesEndedWithEvent:(NSEvent*)event {} |
| - (void)beginGestureWithEvent:(NSEvent*)event {} |
| - (void)endGestureWithEvent:(NSEvent*)event {} |
| - (void)rendererHandledOverscrollEvent:(const ui::DidOverscrollParams&)params { |
| } |
| |
| @end |
| |
| namespace content { |
| |
| namespace { |
| |
| blink::WebPointerProperties::PointerType GetInputMessagePointerTypes( |
| MockRenderProcessHost* process) { |
| blink::WebPointerProperties::PointerType pointer_type; |
| DCHECK_LE(process->sink().message_count(), 1U); |
| for (size_t i = 0; i < process->sink().message_count(); ++i) { |
| const IPC::Message* message = process->sink().GetMessageAt(i); |
| EXPECT_EQ(InputMsg_HandleInputEvent::ID, message->type()); |
| InputMsg_HandleInputEvent::Param params; |
| EXPECT_TRUE(InputMsg_HandleInputEvent::Read(message, ¶ms)); |
| const blink::WebInputEvent* event = std::get<0>(params); |
| if (blink::WebInputEvent::IsMouseEventType(event->GetType())) { |
| pointer_type = |
| static_cast<const blink::WebMouseEvent*>(event)->pointer_type; |
| } |
| } |
| process->sink().ClearMessages(); |
| return pointer_type; |
| } |
| |
| NSEvent* MockTabletEventWithParams(CGEventType type, |
| bool is_entering_proximity, |
| NSPointingDeviceType device_type) { |
| CGEventRef cg_event = CGEventCreate(NULL); |
| CGEventSetType(cg_event, type); |
| CGEventSetIntegerValueField(cg_event, kCGTabletProximityEventEnterProximity, |
| is_entering_proximity); |
| CGEventSetIntegerValueField(cg_event, kCGTabletProximityEventPointerType, |
| device_type); |
| NSEvent* event = [NSEvent eventWithCGEvent:cg_event]; |
| CFRelease(cg_event); |
| return event; |
| } |
| |
| NSEvent* MockMouseEventWithParams(CGEventType mouse_type, |
| CGPoint location, |
| CGMouseButton button, |
| CGEventMouseSubtype subtype) { |
| CGEventRef cg_event = |
| CGEventCreateMouseEvent(NULL, mouse_type, location, button); |
| CGEventSetIntegerValueField(cg_event, kCGMouseEventSubtype, subtype); |
| NSEvent* event = [NSEvent eventWithCGEvent:cg_event]; |
| CFRelease(cg_event); |
| return event; |
| } |
| |
| NSEventPhase PhaseForEventType(NSEventType type) { |
| if (type == NSEventTypeBeginGesture) |
| return NSEventPhaseBegan; |
| if (type == NSEventTypeEndGesture) |
| return NSEventPhaseEnded; |
| return NSEventPhaseChanged; |
| } |
| |
| id MockGestureEvent(NSEventType type, double magnification) { |
| id event = [OCMockObject mockForClass:[NSEvent class]]; |
| NSEventPhase phase = PhaseForEventType(type); |
| NSPoint locationInWindow = NSMakePoint(0, 0); |
| CGFloat deltaX = 0; |
| CGFloat deltaY = 0; |
| NSTimeInterval timestamp = 1; |
| NSUInteger modifierFlags = 0; |
| |
| [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(type)] type]; |
| [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(phase)] phase]; |
| [(NSEvent*)[[event stub] |
| andReturnValue:OCMOCK_VALUE(locationInWindow)] locationInWindow]; |
| [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(deltaX)] deltaX]; |
| [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(deltaY)] deltaY]; |
| [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(timestamp)] timestamp]; |
| [(NSEvent*)[[event stub] |
| andReturnValue:OCMOCK_VALUE(modifierFlags)] modifierFlags]; |
| [(NSEvent*)[[event stub] |
| andReturnValue:OCMOCK_VALUE(magnification)] magnification]; |
| return event; |
| } |
| |
| class MockRenderWidgetHostDelegate : public RenderWidgetHostDelegate { |
| public: |
| MockRenderWidgetHostDelegate() |
| : text_input_manager_(new TextInputManager()) {} |
| ~MockRenderWidgetHostDelegate() override {} |
| |
| TextInputManager* GetTextInputManager() override { |
| return text_input_manager_.get(); |
| } |
| |
| private: |
| void ExecuteEditCommand( |
| const std::string& command, |
| const base::Optional<base::string16>& value) override {} |
| void Cut() override {} |
| void Copy() override {} |
| void Paste() override {} |
| void SelectAll() override {} |
| |
| std::unique_ptr<TextInputManager> text_input_manager_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MockRenderWidgetHostDelegate); |
| }; |
| |
| class MockRenderWidgetHostImpl : public RenderWidgetHostImpl { |
| public: |
| ~MockRenderWidgetHostImpl() override {} |
| |
| // Extracts |latency_info| and stores it in |lastWheelEventLatencyInfo|. |
| void ForwardWheelEventWithLatencyInfo ( |
| const blink::WebMouseWheelEvent& wheel_event, |
| const ui::LatencyInfo& ui_latency) override { |
| RenderWidgetHostImpl::ForwardWheelEventWithLatencyInfo ( |
| wheel_event, ui_latency); |
| lastWheelEventLatencyInfo = ui::LatencyInfo(ui_latency); |
| } |
| |
| MOCK_METHOD0(Focus, void()); |
| MOCK_METHOD0(Blur, void()); |
| |
| ui::LatencyInfo lastWheelEventLatencyInfo; |
| static MockRenderWidgetHostImpl* Create(RenderWidgetHostDelegate* delegate, |
| RenderProcessHost* process, |
| int32_t routing_id) { |
| mojom::WidgetPtr widget; |
| std::unique_ptr<MockWidgetImpl> widget_impl = |
| base::MakeUnique<MockWidgetImpl>(mojo::MakeRequest(&widget)); |
| |
| return new MockRenderWidgetHostImpl(delegate, process, routing_id, |
| std::move(widget_impl), |
| std::move(widget)); |
| } |
| |
| private: |
| MockRenderWidgetHostImpl(RenderWidgetHostDelegate* delegate, |
| RenderProcessHost* process, |
| int32_t routing_id, |
| std::unique_ptr<MockWidgetImpl> widget_impl, |
| mojom::WidgetPtr widget) |
| : RenderWidgetHostImpl(delegate, |
| process, |
| routing_id, |
| std::move(widget), |
| false), |
| widget_impl_(std::move(widget_impl)) { |
| set_renderer_initialized(true); |
| lastWheelEventLatencyInfo = ui::LatencyInfo(); |
| } |
| |
| std::unique_ptr<MockWidgetImpl> widget_impl_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MockRenderWidgetHostImpl); |
| }; |
| |
| // Generates the |length| of composition rectangle vector and save them to |
| // |output|. It starts from |origin| and each rectangle contains |unit_size|. |
| void GenerateCompositionRectArray(const gfx::Point& origin, |
| const gfx::Size& unit_size, |
| size_t length, |
| const std::vector<size_t>& break_points, |
| std::vector<gfx::Rect>* output) { |
| DCHECK(output); |
| output->clear(); |
| |
| base::queue<int> break_point_queue; |
| for (size_t i = 0; i < break_points.size(); ++i) |
| break_point_queue.push(break_points[i]); |
| break_point_queue.push(length); |
| size_t next_break_point = break_point_queue.front(); |
| break_point_queue.pop(); |
| |
| gfx::Rect current_rect(origin, unit_size); |
| for (size_t i = 0; i < length; ++i) { |
| if (i == next_break_point) { |
| current_rect.set_x(origin.x()); |
| current_rect.set_y(current_rect.y() + current_rect.height()); |
| next_break_point = break_point_queue.front(); |
| break_point_queue.pop(); |
| } |
| output->push_back(current_rect); |
| current_rect.set_x(current_rect.right()); |
| } |
| } |
| |
| gfx::Rect GetExpectedRect(const gfx::Point& origin, |
| const gfx::Size& size, |
| const gfx::Range& range, |
| int line_no) { |
| return gfx::Rect( |
| origin.x() + range.start() * size.width(), |
| origin.y() + line_no * size.height(), |
| range.length() * size.width(), |
| size.height()); |
| } |
| |
| // Returns NSScrollWheel event that mocks -phase. |mockPhaseSelector| should |
| // correspond to a method in |MockPhaseMethods| that returns the desired phase. |
| NSEvent* MockScrollWheelEventWithPhase(SEL mockPhaseSelector, int32_t delta) { |
| CGEventRef cg_event = CGEventCreateScrollWheelEvent( |
| nullptr, kCGScrollEventUnitLine, 1, delta, 0); |
| CGEventTimestamp timestamp = 0; |
| CGEventSetTimestamp(cg_event, timestamp); |
| NSEvent* event = [NSEvent eventWithCGEvent:cg_event]; |
| CFRelease(cg_event); |
| method_setImplementation( |
| class_getInstanceMethod([NSEvent class], @selector(phase)), |
| [MockPhaseMethods instanceMethodForSelector:mockPhaseSelector]); |
| return event; |
| } |
| |
| NSEvent* MockScrollWheelEventWithMomentumPhase(SEL mockPhaseSelector, |
| int32_t delta) { |
| // Create a dummy event with phaseNone. This is for resetting the phase info |
| // of CGEventRef. |
| MockScrollWheelEventWithPhase(@selector(phaseNone), 0); |
| CGEventRef cg_event = CGEventCreateScrollWheelEvent( |
| nullptr, kCGScrollEventUnitLine, 1, delta, 0); |
| CGEventTimestamp timestamp = 0; |
| CGEventSetTimestamp(cg_event, timestamp); |
| NSEvent* event = [NSEvent eventWithCGEvent:cg_event]; |
| CFRelease(cg_event); |
| method_setImplementation( |
| class_getInstanceMethod([NSEvent class], @selector(momentumPhase)), |
| [MockPhaseMethods instanceMethodForSelector:mockPhaseSelector]); |
| return event; |
| } |
| |
| } // namespace |
| |
| class RenderWidgetHostViewMacTest : public RenderViewHostImplTestHarness { |
| public: |
| RenderWidgetHostViewMacTest(bool scroll_latching = false) |
| : rwhv_mac_(nullptr), |
| scroll_latching_(scroll_latching), |
| old_rwhv_(nullptr) { |
| std::unique_ptr<base::SimpleTestTickClock> mock_clock( |
| new base::SimpleTestTickClock()); |
| mock_clock->Advance(base::TimeDelta::FromMilliseconds(100)); |
| ui::SetEventTickClockForTesting(std::move(mock_clock)); |
| if (scroll_latching) |
| EnableWheelScrollLatching(); |
| else |
| DisableWheelScrollLatching(); |
| |
| vsync_feature_list_.InitAndEnableFeature( |
| features::kVsyncAlignedInputEvents); |
| } |
| |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| gpu::ImageTransportSurface::SetAllowOSMesaForTesting(true); |
| |
| // TestRenderViewHost's destruction assumes that its view is a |
| // TestRenderWidgetHostView, so store its view and reset it back to the |
| // stored view in |TearDown()|. |
| old_rwhv_ = rvh()->GetWidget()->GetView(); |
| |
| // Owned by its |cocoa_view()|, i.e. |rwhv_cocoa_|. |
| rwhv_mac_ = new RenderWidgetHostViewMac(rvh()->GetWidget(), false); |
| RenderWidgetHostImpl::From(rvh()->GetWidget())->SetView(rwhv_mac_); |
| |
| rwhv_cocoa_.reset([rwhv_mac_->cocoa_view() retain]); |
| } |
| |
| void TearDown() override { |
| // Make sure the rwhv_mac_ is gone once the superclass's |TearDown()| runs. |
| rwhv_cocoa_.reset(); |
| RecycleAndWait(); |
| |
| // See comment in SetUp(). |
| test_rvh()->GetWidget()->SetView( |
| static_cast<RenderWidgetHostViewBase*>(old_rwhv_)); |
| |
| RenderViewHostImplTestHarness::TearDown(); |
| } |
| |
| void RecycleAndWait() { |
| pool_.Recycle(); |
| base::RunLoop().RunUntilIdle(); |
| pool_.Recycle(); |
| } |
| |
| void DestroyHostViewRetainCocoaView() { |
| test_rvh()->GetWidget()->SetView(nullptr); |
| rwhv_mac_->Destroy(); |
| } |
| |
| void ActivateViewWithTextInputManager(RenderWidgetHostViewBase* view, |
| ui::TextInputType type) { |
| TextInputState state; |
| state.type = type; |
| view->TextInputStateChanged(state); |
| } |
| |
| protected: |
| std::string selected_text() const { |
| return base::UTF16ToUTF8(rwhv_mac_->GetTextSelection()->selected_text()); |
| } |
| |
| void EnableWheelScrollLatching() { |
| feature_list_.InitFromCommandLine( |
| features::kTouchpadAndWheelScrollLatching.name, ""); |
| } |
| |
| void DisableWheelScrollLatching() { |
| feature_list_.InitFromCommandLine( |
| "", features::kTouchpadAndWheelScrollLatching.name); |
| } |
| |
| void IgnoreEmptyUnhandledWheelEventWithWheelGestures(); |
| void ScrollWheelEndEventDelivery(); |
| |
| RenderWidgetHostViewMac* rwhv_mac_; |
| base::scoped_nsobject<RenderWidgetHostViewCocoa> rwhv_cocoa_; |
| bool scroll_latching_; |
| |
| private: |
| // This class isn't derived from PlatformTest. |
| base::mac::ScopedNSAutoreleasePool pool_; |
| |
| RenderWidgetHostView* old_rwhv_; |
| |
| base::test::ScopedFeatureList vsync_feature_list_; |
| base::test::ScopedFeatureList feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewMacTest); |
| }; |
| |
| TEST_F(RenderWidgetHostViewMacTest, Basic) { |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, AcceptsFirstResponder) { |
| // The RWHVCocoa should normally accept first responder status. |
| EXPECT_TRUE([rwhv_cocoa_.get() acceptsFirstResponder]); |
| } |
| |
| // This test verifies that RenderWidgetHostViewCocoa's implementation of |
| // NSTextInputClientConformance conforms to requirements. |
| TEST_F(RenderWidgetHostViewMacTest, NSTextInputClientConformance) { |
| NSRange selectedRange = [rwhv_cocoa_ selectedRange]; |
| EXPECT_EQ(0u, selectedRange.location); |
| EXPECT_EQ(0u, selectedRange.length); |
| |
| NSRange actualRange = NSMakeRange(1u, 2u); |
| NSAttributedString* actualString = [rwhv_cocoa_ |
| attributedSubstringForProposedRange:NSMakeRange(NSNotFound, 0u) |
| actualRange:&actualRange]; |
| EXPECT_EQ(nil, actualString); |
| EXPECT_EQ(static_cast<NSUInteger>(NSNotFound), actualRange.location); |
| EXPECT_EQ(0u, actualRange.length); |
| |
| actualString = [rwhv_cocoa_ |
| attributedSubstringForProposedRange:NSMakeRange(NSNotFound, 15u) |
| actualRange:&actualRange]; |
| EXPECT_EQ(nil, actualString); |
| EXPECT_EQ(static_cast<NSUInteger>(NSNotFound), actualRange.location); |
| EXPECT_EQ(0u, actualRange.length); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, Fullscreen) { |
| rwhv_mac_->InitAsFullscreen(nullptr); |
| EXPECT_TRUE(rwhv_mac_->pepper_fullscreen_window()); |
| |
| // Break the reference cycle caused by pepper_fullscreen_window() without |
| // an <esc> event. See comment in |
| // release_pepper_fullscreen_window_for_testing(). |
| rwhv_mac_->release_pepper_fullscreen_window_for_testing(); |
| } |
| |
| // Verify that escape key down in fullscreen mode suppressed the keyup event on |
| // the parent. |
| TEST_F(RenderWidgetHostViewMacTest, FullscreenCloseOnEscape) { |
| // Use our own RWH since we need to destroy it. |
| MockRenderWidgetHostDelegate delegate; |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| // Owned by its |cocoa_view()|. |
| MockRenderWidgetHostImpl* rwh = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(rwh, false); |
| |
| view->InitAsFullscreen(rwhv_mac_); |
| |
| WindowedNotificationObserver observer( |
| NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, |
| Source<RenderWidgetHost>(rwh)); |
| EXPECT_FALSE([rwhv_mac_->cocoa_view() suppressNextEscapeKeyUp]); |
| |
| // Escape key down. Should close window and set |suppressNextEscapeKeyUp| on |
| // the parent. |
| [view->cocoa_view() keyEvent: |
| cocoa_test_event_utils::KeyEventWithKeyCode(53, 27, NSKeyDown, 0)]; |
| observer.Wait(); |
| EXPECT_TRUE([rwhv_mac_->cocoa_view() suppressNextEscapeKeyUp]); |
| |
| // Escape key up on the parent should clear |suppressNextEscapeKeyUp|. |
| [rwhv_mac_->cocoa_view() keyEvent: |
| cocoa_test_event_utils::KeyEventWithKeyCode(53, 27, NSKeyUp, 0)]; |
| EXPECT_FALSE([rwhv_mac_->cocoa_view() suppressNextEscapeKeyUp]); |
| } |
| |
| // Test that command accelerators which destroy the fullscreen window |
| // don't crash when forwarded via the window's responder machinery. |
| TEST_F(RenderWidgetHostViewMacTest, AcceleratorDestroy) { |
| // Use our own RWH since we need to destroy it. |
| MockRenderWidgetHostDelegate delegate; |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| // Owned by its |cocoa_view()|. |
| MockRenderWidgetHostImpl* rwh = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(rwh, false); |
| |
| view->InitAsFullscreen(rwhv_mac_); |
| |
| WindowedNotificationObserver observer( |
| NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, |
| Source<RenderWidgetHost>(rwh)); |
| |
| // Key equivalents are only sent to the renderer if the window is key. |
| ui::test::ScopedFakeNSWindowFocus key_window_faker; |
| [[view->cocoa_view() window] makeKeyWindow]; |
| |
| // Command-ESC will destroy the view, while the window is still in |
| // |-performKeyEquivalent:|. There are other cases where this can |
| // happen, Command-ESC is the easiest to trigger. |
| [[view->cocoa_view() window] performKeyEquivalent: |
| cocoa_test_event_utils::KeyEventWithKeyCode( |
| 53, 27, NSKeyDown, NSCommandKeyMask)]; |
| observer.Wait(); |
| } |
| |
| // Test that NSEvent of private use character won't generate keypress event |
| // http://crbug.com/459089 |
| TEST_F(RenderWidgetHostViewMacTest, FilterNonPrintableCharacter) { |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| |
| // Simulate ctrl+F12, will produce a private use character but shouldn't |
| // fire keypress event |
| process_host->sink().ClearMessages(); |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| [view->cocoa_view() keyEvent: |
| cocoa_test_event_utils::KeyEventWithKeyCode( |
| 0x7B, 0xF70F, NSKeyDown, NSControlKeyMask)]; |
| EXPECT_EQ(1U, process_host->sink().message_count()); |
| EXPECT_EQ("RawKeyDown", GetInputMessageTypes(process_host)); |
| |
| // Simulate ctrl+delete, will produce a private use character but shouldn't |
| // fire keypress event |
| process_host->sink().ClearMessages(); |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| [view->cocoa_view() keyEvent: |
| cocoa_test_event_utils::KeyEventWithKeyCode( |
| 0x2E, 0xF728, NSKeyDown, NSControlKeyMask)]; |
| EXPECT_EQ(1U, process_host->sink().message_count()); |
| EXPECT_EQ("RawKeyDown", GetInputMessageTypes(process_host)); |
| |
| // Simulate a printable char, should generate keypress event |
| process_host->sink().ClearMessages(); |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| [view->cocoa_view() keyEvent: |
| cocoa_test_event_utils::KeyEventWithKeyCode( |
| 0x58, 'x', NSKeyDown, NSControlKeyMask)]; |
| EXPECT_EQ(2U, process_host->sink().message_count()); |
| EXPECT_EQ("RawKeyDown Char", GetInputMessageTypes(process_host)); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| // Test that invalid |keyCode| shouldn't generate key events. |
| // https://crbug.com/601964 |
| TEST_F(RenderWidgetHostViewMacTest, InvalidKeyCode) { |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| |
| // Simulate "Convert" key on JIS PC keyboard, will generate a |NSFlagsChanged| |
| // NSEvent with |keyCode| == 0xFF. |
| process_host->sink().ClearMessages(); |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| [view->cocoa_view() keyEvent:cocoa_test_event_utils::KeyEventWithKeyCode( |
| 0xFF, 0, NSFlagsChanged, 0)]; |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, GetFirstRectForCharacterRangeCaretCase) { |
| const base::string16 kDummyString = base::UTF8ToUTF16("hogehoge"); |
| const size_t kDummyOffset = 0; |
| |
| gfx::Rect caret_rect(10, 11, 0, 10); |
| gfx::Range caret_range(0, 0); |
| ViewHostMsg_SelectionBounds_Params params; |
| |
| NSRect rect; |
| NSRange actual_range; |
| rwhv_mac_->SelectionChanged(kDummyString, kDummyOffset, caret_range); |
| params.anchor_rect = params.focus_rect = caret_rect; |
| params.anchor_dir = params.focus_dir = blink::kWebTextDirectionLeftToRight; |
| rwhv_mac_->SelectionBoundsChanged(params); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| caret_range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(caret_rect, gfx::Rect(NSRectToCGRect(rect))); |
| EXPECT_EQ(caret_range, gfx::Range(actual_range)); |
| |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 3).ToNSRange(), |
| &rect, |
| &actual_range)); |
| |
| // Caret moved. |
| caret_rect = gfx::Rect(20, 11, 0, 10); |
| caret_range = gfx::Range(1, 1); |
| params.anchor_rect = params.focus_rect = caret_rect; |
| rwhv_mac_->SelectionChanged(kDummyString, kDummyOffset, caret_range); |
| rwhv_mac_->SelectionBoundsChanged(params); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| caret_range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(caret_rect, gfx::Rect(NSRectToCGRect(rect))); |
| EXPECT_EQ(caret_range, gfx::Range(actual_range)); |
| |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 3).ToNSRange(), |
| &rect, |
| &actual_range)); |
| |
| // No caret. |
| caret_range = gfx::Range(1, 2); |
| rwhv_mac_->SelectionChanged(kDummyString, kDummyOffset, caret_range); |
| params.anchor_rect = caret_rect; |
| params.focus_rect = gfx::Rect(30, 11, 0, 10); |
| rwhv_mac_->SelectionBoundsChanged(params); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 2).ToNSRange(), |
| &rect, |
| &actual_range)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, UpdateCompositionSinglelineCase) { |
| ActivateViewWithTextInputManager(rwhv_mac_, ui::TEXT_INPUT_TYPE_TEXT); |
| const gfx::Point kOrigin(10, 11); |
| const gfx::Size kBoundsUnit(10, 20); |
| |
| NSRect rect; |
| // Make sure not crashing by passing nullptr pointer instead of |
| // |actual_range|. |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0).ToNSRange(), &rect, nullptr)); |
| |
| // If there are no update from renderer, always returned caret position. |
| NSRange actual_range; |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 0).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2).ToNSRange(), |
| &rect, |
| &actual_range)); |
| |
| // If the firstRectForCharacterRange is failed in renderer, empty rect vector |
| // is sent. Make sure this does not crash. |
| rwhv_mac_->ImeCompositionRangeChanged(gfx::Range(10, 12), |
| std::vector<gfx::Rect>()); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(10, 11).ToNSRange(), &rect, nullptr)); |
| |
| const int kCompositionLength = 10; |
| std::vector<gfx::Rect> composition_bounds; |
| const int kCompositionStart = 3; |
| const gfx::Range kCompositionRange(kCompositionStart, |
| kCompositionStart + kCompositionLength); |
| GenerateCompositionRectArray(kOrigin, |
| kBoundsUnit, |
| kCompositionLength, |
| std::vector<size_t>(), |
| &composition_bounds); |
| rwhv_mac_->ImeCompositionRangeChanged(kCompositionRange, composition_bounds); |
| |
| // Out of range requests will return caret position. |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 2).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(13, 14).ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(14, 15).ToNSRange(), |
| &rect, |
| &actual_range)); |
| |
| for (int i = 0; i <= kCompositionLength; ++i) { |
| for (int j = 0; j <= kCompositionLength - i; ++j) { |
| const gfx::Range range(i, i + j); |
| const gfx::Rect expected_rect = GetExpectedRect(kOrigin, |
| kBoundsUnit, |
| range, |
| 0); |
| const NSRange request_range = gfx::Range( |
| kCompositionStart + range.start(), |
| kCompositionStart + range.end()).ToNSRange(); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| request_range, |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(request_range), gfx::Range(actual_range)); |
| EXPECT_EQ(expected_rect, gfx::Rect(NSRectToCGRect(rect))); |
| |
| // Make sure not crashing by passing nullptr pointer instead of |
| // |actual_range|. |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| request_range, &rect, nullptr)); |
| } |
| } |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, UpdateCompositionMultilineCase) { |
| ActivateViewWithTextInputManager(rwhv_mac_, ui::TEXT_INPUT_TYPE_TEXT); |
| const gfx::Point kOrigin(10, 11); |
| const gfx::Size kBoundsUnit(10, 20); |
| NSRect rect; |
| |
| const int kCompositionLength = 30; |
| std::vector<gfx::Rect> composition_bounds; |
| const gfx::Range kCompositionRange(0, kCompositionLength); |
| // Set breaking point at 10 and 20. |
| std::vector<size_t> break_points; |
| break_points.push_back(10); |
| break_points.push_back(20); |
| GenerateCompositionRectArray(kOrigin, |
| kBoundsUnit, |
| kCompositionLength, |
| break_points, |
| &composition_bounds); |
| rwhv_mac_->ImeCompositionRangeChanged(kCompositionRange, composition_bounds); |
| |
| // Range doesn't contain line breaking point. |
| gfx::Range range; |
| range = gfx::Range(5, 8); |
| NSRange actual_range; |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(range, gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, range, 0), |
| gfx::Rect(NSRectToCGRect(rect))); |
| range = gfx::Range(15, 18); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(range, gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 8), 1), |
| gfx::Rect(NSRectToCGRect(rect))); |
| range = gfx::Range(25, 28); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(range, gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 8), 2), |
| gfx::Rect(NSRectToCGRect(rect))); |
| |
| // Range contains line breaking point. |
| range = gfx::Range(8, 12); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(8, 10), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(8, 10), 0), |
| gfx::Rect(NSRectToCGRect(rect))); |
| range = gfx::Range(18, 22); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(18, 20), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(8, 10), 1), |
| gfx::Rect(NSRectToCGRect(rect))); |
| |
| // Start point is line breaking point. |
| range = gfx::Range(10, 12); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(10, 12), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 2), 1), |
| gfx::Rect(NSRectToCGRect(rect))); |
| range = gfx::Range(20, 22); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(20, 22), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 2), 2), |
| gfx::Rect(NSRectToCGRect(rect))); |
| |
| // End point is line breaking point. |
| range = gfx::Range(5, 10); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(5, 10), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 10), 0), |
| gfx::Rect(NSRectToCGRect(rect))); |
| range = gfx::Range(15, 20); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(15, 20), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 10), 1), |
| gfx::Rect(NSRectToCGRect(rect))); |
| |
| // Start and end point are same line breaking point. |
| range = gfx::Range(10, 10); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(10, 10), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 0), 1), |
| gfx::Rect(NSRectToCGRect(rect))); |
| range = gfx::Range(20, 20); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(20, 20), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 0), 2), |
| gfx::Rect(NSRectToCGRect(rect))); |
| |
| // Start and end point are different line breaking point. |
| range = gfx::Range(10, 20); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range.ToNSRange(), |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(10, 20), gfx::Range(actual_range)); |
| EXPECT_EQ( |
| GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 10), 1), |
| gfx::Rect(NSRectToCGRect(rect))); |
| } |
| |
| // Check that events coming from AppKit via -[NSTextInputClient |
| // firstRectForCharacterRange:actualRange] are handled in a sane manner if they |
| // arrive after the C++ RenderWidgetHostView is destroyed. |
| TEST_F(RenderWidgetHostViewMacTest, CompositionEventAfterDestroy) { |
| ActivateViewWithTextInputManager(rwhv_mac_, ui::TEXT_INPUT_TYPE_TEXT); |
| const gfx::Rect composition_bounds(0, 0, 30, 40); |
| const gfx::Range range(0, 1); |
| rwhv_mac_->ImeCompositionRangeChanged( |
| range, std::vector<gfx::Rect>(1, composition_bounds)); |
| |
| NSRange actual_range = NSMakeRange(0, 0); |
| |
| base::scoped_nsobject<CocoaTestHelperWindow> window( |
| [[CocoaTestHelperWindow alloc] init]); |
| [[window contentView] addSubview:rwhv_cocoa_]; |
| [rwhv_cocoa_ setFrame:NSMakeRect(0, 0, 400, 400)]; |
| |
| NSRect rect = [rwhv_cocoa_ firstRectForCharacterRange:range.ToNSRange() |
| actualRange:&actual_range]; |
| EXPECT_EQ(30, rect.size.width); |
| EXPECT_EQ(40, rect.size.height); |
| EXPECT_EQ(range, gfx::Range(actual_range)); |
| |
| DestroyHostViewRetainCocoaView(); |
| actual_range = NSMakeRange(0, 0); |
| rect = [rwhv_cocoa_ firstRectForCharacterRange:range.ToNSRange() |
| actualRange:&actual_range]; |
| EXPECT_NSEQ(NSZeroRect, rect); |
| EXPECT_EQ(gfx::Range(), gfx::Range(actual_range)); |
| } |
| |
| // Verify that |SetActive()| calls |RenderWidgetHostImpl::Blur()| and |
| // |RenderWidgetHostImp::Focus()|. |
| TEST_F(RenderWidgetHostViewMacTest, BlurAndFocusOnSetActive) { |
| MockRenderWidgetHostDelegate delegate; |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| |
| // Owned by its |cocoa_view()|. |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* rwh = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(rwh, false); |
| |
| base::scoped_nsobject<CocoaTestHelperWindow> window( |
| [[CocoaTestHelperWindow alloc] init]); |
| [[window contentView] addSubview:view->cocoa_view()]; |
| |
| EXPECT_CALL(*rwh, Focus()); |
| [window makeFirstResponder:view->cocoa_view()]; |
| testing::Mock::VerifyAndClearExpectations(rwh); |
| |
| EXPECT_CALL(*rwh, Blur()); |
| view->SetActive(false); |
| testing::Mock::VerifyAndClearExpectations(rwh); |
| |
| EXPECT_CALL(*rwh, Focus()); |
| view->SetActive(true); |
| testing::Mock::VerifyAndClearExpectations(rwh); |
| |
| // Unsetting first responder should blur. |
| EXPECT_CALL(*rwh, Blur()); |
| [window makeFirstResponder:nil]; |
| testing::Mock::VerifyAndClearExpectations(rwh); |
| |
| // |SetActive()| shoud not focus if view is not first responder. |
| EXPECT_CALL(*rwh, Focus()).Times(0); |
| view->SetActive(true); |
| testing::Mock::VerifyAndClearExpectations(rwh); |
| |
| // Clean up. |
| rwh->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, LastWheelEventLatencyInfoExists) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| // Verifies that ui::INPUT_EVENT_LATENCY_UI_COMPONENT is added |
| // properly in scrollWheel function. |
| NSEvent* wheelEvent1 = MockScrollWheelEventWithPhase(@selector(phaseBegan),3); |
| [view->cocoa_view() scrollWheel:wheelEvent1]; |
| ui::LatencyInfo::LatencyComponent ui_component1; |
| ASSERT_TRUE(host->lastWheelEventLatencyInfo.FindLatency( |
| ui::INPUT_EVENT_LATENCY_UI_COMPONENT, 0, &ui_component1) ); |
| |
| // Send a wheel event with phaseEnded. |
| // Verifies that ui::INPUT_EVENT_LATENCY_UI_COMPONENT is added |
| // properly in shortCircuitScrollWheelEvent function which is called |
| // in scrollWheel. |
| NSEvent* wheelEvent2 = MockScrollWheelEventWithPhase(@selector(phaseEnded),0); |
| [view->cocoa_view() scrollWheel:wheelEvent2]; |
| ui::LatencyInfo::LatencyComponent ui_component2; |
| ASSERT_TRUE(host->lastWheelEventLatencyInfo.FindLatency( |
| ui::INPUT_EVENT_LATENCY_UI_COMPONENT, 0, &ui_component2) ); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, SourceEventTypeExistsInLatencyInfo) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send a wheel event for scrolling by 3 lines. |
| // Verifies that SourceEventType exists in forwarded LatencyInfo object. |
| NSEvent* wheelEvent = MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->cocoa_view() scrollWheel:wheelEvent]; |
| ASSERT_TRUE(host->lastWheelEventLatencyInfo.source_event_type() == |
| ui::SourceEventType::WHEEL); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| void RenderWidgetHostViewMacTest::ScrollWheelEndEventDelivery() { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send an initial wheel event with NSEventPhaseBegan to the view. |
| NSEvent* event1 = MockScrollWheelEventWithPhase(@selector(phaseBegan), 0); |
| [view->cocoa_view() scrollWheel:event1]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| process_host->sink().ClearMessages(); |
| |
| // Send an ACK for the first wheel event, so that the queue will be flushed. |
| InputEventAck ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kMouseWheel, |
| INPUT_EVENT_ACK_STATE_CONSUMED); |
| std::unique_ptr<IPC::Message> response( |
| new InputHostMsg_HandleInputEvent_ACK(0, ack)); |
| host->OnMessageReceived(*response); |
| |
| // Post the NSEventPhaseEnded wheel event to NSApp and check whether the |
| // render view receives it. |
| NSEvent* event2 = MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [NSApp postEvent:event2 atStart:NO]; |
| base::RunLoop().RunUntilIdle(); |
| if (scroll_latching_) { |
| // The wheel event with phaseEnded won't be sent to the render view |
| // immediately, instead the mouse_wheel_phase_handler will wait for 100ms |
| // to see if a wheel event with momentumPhase began arrives or not. |
| ASSERT_EQ(0U, process_host->sink().message_count()); |
| } else { |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| } |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| TEST_F(RenderWidgetHostViewMacTest, ScrollWheelEndEventDelivery) { |
| ScrollWheelEndEventDelivery(); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithEraserType) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send a NSEvent of NSTabletProximity type which has a device type of eraser. |
| NSEvent* event = MockTabletEventWithParams(kCGEventTabletProximity, true, |
| NSEraserPointingDevice); |
| [view->cocoa_view() tabletEvent:event]; |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| process_host->sink().ClearMessages(); |
| |
| event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint); |
| [view->cocoa_view() mouseEvent:event]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kEraser, |
| GetInputMessagePointerTypes(process_host)); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithPenType) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send a NSEvent of NSTabletProximity type which has a device type of pen. |
| NSEvent* event = MockTabletEventWithParams(kCGEventTabletProximity, true, |
| NSPenPointingDevice); |
| [view->cocoa_view() tabletEvent:event]; |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| process_host->sink().ClearMessages(); |
| |
| event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint); |
| [view->cocoa_view() mouseEvent:event]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| GetInputMessagePointerTypes(process_host)); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithMouseType) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send a NSEvent of a mouse type. |
| NSEvent* event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeDefault); |
| [view->cocoa_view() mouseEvent:event]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kMouse, |
| GetInputMessagePointerTypes(process_host)); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| void RenderWidgetHostViewMacTest:: |
| IgnoreEmptyUnhandledWheelEventWithWheelGestures() { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Add a delegate to the view. |
| base::scoped_nsobject<MockRenderWidgetHostViewMacDelegate> view_delegate( |
| [[MockRenderWidgetHostViewMacDelegate alloc] init]); |
| view->SetDelegate(view_delegate.get()); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* event1 = MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->cocoa_view() scrollWheel:event1]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| process_host->sink().ClearMessages(); |
| |
| // Indicate that the wheel event was unhandled. |
| InputEventAck unhandled_ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kMouseWheel, |
| INPUT_EVENT_ACK_STATE_NOT_CONSUMED); |
| std::unique_ptr<IPC::Message> response1( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_ack)); |
| host->OnMessageReceived(*response1); |
| |
| if (scroll_latching_) { |
| // GestureEventQueue allows multiple in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", |
| GetInputMessageTypes(process_host)); |
| // Send GSB ack. |
| InputEventAck unhandled_scroll_ack( |
| InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kGestureScrollBegin, |
| INPUT_EVENT_ACK_STATE_CONSUMED); |
| std::unique_ptr<IPC::Message> scroll_response1( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_scroll_ack)); |
| host->OnMessageReceived(*scroll_response1); |
| } else { |
| // GestureEventQueue allows multiple in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate GestureScrollEnd", |
| GetInputMessageTypes(process_host)); |
| } |
| process_host->sink().ClearMessages(); |
| |
| InputEventAck unhandled_scroll_ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kGestureScrollUpdate, |
| INPUT_EVENT_ACK_STATE_NOT_CONSUMED); |
| std::unique_ptr<IPC::Message> scroll_response1( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_scroll_ack)); |
| host->OnMessageReceived(*scroll_response1); |
| |
| // Check that the view delegate got an unhandled wheel event. |
| ASSERT_EQ(YES, view_delegate.get().unhandledWheelEventReceived); |
| view_delegate.get().unhandledWheelEventReceived = NO; |
| |
| // Send another wheel event, this time for scrolling by 0 lines (empty event). |
| NSEvent* event2 = MockScrollWheelEventWithPhase(@selector(phaseChanged), 0); |
| [view->cocoa_view() scrollWheel:event2]; |
| ASSERT_EQ("MouseWheel", GetInputMessageTypes(process_host)); |
| |
| // Indicate that the wheel event was also unhandled. |
| std::unique_ptr<IPC::Message> response2( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_ack)); |
| host->OnMessageReceived(*response2); |
| |
| // Check that the view delegate ignored the empty unhandled wheel event. |
| ASSERT_EQ(NO, view_delegate.get().unhandledWheelEventReceived); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| TEST_F(RenderWidgetHostViewMacTest, |
| IgnoreEmptyUnhandledWheelEventWithWheelGestures) { |
| IgnoreEmptyUnhandledWheelEventWithWheelGestures(); |
| } |
| |
| // Tests that when view initiated shutdown happens (i.e. RWHView is deleted |
| // before RWH), we clean up properly and don't leak the RWHVGuest. |
| TEST_F(RenderWidgetHostViewMacTest, GuestViewDoesNotLeak) { |
| MockRenderWidgetHostDelegate delegate; |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| |
| // Owned by its |cocoa_view()|. |
| MockRenderWidgetHostImpl* rwh = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(rwh, true); |
| |
| // Add a delegate to the view. |
| base::scoped_nsobject<MockRenderWidgetHostViewMacDelegate> view_delegate( |
| [[MockRenderWidgetHostViewMacDelegate alloc] init]); |
| view->SetDelegate(view_delegate.get()); |
| |
| base::WeakPtr<RenderWidgetHostViewBase> guest_rwhv_weak = |
| (RenderWidgetHostViewGuest::Create(rwh, nullptr, view->GetWeakPtr())) |
| ->GetWeakPtr(); |
| |
| // Remove the cocoa_view() so |view| also goes away before |rwh|. |
| { |
| base::scoped_nsobject<RenderWidgetHostViewCocoa> rwhv_cocoa; |
| rwhv_cocoa.reset([view->cocoa_view() retain]); |
| } |
| RecycleAndWait(); |
| |
| // Clean up. |
| rwh->ShutdownAndDestroyWidget(true); |
| |
| // Let |guest_rwhv_weak| have a chance to delete itself. |
| base::RunLoop run_loop; |
| content::BrowserThread::PostTask( |
| content::BrowserThread::UI, FROM_HERE, run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| ASSERT_FALSE(guest_rwhv_weak.get()); |
| } |
| |
| // Tests setting background transparency. See also (disabled on Mac) |
| // RenderWidgetHostTest.Background. This test has some additional checks for |
| // Mac. |
| TEST_F(RenderWidgetHostViewMacTest, Background) { |
| const IPC::Message* set_background = nullptr; |
| std::tuple<bool> sent_background; |
| |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| |
| // If no color has been specified then default color of white should be |
| // returned. |
| EXPECT_EQ(static_cast<unsigned>(SK_ColorWHITE), view->background_color()); |
| |
| // Set the color to red. The background is initially assumed to be opaque, so |
| // no opacity message change should be sent. |
| view->SetBackgroundColor(SK_ColorRED); |
| EXPECT_EQ(static_cast<unsigned>(SK_ColorRED), view->background_color()); |
| set_background = process_host->sink().GetUniqueMessageMatching( |
| ViewMsg_SetBackgroundOpaque::ID); |
| ASSERT_FALSE(set_background); |
| |
| // Set the color to blue. This should not send an opacity message. |
| view->SetBackgroundColor(SK_ColorBLUE); |
| EXPECT_EQ(static_cast<unsigned>(SK_ColorBLUE), view->background_color()); |
| set_background = process_host->sink().GetUniqueMessageMatching( |
| ViewMsg_SetBackgroundOpaque::ID); |
| ASSERT_FALSE(set_background); |
| |
| // Set the color back to transparent. The background color should now be |
| // reported as the default (white), and a transparency change message should |
| // be sent. |
| process_host->sink().ClearMessages(); |
| view->SetBackgroundColor(SK_ColorTRANSPARENT); |
| EXPECT_EQ(static_cast<unsigned>(SK_ColorWHITE), view->background_color()); |
| set_background = process_host->sink().GetUniqueMessageMatching( |
| ViewMsg_SetBackgroundOpaque::ID); |
| ASSERT_TRUE(set_background); |
| ViewMsg_SetBackgroundOpaque::Read(set_background, &sent_background); |
| EXPECT_FALSE(std::get<0>(sent_background)); |
| |
| // Set the color to red. This should send an opacity message. |
| process_host->sink().ClearMessages(); |
| view->SetBackgroundColor(SK_ColorBLUE); |
| EXPECT_EQ(static_cast<unsigned>(SK_ColorBLUE), view->background_color()); |
| set_background = process_host->sink().GetUniqueMessageMatching( |
| ViewMsg_SetBackgroundOpaque::ID); |
| ASSERT_TRUE(set_background); |
| ViewMsg_SetBackgroundOpaque::Read(set_background, &sent_background); |
| EXPECT_TRUE(std::get<0>(sent_background)); |
| |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| class RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest |
| : public RenderWidgetHostViewMacTest { |
| public: |
| RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest() |
| : RenderWidgetHostViewMacTest(true) {} |
| }; |
| |
| TEST_F(RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest, |
| IgnoreEmptyUnhandledWheelEventWithWheelGestures) { |
| IgnoreEmptyUnhandledWheelEventWithWheelGestures(); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest, |
| ScrollWheelEndEventDelivery) { |
| ScrollWheelEndEventDelivery(); |
| } |
| |
| // When wheel scroll latching is enabled, wheel end events are not sent |
| // immediately, instead we start a timer to see if momentum phase of the scroll |
| // starts or not. |
| TEST_F(RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest, |
| WheelWithPhaseEndedIsNotForwardedImmediately) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* wheelEvent1 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->cocoa_view() scrollWheel:wheelEvent1]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| process_host->sink().ClearMessages(); |
| |
| // Indicate that the wheel event was unhandled. |
| InputEventAck unhandled_ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kMouseWheel, |
| INPUT_EVENT_ACK_STATE_NOT_CONSUMED); |
| std::unique_ptr<IPC::Message> response1( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_ack)); |
| host->OnMessageReceived(*response1); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", |
| GetInputMessageTypes(process_host)); |
| |
| // Send a wheel event with phaseEnded. When wheel scroll latching is enabled |
| // the event will be dropped and the mouse_wheel_end_dispatch_timer_ will |
| // start. |
| NSEvent* wheelEvent2 = |
| MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [view->cocoa_view() scrollWheel:wheelEvent2]; |
| ASSERT_EQ(0U, process_host->sink().message_count()); |
| DCHECK(view->HasPendingWheelEndEventForTesting()); |
| process_host->sink().ClearMessages(); |
| |
| host->ShutdownAndDestroyWidget(true); |
| |
| // Wait for the mouse_wheel_end_dispatch_timer_ to expire after host is |
| // destroyed. The pending wheel end event won't get dispatched since the |
| // render_widget_host_ is null. This waiting confirms that no crash happens |
| // because of an attempt to send the pending wheel end event. |
| // https://crbug.com/770057 |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), |
| base::TimeDelta::FromMilliseconds(100)); |
| run_loop.Run(); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest, |
| WheelWithMomentumPhaseBeganStopsTheWheelEndDispatchTimer) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* wheelEvent1 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->cocoa_view() scrollWheel:wheelEvent1]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| process_host->sink().ClearMessages(); |
| |
| // Indicate that the wheel event was unhandled. |
| InputEventAck unhandled_ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kMouseWheel, |
| INPUT_EVENT_ACK_STATE_NOT_CONSUMED); |
| std::unique_ptr<IPC::Message> response1( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_ack)); |
| host->OnMessageReceived(*response1); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", |
| GetInputMessageTypes(process_host)); |
| |
| // Send a wheel event with phaseEnded. When wheel scroll latching is enabled |
| // the event will be dropped and the mouse_wheel_end_dispatch_timer_ will |
| // start. |
| NSEvent* wheelEvent2 = |
| MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [view->cocoa_view() scrollWheel:wheelEvent2]; |
| ASSERT_EQ(0U, process_host->sink().message_count()); |
| DCHECK(view->HasPendingWheelEndEventForTesting()); |
| process_host->sink().ClearMessages(); |
| |
| // Send a wheel event with momentum phase started, this should stop the wheel |
| // end dispatch timer. |
| NSEvent* wheelEvent3 = |
| MockScrollWheelEventWithMomentumPhase(@selector(phaseBegan), 3); |
| ASSERT_TRUE(wheelEvent3); |
| [view->cocoa_view() scrollWheel:wheelEvent3]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| DCHECK(!view->HasPendingWheelEndEventForTesting()); |
| process_host->sink().ClearMessages(); |
| |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacWithWheelScrollLatchingEnabledTest, |
| WheelWithPhaseBeganDispatchesThePendingWheelEnd) { |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* wheelEvent1 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->cocoa_view() scrollWheel:wheelEvent1]; |
| ASSERT_EQ(1U, process_host->sink().message_count()); |
| process_host->sink().ClearMessages(); |
| |
| // Indicate that the wheel event was unhandled. |
| InputEventAck unhandled_ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kMouseWheel, |
| INPUT_EVENT_ACK_STATE_NOT_CONSUMED); |
| std::unique_ptr<IPC::Message> response1( |
| new InputHostMsg_HandleInputEvent_ACK(0, unhandled_ack)); |
| host->OnMessageReceived(*response1); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", |
| GetInputMessageTypes(process_host)); |
| |
| // Send a wheel event with phaseEnded. When wheel scroll latching is enabled |
| // the event will be dropped and the mouse_wheel_end_dispatch_timer_ will |
| // start. |
| NSEvent* wheelEvent2 = |
| MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [view->cocoa_view() scrollWheel:wheelEvent2]; |
| ASSERT_EQ(0U, process_host->sink().message_count()); |
| DCHECK(view->HasPendingWheelEndEventForTesting()); |
| process_host->sink().ClearMessages(); |
| |
| // Send a wheel event with phase started, this should stop the wheel end |
| // dispatch timer and dispatch the pending wheel end event for the previous |
| // scroll sequence. |
| NSEvent* wheelEvent3 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| ASSERT_TRUE(wheelEvent3); |
| [view->cocoa_view() scrollWheel:wheelEvent3]; |
| ASSERT_EQ("MouseWheel GestureScrollEnd MouseWheel", |
| GetInputMessageTypes(process_host)); |
| DCHECK(!view->HasPendingWheelEndEventForTesting()); |
| process_host->sink().ClearMessages(); |
| |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| class RenderWidgetHostViewMacPinchTest : public RenderWidgetHostViewMacTest { |
| public: |
| RenderWidgetHostViewMacPinchTest() = default; |
| |
| void SetUp() override { |
| RenderWidgetHostViewMacTest::SetUp(); |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather |
| // than the MockRenderProcessHost that is set up by the test harness which |
| // mocks out |OnMessageReceived()|. |
| browser_context_.reset(new TestBrowserContext); |
| process_host_.reset(new MockRenderProcessHost(browser_context_.get())); |
| process_host_->Init(); |
| delegate_.reset(new MockRenderWidgetHostDelegate); |
| int32_t routing_id = process_host_->GetNextRoutingID(); |
| host_.reset(MockRenderWidgetHostImpl::Create( |
| delegate_.get(), process_host_.get(), routing_id)); |
| view_ = new RenderWidgetHostViewMac(host_.get(), false); |
| cocoa_view_.reset([view_->cocoa_view() retain]); |
| process_host_->sink().ClearMessages(); |
| } |
| |
| void TearDown() override { |
| cocoa_view_.reset(); |
| host_->ShutdownAndDestroyWidget(false); |
| host_.reset(); |
| delegate_.reset(); |
| process_host_.reset(); |
| browser_context_.reset(); |
| RenderWidgetHostViewMacTest::TearDown(); |
| } |
| |
| bool ZoomDisabledForPinchUpdateMessage() { |
| const IPC::Message* message = nullptr; |
| // The first message may be a PinchBegin. Go for the second message if |
| // there are two. |
| switch (process_host_->sink().message_count()) { |
| case 1: |
| message = process_host_->sink().GetMessageAt(0); |
| break; |
| case 2: |
| message = process_host_->sink().GetMessageAt(1); |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| DCHECK(message); |
| std::tuple<IPC::WebInputEventPointer, |
| std::vector<IPC::WebInputEventPointer>, ui::LatencyInfo, |
| InputEventDispatchType> |
| data; |
| InputMsg_HandleInputEvent::Read(message, &data); |
| IPC::WebInputEventPointer ipc_event = std::get<0>(data); |
| const blink::WebGestureEvent* gesture_event = |
| static_cast<const blink::WebGestureEvent*>(ipc_event); |
| return gesture_event->data.pinch_update.zoom_disabled; |
| } |
| |
| bool ShouldSendGestureEvents() { |
| #if defined(MAC_OS_X_VERSION_10_11) && \ |
| MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11 |
| return base::mac::IsAtMostOS10_10(); |
| #endif |
| return true; |
| } |
| |
| void SendBeginEvent() { |
| NSEvent* pinchBeginEvent = MockGestureEvent(NSEventTypeBeginGesture, 0); |
| if (ShouldSendGestureEvents()) |
| [cocoa_view_ beginGestureWithEvent:pinchBeginEvent]; |
| [cocoa_view_ magnifyWithEvent:pinchBeginEvent]; |
| } |
| |
| void SendEndEvent() { |
| NSEvent* pinchEndEvent = MockGestureEvent(NSEventTypeEndGesture, 0); |
| [cocoa_view_ magnifyWithEvent:pinchEndEvent]; |
| if (ShouldSendGestureEvents()) |
| [cocoa_view_ endGestureWithEvent:pinchEndEvent]; |
| } |
| |
| std::unique_ptr<TestBrowserContext> browser_context_; |
| std::unique_ptr<MockRenderProcessHost> process_host_; |
| std::unique_ptr<MockRenderWidgetHostImpl> host_; |
| std::unique_ptr<MockRenderWidgetHostDelegate> delegate_; |
| |
| // Owned by view_->cocoa_view(), which is stored in cocoa_view_. |
| RenderWidgetHostViewMac* view_; |
| base::scoped_nsobject<RenderWidgetHostViewCocoa> cocoa_view_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewMacPinchTest); |
| }; |
| |
| TEST_F(RenderWidgetHostViewMacPinchTest, PinchThresholding) { |
| // We'll use this IPC message to ack events. |
| InputEventAck ack(InputEventAckSource::COMPOSITOR_THREAD, |
| blink::WebInputEvent::kGesturePinchUpdate, |
| INPUT_EVENT_ACK_STATE_CONSUMED); |
| std::unique_ptr<IPC::Message> response( |
| new InputHostMsg_HandleInputEvent_ACK(0, ack)); |
| |
| // Do a gesture that crosses the threshold. |
| { |
| NSEvent* pinchUpdateEvents[3] = { |
| MockGestureEvent(NSEventTypeMagnify, 0.25), |
| MockGestureEvent(NSEventTypeMagnify, 0.25), |
| MockGestureEvent(NSEventTypeMagnify, 0.25), |
| }; |
| |
| SendBeginEvent(); |
| EXPECT_EQ(0U, process_host_->sink().message_count()); |
| |
| // No zoom is sent for the first update event. |
| [cocoa_view_ magnifyWithEvent:pinchUpdateEvents[0]]; |
| host_->OnMessageReceived(*response); |
| EXPECT_EQ(2U, process_host_->sink().message_count()); |
| EXPECT_TRUE(ZoomDisabledForPinchUpdateMessage()); |
| process_host_->sink().ClearMessages(); |
| |
| // The second update event crosses the threshold of 0.4, and so zoom is no |
| // longer disabled. |
| [cocoa_view_ magnifyWithEvent:pinchUpdateEvents[1]]; |
| EXPECT_FALSE(ZoomDisabledForPinchUpdateMessage()); |
| host_->OnMessageReceived(*response); |
| EXPECT_EQ(1U, process_host_->sink().message_count()); |
| process_host_->sink().ClearMessages(); |
| |
| // The third update still has zoom enabled. |
| [cocoa_view_ magnifyWithEvent:pinchUpdateEvents[2]]; |
| EXPECT_FALSE(ZoomDisabledForPinchUpdateMessage()); |
| host_->OnMessageReceived(*response); |
| EXPECT_EQ(1U, process_host_->sink().message_count()); |
| process_host_->sink().ClearMessages(); |
| |
| SendEndEvent(); |
| EXPECT_EQ(1U, process_host_->sink().message_count()); |
| process_host_->sink().ClearMessages(); |
| } |
| |
| // Do a gesture that doesn't cross the threshold, but happens when we're not |
| // at page scale factor one, so it should be sent to the renderer. |
| { |
| NSEvent* pinchUpdateEvent = MockGestureEvent(NSEventTypeMagnify, 0.25); |
| |
| view_->page_at_minimum_scale_ = false; |
| |
| SendBeginEvent(); |
| EXPECT_EQ(0U, process_host_->sink().message_count()); |
| |
| // Expect that a zoom happen because the time threshold has not passed. |
| [cocoa_view_ magnifyWithEvent:pinchUpdateEvent]; |
| EXPECT_FALSE(ZoomDisabledForPinchUpdateMessage()); |
| host_->OnMessageReceived(*response); |
| EXPECT_EQ(2U, process_host_->sink().message_count()); |
| process_host_->sink().ClearMessages(); |
| |
| SendEndEvent(); |
| EXPECT_EQ(1U, process_host_->sink().message_count()); |
| process_host_->sink().ClearMessages(); |
| } |
| |
| // Do a gesture again, after the page scale is no longer at one, and ensure |
| // that it is thresholded again. |
| { |
| NSEvent* pinchUpdateEvent = MockGestureEvent(NSEventTypeMagnify, 0.25); |
| |
| view_->page_at_minimum_scale_ = true; |
| |
| SendBeginEvent(); |
| EXPECT_EQ(0U, process_host_->sink().message_count()); |
| |
| // Get back to zoom one right after the begin event. This should still keep |
| // the thresholding in place (it is latched at the begin event). |
| view_->page_at_minimum_scale_ = false; |
| |
| // Expect that zoom be disabled because the time threshold has passed. |
| [cocoa_view_ magnifyWithEvent:pinchUpdateEvent]; |
| EXPECT_EQ(2U, process_host_->sink().message_count()); |
| EXPECT_TRUE(ZoomDisabledForPinchUpdateMessage()); |
| host_->OnMessageReceived(*response); |
| process_host_->sink().ClearMessages(); |
| |
| SendEndEvent(); |
| EXPECT_EQ(1U, process_host_->sink().message_count()); |
| process_host_->sink().ClearMessages(); |
| } |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, EventLatencyOSMouseWheelHistogram) { |
| base::HistogramTester histogram_tester; |
| |
| // Initialize the view associated with a MockRenderWidgetHostImpl, rather than |
| // the MockRenderProcessHost that is set up by the test harness which mocks |
| // out |OnMessageReceived()|. |
| TestBrowserContext browser_context; |
| MockRenderProcessHost* process_host = |
| new MockRenderProcessHost(&browser_context); |
| process_host->Init(); |
| MockRenderWidgetHostDelegate delegate; |
| int32_t routing_id = process_host->GetNextRoutingID(); |
| MockRenderWidgetHostImpl* host = |
| MockRenderWidgetHostImpl::Create(&delegate, process_host, routing_id); |
| RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(host, false); |
| process_host->sink().ClearMessages(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| // Verify that Event.Latency.OS.MOUSE_WHEEL histogram is computed properly. |
| NSEvent* wheelEvent = MockScrollWheelEventWithPhase(@selector(phaseBegan),3); |
| [view->cocoa_view() scrollWheel:wheelEvent]; |
| histogram_tester.ExpectTotalCount("Event.Latency.OS.MOUSE_WHEEL", 1); |
| |
| // Clean up. |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| // This test verifies that |selected_text_| is updated accordingly with |
| // different variations of RWHVMac::SelectChanged updates. |
| TEST_F(RenderWidgetHostViewMacTest, SelectedText) { |
| base::string16 sample_text; |
| base::UTF8ToUTF16("hello world!", 12, &sample_text); |
| gfx::Range range(6, 11); |
| |
| // Send a valid selection for the word 'World'. |
| rwhv_mac_->SelectionChanged(sample_text, 0U, range); |
| EXPECT_EQ("world", selected_text()); |
| |
| // Make the range cover some of the text and extend more. |
| range.set_end(100); |
| rwhv_mac_->SelectionChanged(sample_text, 0U, range); |
| EXPECT_EQ("world!", selected_text()); |
| |
| // Finally, send an empty range. This should clear the selected text. |
| range.set_start(100); |
| rwhv_mac_->SelectionChanged(sample_text, 0U, range); |
| EXPECT_EQ("", selected_text()); |
| } |
| |
| // This class is used for IME-related unit tests which verify correctness of IME |
| // for pages with multiple RWHVs. |
| class InputMethodMacTest : public RenderWidgetHostViewMacTest { |
| public: |
| InputMethodMacTest() {} |
| ~InputMethodMacTest() override {} |
| |
| void SetUp() override { |
| RenderWidgetHostViewMacTest::SetUp(); |
| |
| // Initializing a child frame's view. |
| child_process_host_ = new MockRenderProcessHost(&browser_context_); |
| RenderWidgetHostDelegate* rwh_delegate = |
| RenderWidgetHostImpl::From(rvh()->GetWidget())->delegate(); |
| child_widget_ = MockRenderWidgetHostImpl::Create( |
| rwh_delegate, child_process_host_, |
| child_process_host_->GetNextRoutingID()); |
| child_view_ = new TestRenderWidgetHostView(child_widget_); |
| text_input_manager_ = rwh_delegate->GetTextInputManager(); |
| tab_widget_ = RenderWidgetHostImpl::From(rvh()->GetWidget()); |
| } |
| |
| void TearDown() override { |
| child_widget_->ShutdownAndDestroyWidget(true); |
| |
| RenderWidgetHostViewMacTest::TearDown(); |
| } |
| |
| void SetTextInputType(RenderWidgetHostViewBase* view, |
| ui::TextInputType type) { |
| TextInputState state; |
| state.type = type; |
| view->TextInputStateChanged(state); |
| } |
| |
| IPC::TestSink& tab_sink() { return process()->sink(); } |
| IPC::TestSink& child_sink() { return child_process_host_->sink(); } |
| TextInputManager* text_input_manager() { return text_input_manager_; } |
| RenderWidgetHostViewBase* tab_view() { return rwhv_mac_; } |
| RenderWidgetHostImpl* tab_widget() { return tab_widget_; } |
| |
| protected: |
| MockRenderProcessHost* child_process_host_; |
| RenderWidgetHostImpl* child_widget_; |
| TestRenderWidgetHostView* child_view_; |
| |
| private: |
| TestBrowserContext browser_context_; |
| TextInputManager* text_input_manager_; |
| RenderWidgetHostImpl* tab_widget_; |
| |
| DISALLOW_COPY_AND_ASSIGN(InputMethodMacTest); |
| }; |
| |
| // This test will verify that calling unmarkText on the cocoa view will lead to |
| // a finish composing text IPC for the corresponding active widget. |
| TEST_F(InputMethodMacTest, UnmarkText) { |
| // Make the child view active and then call unmarkText on the view (Note that |
| // |RenderWidgetHostViewCocoa::handlingKeyDown_| is false so calling |
| // unmarkText would lead to an IPC. This assumption is made in other similar |
| // tests as well). We should observe an IPC being sent to the |child_widget_|. |
| SetTextInputType(child_view_, ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(child_widget_, text_input_manager()->GetActiveWidget()); |
| child_sink().ClearMessages(); |
| [rwhv_cocoa_ unmarkText]; |
| EXPECT_TRUE(!!child_sink().GetFirstMessageMatching( |
| InputMsg_ImeFinishComposingText::ID)); |
| |
| // Repeat the same steps for the tab's view . |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(tab_widget(), text_input_manager()->GetActiveWidget()); |
| tab_sink().ClearMessages(); |
| [rwhv_cocoa_ unmarkText]; |
| EXPECT_TRUE(!!tab_sink().GetFirstMessageMatching( |
| InputMsg_ImeFinishComposingText::ID)); |
| } |
| |
| // This test makes sure that calling setMarkedText on the cocoa view will lead |
| // to a set composition IPC for the corresponding active widget. |
| TEST_F(InputMethodMacTest, SetMarkedText) { |
| // Some values for the call to setMarkedText. |
| base::scoped_nsobject<NSString> text( |
| [[NSString alloc] initWithString:@"sample text"]); |
| NSRange selectedRange = NSMakeRange(0, 4); |
| NSRange replacementRange = NSMakeRange(0, 1); |
| |
| // Make the child view active and then call setMarkedText with some values. We |
| // should observe an IPC being sent to the |child_widget_|. |
| SetTextInputType(child_view_, ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(child_widget_, text_input_manager()->GetActiveWidget()); |
| child_sink().ClearMessages(); |
| [rwhv_cocoa_ setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE( |
| !!child_sink().GetFirstMessageMatching(InputMsg_ImeSetComposition::ID)); |
| |
| // Repeat the same steps for the tab's view. |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(tab_widget(), text_input_manager()->GetActiveWidget()); |
| tab_sink().ClearMessages(); |
| [rwhv_cocoa_ setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE( |
| !!tab_sink().GetFirstMessageMatching(InputMsg_ImeSetComposition::ID)); |
| } |
| |
| // This test verifies that calling insertText on the cocoa view will lead to a |
| // commit text IPC sent to the active widget. |
| TEST_F(InputMethodMacTest, InsertText) { |
| // Some values for the call to insertText. |
| base::scoped_nsobject<NSString> text( |
| [[NSString alloc] initWithString:@"sample text"]); |
| NSRange replacementRange = NSMakeRange(0, 1); |
| |
| // Make the child view active and then call insertText with some values. We |
| // should observe an IPC being sent to the |child_widget_|. |
| SetTextInputType(child_view_, ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(child_widget_, text_input_manager()->GetActiveWidget()); |
| child_sink().ClearMessages(); |
| [rwhv_cocoa_ insertText:text replacementRange:replacementRange]; |
| EXPECT_TRUE( |
| !!child_sink().GetFirstMessageMatching(InputMsg_ImeCommitText::ID)); |
| |
| // Repeat the same steps for the tab's view. |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(tab_widget(), text_input_manager()->GetActiveWidget()); |
| [rwhv_cocoa_ insertText:text replacementRange:replacementRange]; |
| EXPECT_TRUE(!!tab_sink().GetFirstMessageMatching(InputMsg_ImeCommitText::ID)); |
| } |
| |
| // This test makes sure that calling finishComposingText on the cocoa view will |
| // lead to a finish composing text IPC for a the corresponding active widget. |
| TEST_F(InputMethodMacTest, FinishComposingText) { |
| // Some values for the call to setMarkedText. |
| base::scoped_nsobject<NSString> text( |
| [[NSString alloc] initWithString:@"sample text"]); |
| NSRange selectedRange = NSMakeRange(0, 4); |
| NSRange replacementRange = NSMakeRange(0, 1); |
| |
| // Make child view active and then call finishComposingText. We should observe |
| // an IPC being sent to the |child_widget_|. |
| SetTextInputType(child_view_, ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(child_widget_, text_input_manager()->GetActiveWidget()); |
| child_sink().ClearMessages(); |
| // In order to finish composing text, we must first have some marked text. So, |
| // we will first call setMarkedText on cocoa view. This would lead to a set |
| // composition IPC in the sink, but it doesn't matter since we will be looking |
| // for a finish composing text IPC for this test. |
| [rwhv_cocoa_ setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| [rwhv_cocoa_ finishComposingText]; |
| EXPECT_TRUE(!!child_sink().GetFirstMessageMatching( |
| InputMsg_ImeFinishComposingText::ID)); |
| |
| // Repeat the same steps for the tab's view. |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(tab_widget(), text_input_manager()->GetActiveWidget()); |
| tab_sink().ClearMessages(); |
| [rwhv_cocoa_ setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| [rwhv_cocoa_ finishComposingText]; |
| EXPECT_TRUE(!!tab_sink().GetFirstMessageMatching( |
| InputMsg_ImeFinishComposingText::ID)); |
| } |
| |
| // This test creates a test view to mimic a child frame's view and verifies that |
| // calling ImeCancelComposition on either the child view or the tab's view will |
| // always lead to a call to cancelComposition on the cocoa view. |
| TEST_F(InputMethodMacTest, ImeCancelCompositionForAllViews) { |
| // Some values for the call to setMarkedText. |
| base::scoped_nsobject<NSString> text( |
| [[NSString alloc] initWithString:@"sample text"]); |
| NSRange selectedRange = NSMakeRange(0, 1); |
| NSRange replacementRange = NSMakeRange(0, 1); |
| |
| // Make Cocoa view assume there is marked text. |
| [rwhv_cocoa_ setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE([rwhv_cocoa_ hasMarkedText]); |
| child_view_->ImeCancelComposition(); |
| EXPECT_FALSE([rwhv_cocoa_ hasMarkedText]); |
| |
| // Repeat for the tab's view. |
| [rwhv_cocoa_ setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE([rwhv_cocoa_ hasMarkedText]); |
| rwhv_mac_->ImeCancelComposition(); |
| EXPECT_FALSE([rwhv_cocoa_ hasMarkedText]); |
| } |
| |
| // This test verifies that when a RenderWidgetHostView changes its |
| // TextInputState to NONE we send the IPC to stop monitor composition info and, |
| // conversely, when its state is set to non-NONE, we start monitoring the |
| // composition info. |
| TEST_F(InputMethodMacTest, MonitorCompositionRangeForActiveWidget) { |
| // First, we need to make the cocoa view the first responder so that the |
| // method RWHVMac::HasFocus() returns true. Then we can make sure that as long |
| // as there is some TextInputState of non-NONE, the corresponding widget will |
| // be asked to start monitoring composition info. |
| base::scoped_nsobject<CocoaTestHelperWindow> window( |
| [[CocoaTestHelperWindow alloc] init]); |
| [[window contentView] addSubview:rwhv_cocoa_]; |
| [window makeFirstResponder:rwhv_cocoa_]; |
| EXPECT_TRUE(rwhv_mac_->HasFocus()); |
| |
| TextInputState state; |
| state.type = ui::TEXT_INPUT_TYPE_TEXT; |
| tab_sink().ClearMessages(); |
| |
| // Make the tab's widget active. |
| rwhv_mac_->TextInputStateChanged(state); |
| |
| // The tab's widget must have received an IPC regarding composition updates. |
| const IPC::Message* composition_request_msg_for_tab = |
| tab_sink().GetUniqueMessageMatching( |
| InputMsg_RequestCompositionUpdates::ID); |
| EXPECT_TRUE(composition_request_msg_for_tab); |
| |
| // The message should ask for monitoring updates, but no immediate update. |
| InputMsg_RequestCompositionUpdates::Param tab_msg_params; |
| InputMsg_RequestCompositionUpdates::Read(composition_request_msg_for_tab, |
| &tab_msg_params); |
| bool is_tab_msg_for_immediate_request = std::get<0>(tab_msg_params); |
| bool is_tab_msg_for_monitor_request = std::get<1>(tab_msg_params); |
| EXPECT_FALSE(is_tab_msg_for_immediate_request); |
| EXPECT_TRUE(is_tab_msg_for_monitor_request); |
| tab_sink().ClearMessages(); |
| child_sink().ClearMessages(); |
| |
| // Now make the child view active. |
| child_view_->TextInputStateChanged(state); |
| |
| // The tab should receive another IPC for composition updates. |
| composition_request_msg_for_tab = tab_sink().GetUniqueMessageMatching( |
| InputMsg_RequestCompositionUpdates::ID); |
| EXPECT_TRUE(composition_request_msg_for_tab); |
| |
| // This time, the tab should have been asked to stop monitoring (and no |
| // immediate updates). |
| InputMsg_RequestCompositionUpdates::Read(composition_request_msg_for_tab, |
| &tab_msg_params); |
| is_tab_msg_for_immediate_request = std::get<0>(tab_msg_params); |
| is_tab_msg_for_monitor_request = std::get<1>(tab_msg_params); |
| EXPECT_FALSE(is_tab_msg_for_immediate_request); |
| EXPECT_FALSE(is_tab_msg_for_monitor_request); |
| tab_sink().ClearMessages(); |
| |
| // The child too must have received an IPC for composition updates. |
| const IPC::Message* composition_request_msg_for_child = |
| child_sink().GetUniqueMessageMatching( |
| InputMsg_RequestCompositionUpdates::ID); |
| EXPECT_TRUE(composition_request_msg_for_child); |
| |
| // Verify that the message is asking for monitoring to start; but no immediate |
| // updates. |
| InputMsg_RequestCompositionUpdates::Param child_msg_params; |
| InputMsg_RequestCompositionUpdates::Read(composition_request_msg_for_child, |
| &child_msg_params); |
| bool is_child_msg_for_immediate_request = std::get<0>(child_msg_params); |
| bool is_child_msg_for_monitor_request = std::get<1>(child_msg_params); |
| EXPECT_FALSE(is_child_msg_for_immediate_request); |
| EXPECT_TRUE(is_child_msg_for_monitor_request); |
| child_sink().ClearMessages(); |
| |
| // Make the tab view active again. |
| rwhv_mac_->TextInputStateChanged(state); |
| |
| // Verify that the child received another IPC for composition updates. |
| composition_request_msg_for_child = child_sink().GetUniqueMessageMatching( |
| InputMsg_RequestCompositionUpdates::ID); |
| EXPECT_TRUE(composition_request_msg_for_child); |
| |
| // Verify that this IPC is asking for no monitoring or immediate updates. |
| InputMsg_RequestCompositionUpdates::Read(composition_request_msg_for_child, |
| &child_msg_params); |
| is_child_msg_for_immediate_request = std::get<0>(child_msg_params); |
| is_child_msg_for_monitor_request = std::get<1>(child_msg_params); |
| EXPECT_FALSE(is_child_msg_for_immediate_request); |
| EXPECT_FALSE(is_child_msg_for_monitor_request); |
| } |
| |
| // Ensure RenderWidgetHostViewMac claims hotkeys when AppKit spams the UI with |
| // -performKeyEquivalent:, but only when the window is key. |
| TEST_F(RenderWidgetHostViewMacTest, ForwardKeyEquivalentsOnlyIfKey) { |
| // This test needs an NSWindow. |rwhv_cocoa_| isn't in one, but going |
| // fullscreen conveniently puts it in one. |
| EXPECT_FALSE([rwhv_cocoa_ window]); |
| rwhv_mac_->InitAsFullscreen(nullptr); |
| NSWindow* window = [rwhv_cocoa_ window]; |
| EXPECT_TRUE(window); |
| |
| MockRenderProcessHost* process_host = test_rvh()->GetProcess(); |
| process_host->sink().ClearMessages(); |
| |
| ui::test::ScopedFakeNSWindowFocus key_window_faker; |
| EXPECT_FALSE([window isKeyWindow]); |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| |
| // Cmd+x. |
| NSEvent* key_down = |
| cocoa_test_event_utils::KeyEventWithType(NSKeyDown, NSCommandKeyMask); |
| |
| // Sending while not key should forward along the responder chain (e.g. to the |
| // mainMenu). Note the event is being sent to the NSWindow, which may also ask |
| // other parts of the UI to handle it, but in the test they should all say |
| // "NO" as well. |
| EXPECT_FALSE([window performKeyEquivalent:key_down]); |
| EXPECT_EQ(0U, process_host->sink().message_count()); |
| |
| // Make key and send again. Event should be seen. |
| [window makeKeyWindow]; |
| EXPECT_TRUE([window isKeyWindow]); |
| process_host->sink().ClearMessages(); // Ignore the focus messages. |
| |
| // -performKeyEquivalent: now returns YES to prevent further propagation, and |
| // the event is sent to the renderer. |
| EXPECT_TRUE([window performKeyEquivalent:key_down]); |
| EXPECT_EQ(2U, process_host->sink().message_count()); |
| EXPECT_EQ("RawKeyDown Char", GetInputMessageTypes(process_host)); |
| |
| rwhv_mac_->release_pepper_fullscreen_window_for_testing(); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, ClearCompositorFrame) { |
| BrowserCompositorMac* browser_compositor = |
| rwhv_mac_->BrowserCompositorForTesting(); |
| EXPECT_NE(browser_compositor->CompositorForTesting(), nullptr); |
| EXPECT_TRUE(browser_compositor->CompositorForTesting()->IsLocked()); |
| rwhv_mac_->ClearCompositorFrame(); |
| EXPECT_NE(browser_compositor->CompositorForTesting(), nullptr); |
| EXPECT_FALSE(browser_compositor->CompositorForTesting()->IsLocked()); |
| } |
| |
| } // namespace content |