| // 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/task/post_task.h" |
| #include "base/test/metrics/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 "base/time/time.h" |
| #include "components/viz/common/surfaces/child_local_surface_id_allocator.h" |
| #import "content/app_shim_remote_cocoa/render_widget_host_view_cocoa.h" |
| #include "content/browser/compositor/image_transport_factory.h" |
| #include "content/browser/gpu/compositor_util.h" |
| #include "content/browser/renderer_host/frame_token_message_queue.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/common/widget_messages.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.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_render_widget_host_delegate.h" |
| #include "content/test/mock_widget_impl.h" |
| #include "content/test/stub_render_widget_host_owner_delegate.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 "mojo/public/cpp/bindings/pending_remote.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" |
| #include "ui/base/cocoa/secure_password_input.h" |
| #import "ui/base/test/cocoa_helper.h" |
| #import "ui/base/test/scoped_fake_nswindow_focus.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/compositor/recyclable_compositor_mac.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" |
| #include "ui/gfx/mac/coordinate_conversion.h" |
| #include "ui/latency/latency_info.h" |
| |
| using testing::_; |
| |
| // 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 |
| |
| @interface FakeTextCheckingResult : NSObject<NSCopying> |
| @property(readonly) NSRange range; |
| @property(readonly) NSString* replacementString; |
| @end |
| |
| @implementation FakeTextCheckingResult { |
| base::scoped_nsobject<NSString> replacementString_; |
| } |
| @synthesize range = range_; |
| |
| + (FakeTextCheckingResult*)resultWithRange:(NSRange)range |
| replacementString:(NSString*)replacementString { |
| FakeTextCheckingResult* result = |
| [[[FakeTextCheckingResult alloc] init] autorelease]; |
| result->range_ = range; |
| result->replacementString_.reset([replacementString retain]); |
| return result; |
| } |
| |
| - (id)copyWithZone:(NSZone*)zone { |
| return |
| [[FakeTextCheckingResult resultWithRange:self.range |
| replacementString:self.replacementString] retain]; |
| } |
| |
| - (NSString*)replacementString { |
| return replacementString_; |
| } |
| @end |
| |
| @interface FakeSpellChecker : NSObject |
| @property NSInteger sequenceNumber; |
| @end |
| |
| @implementation FakeSpellChecker { |
| base::mac::ScopedBlock<void (^)(NSInteger sequenceNumber, |
| NSArray<NSTextCheckingResult*>* candidates)> |
| lastCompletionHandler_; |
| } |
| @synthesize sequenceNumber = sequenceNumber_; |
| |
| - (NSInteger) |
| requestCandidatesForSelectedRange:(NSRange)selectedRange |
| inString:(NSString*)stringToCheck |
| types:(NSTextCheckingTypes)checkingTypes |
| options: |
| (nullable NSDictionary<NSTextCheckingOptionKey, |
| id>*)options |
| inSpellDocumentWithTag:(NSInteger)tag |
| completionHandler: |
| (void (^__nullable)(NSInteger sequenceNumber, |
| NSArray<NSTextCheckingResult*>* |
| candidates))completionHandler |
| NS_AVAILABLE_MAC(10_12_2) { |
| sequenceNumber_ += 1; |
| lastCompletionHandler_.reset([completionHandler copy]); |
| return sequenceNumber_; |
| } |
| |
| - (base::mac::ScopedBlock<void (^)(NSInteger sequenceNumber, |
| NSArray<NSTextCheckingResult*>* candidates)>) |
| takeCompletionHandler { |
| return std::move(lastCompletionHandler_); |
| } |
| |
| @end |
| |
| namespace content { |
| |
| namespace { |
| |
| std::string GetMessageNames( |
| const MockWidgetInputHandler::MessageVector& events) { |
| std::vector<std::string> result; |
| for (auto& event : events) |
| result.push_back(event->name()); |
| return base::JoinString(result, " "); |
| } |
| |
| blink::WebPointerProperties::PointerType GetPointerType( |
| const MockWidgetInputHandler::MessageVector& events) { |
| EXPECT_EQ(events.size(), 1U); |
| MockWidgetInputHandler::DispatchedEventMessage* event = events[0]->ToEvent(); |
| if (!event) |
| return blink::WebPointerProperties::PointerType::kUnknown; |
| |
| if (blink::WebInputEvent::IsMouseEventType( |
| event->Event()->web_event->GetType())) { |
| return static_cast<const blink::WebMouseEvent*>( |
| event->Event()->web_event.get()) |
| ->pointer_type; |
| } |
| |
| if (blink::WebInputEvent::IsTouchEventType( |
| event->Event()->web_event->GetType())) { |
| return static_cast<const blink::WebTouchEvent*>( |
| event->Event()->web_event.get()) |
| ->touches[0] |
| .pointer_type; |
| } |
| return blink::WebPointerProperties::PointerType::kUnknown; |
| } |
| |
| 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, |
| bool is_entering_proximity = false, |
| bool is_pen_tip = false) { |
| // CGEvents have their origin at the *top* left screen corner. Converting to |
| // an NSEvent, below, flips the location back to bottom left origin. |
| CGPoint cg_location = |
| CGPointMake(location.x, NSHeight(NSScreen.screens[0].frame) - location.y); |
| CGEventRef cg_event = |
| CGEventCreateMouseEvent(NULL, mouse_type, cg_location, button); |
| CGEventSetIntegerValueField(cg_event, kCGMouseEventSubtype, subtype); |
| CGEventSetIntegerValueField(cg_event, kCGTabletProximityEventEnterProximity, |
| is_entering_proximity); |
| CGEventSetIntegerValueField(cg_event, kCGTabletEventRotation, 300); |
| if (is_pen_tip) |
| CGEventSetIntegerValueField(cg_event, kCGTabletEventPointButtons, 1); |
| CGEventTimestamp timestamp = |
| (ui::EventTimeForNow() - base::TimeTicks()).InMicroseconds() * |
| base::Time::kNanosecondsPerMicrosecond; |
| CGEventSetTimestamp(cg_event, timestamp); |
| NSEvent* event = [NSEvent eventWithCGEvent:cg_event]; |
| CFRelease(cg_event); |
| return event; |
| } |
| |
| id MockPinchEvent(NSEventPhase phase, double magnification) { |
| id event = [OCMockObject mockForClass:[NSEvent class]]; |
| NSEventType type = NSEventTypeMagnify; |
| 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; |
| } |
| |
| id MockSmartMagnifyEvent() { |
| id event = [OCMockObject mockForClass:[NSEvent class]]; |
| NSEventType type = NSEventTypeSmartMagnify; |
| 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(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]; |
| return event; |
| } |
| |
| 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) { |
| mojo::PendingRemote<mojom::Widget> widget; |
| std::unique_ptr<MockWidgetImpl> widget_impl = |
| std::make_unique<MockWidgetImpl>( |
| widget.InitWithNewPipeAndPassReceiver()); |
| |
| return new MockRenderWidgetHostImpl(delegate, process, routing_id, |
| std::move(widget_impl), |
| std::move(widget)); |
| } |
| |
| MockWidgetInputHandler* input_handler() { |
| return widget_impl_->input_handler(); |
| } |
| MockWidgetInputHandler::MessageVector GetAndResetDispatchedMessages() { |
| return input_handler()->GetAndResetDispatchedMessages(); |
| } |
| |
| private: |
| MockRenderWidgetHostImpl(RenderWidgetHostDelegate* delegate, |
| RenderProcessHost* process, |
| int32_t routing_id, |
| std::unique_ptr<MockWidgetImpl> widget_impl, |
| mojo::PendingRemote<mojom::Widget> widget) |
| : RenderWidgetHostImpl(delegate, |
| process, |
| routing_id, |
| std::move(widget), |
| /*hidden=*/false, |
| std::make_unique<FrameTokenMessageQueue>()), |
| widget_impl_(std::move(widget_impl)) { |
| set_renderer_initialized(true); |
| lastWheelEventLatencyInfo = ui::LatencyInfo(); |
| |
| ON_CALL(*this, Focus()) |
| .WillByDefault( |
| testing::Invoke(this, &MockRenderWidgetHostImpl::FocusImpl)); |
| ON_CALL(*this, Blur()) |
| .WillByDefault( |
| testing::Invoke(this, &MockRenderWidgetHostImpl::BlurImpl)); |
| } |
| |
| void FocusImpl() { RenderWidgetHostImpl::Focus(); } |
| void BlurImpl() { RenderWidgetHostImpl::Blur(); } |
| |
| 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 fake 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; |
| } |
| |
| NSEvent* MockScrollWheelEventWithoutPhase(int32_t delta) { |
| return MockScrollWheelEventWithMomentumPhase(@selector(phaseNone), delta); |
| } |
| |
| class MockRenderWidgetHostOwnerDelegate |
| : public StubRenderWidgetHostOwnerDelegate { |
| public: |
| MOCK_METHOD1(SetBackgroundOpaque, void(bool opaque)); |
| }; |
| |
| } // namespace |
| |
| class RenderWidgetHostViewMacTest : public RenderViewHostImplTestHarness { |
| public: |
| RenderWidgetHostViewMacTest() : rwhv_mac_(nullptr) { |
| } |
| |
| void SetUp() override { |
| mock_clock_.Advance(base::TimeDelta::FromMilliseconds(100)); |
| ui::SetEventTickClockForTesting(&mock_clock_); |
| RenderViewHostImplTestHarness::SetUp(); |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(features::kDirectManipulationStylus); |
| |
| browser_context_ = std::make_unique<TestBrowserContext>(); |
| process_host_ = |
| std::make_unique<MockRenderProcessHost>(browser_context_.get()); |
| process_host_->Init(); |
| host_ = base::WrapUnique(MockRenderWidgetHostImpl::Create( |
| &delegate_, process_host_.get(), process_host_->GetNextRoutingID())); |
| host_->set_owner_delegate(&mock_owner_delegate_); |
| rwhv_mac_ = new RenderWidgetHostViewMac(host_.get()); |
| rwhv_cocoa_.reset([rwhv_mac_->GetInProcessNSView() retain]); |
| |
| window_.reset([[CocoaTestHelperWindow alloc] init]); |
| window_.get().pretendIsKeyWindow = YES; |
| [[window_ contentView] addSubview:rwhv_cocoa_]; |
| [rwhv_cocoa_ setFrame:[window_ contentView].bounds]; |
| |
| base::RunLoop().RunUntilIdle(); |
| process_host_->sink().ClearMessages(); |
| } |
| |
| void TearDown() override { |
| ui::SetEventTickClockForTesting(nullptr); |
| rwhv_cocoa_.reset(); |
| // RenderWidgetHostImpls with an owner delegate are not expected to be self- |
| // deleting. |
| host_->ShutdownAndDestroyWidget(/*also_delete=*/false); |
| host_.reset(); |
| process_host_.reset(); |
| browser_context_.reset(); |
| RecycleAndWait(); |
| 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()); |
| } |
| |
| MockRenderWidgetHostDelegate delegate_; |
| |
| std::unique_ptr<TestBrowserContext> browser_context_; |
| std::unique_ptr<MockRenderProcessHost> process_host_; |
| testing::NiceMock<MockRenderWidgetHostOwnerDelegate> mock_owner_delegate_; |
| std::unique_ptr<MockRenderWidgetHostImpl> host_; |
| RenderWidgetHostViewMac* rwhv_mac_ = nullptr; |
| base::scoped_nsobject<RenderWidgetHostViewCocoa> rwhv_cocoa_; |
| base::scoped_nsobject<CocoaTestHelperWindow> window_; |
| |
| private: |
| // This class isn't derived from PlatformTest. |
| base::mac::ScopedNSAutoreleasePool pool_; |
| |
| base::SimpleTestTickClock mock_clock_; |
| |
| 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) { |
| EXPECT_NSEQ(NSMakeRange(0, 0), [rwhv_cocoa_ selectedRange]); |
| |
| rwhv_mac_->SelectionChanged(base::UTF8ToUTF16("llo, world!"), 2, |
| gfx::Range(5, 10)); |
| EXPECT_NSEQ(NSMakeRange(5, 5), [rwhv_cocoa_ selectedRange]); |
| |
| 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 that NSEvent of private use character won't generate keypress event |
| // http://crbug.com/459089 |
| TEST_F(RenderWidgetHostViewMacTest, FilterNonPrintableCharacter) { |
| // Simulate ctrl+F12, will produce a private use character but shouldn't |
| // fire keypress event |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| |
| EXPECT_EQ(0U, events.size()); |
| [rwhv_mac_->GetInProcessNSView() |
| keyEvent:cocoa_test_event_utils::KeyEventWithKeyCode( |
| 0x7B, 0xF70F, NSKeyDown, NSControlKeyMask)]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| |
| EXPECT_EQ("RawKeyDown", GetMessageNames(events)); |
| |
| // 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()); |
| [rwhv_mac_->GetInProcessNSView() |
| keyEvent:cocoa_test_event_utils::KeyEventWithKeyCode( |
| 0x2E, 0xF728, NSKeyDown, NSControlKeyMask)]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("RawKeyDown", GetMessageNames(events)); |
| |
| // Simulate a printable char, should generate keypress event |
| [rwhv_mac_->GetInProcessNSView() |
| keyEvent:cocoa_test_event_utils::KeyEventWithKeyCode(0x58, 'x', NSKeyDown, |
| NSControlKeyMask)]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("RawKeyDown Char", GetMessageNames(events)); |
| } |
| |
| // Test that invalid |keyCode| shouldn't generate key events. |
| // https://crbug.com/601964 |
| TEST_F(RenderWidgetHostViewMacTest, InvalidKeyCode) { |
| // Simulate "Convert" key on JIS PC keyboard, will generate a |NSFlagsChanged| |
| // NSEvent with |keyCode| == 0xFF. |
| [rwhv_mac_->GetInProcessNSView() |
| keyEvent:cocoa_test_event_utils::KeyEventWithKeyCode(0xFF, 0, |
| NSFlagsChanged, 0)]; |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(0U, host_->GetAndResetDispatchedMessages().size()); |
| } |
| |
| 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); |
| WidgetHostMsg_SelectionBounds_Params params; |
| |
| gfx::Rect rect; |
| gfx::Range 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, &rect, |
| &actual_range)); |
| EXPECT_EQ(caret_rect, rect); |
| EXPECT_EQ(caret_range, gfx::Range(actual_range)); |
| |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 1), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 3), &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, &rect, |
| &actual_range)); |
| EXPECT_EQ(caret_rect, rect); |
| EXPECT_EQ(caret_range, actual_range); |
| |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 3), &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), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 1), &rect, &actual_range)); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1), &rect, &actual_range)); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2), &rect, &actual_range)); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 2), &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); |
| |
| gfx::Rect rect; |
| // Make sure not crashing by passing nullptr pointer instead of |
| // |actual_range|. |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange(gfx::Range(0, 0), |
| &rect, nullptr)); |
| |
| // If there are no update from renderer, always returned caret position. |
| gfx::Range actual_range; |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 0), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(0, 1), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 0), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2), &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), &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), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 1), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(1, 2), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(2, 2), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(13, 14), &rect, &actual_range)); |
| EXPECT_FALSE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| gfx::Range(14, 15), &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 gfx::Range request_range = gfx::Range( |
| kCompositionStart + range.start(), kCompositionStart + range.end()); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange( |
| request_range, |
| &rect, |
| &actual_range)); |
| EXPECT_EQ(request_range, actual_range); |
| EXPECT_EQ(expected_rect, 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); |
| gfx::Rect 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); |
| gfx::Range actual_range; |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(range, actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, range, 0), rect); |
| range = gfx::Range(15, 18); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(range, actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 8), 1), rect); |
| range = gfx::Range(25, 28); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(range, actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 8), 2), rect); |
| |
| // Range contains line breaking point. |
| range = gfx::Range(8, 12); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(8, 10), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(8, 10), 0), rect); |
| range = gfx::Range(18, 22); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(18, 20), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(8, 10), 1), rect); |
| |
| // Start point is line breaking point. |
| range = gfx::Range(10, 12); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(10, 12), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 2), 1), rect); |
| range = gfx::Range(20, 22); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(20, 22), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 2), 2), rect); |
| |
| // End point is line breaking point. |
| range = gfx::Range(5, 10); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(5, 10), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 10), 0), rect); |
| range = gfx::Range(15, 20); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(15, 20), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(5, 10), 1), rect); |
| |
| // Start and end point are same line breaking point. |
| range = gfx::Range(10, 10); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(10, 10), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 0), 1), rect); |
| range = gfx::Range(20, 20); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(20, 20), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 0), 2), rect); |
| |
| // Start and end point are different line breaking point. |
| range = gfx::Range(10, 20); |
| EXPECT_TRUE(rwhv_mac_->GetCachedFirstRectForCharacterRange(range, &rect, |
| &actual_range)); |
| EXPECT_EQ(gfx::Range(10, 20), actual_range); |
| EXPECT_EQ(GetExpectedRect(kOrigin, kBoundsUnit, gfx::Range(0, 10), 1), 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); |
| |
| 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) { |
| EXPECT_CALL(*host_, Focus()); |
| [window_ makeFirstResponder:rwhv_mac_->GetInProcessNSView()]; |
| testing::Mock::VerifyAndClearExpectations(host_.get()); |
| |
| EXPECT_CALL(*host_, Blur()); |
| rwhv_mac_->SetActive(false); |
| testing::Mock::VerifyAndClearExpectations(host_.get()); |
| |
| EXPECT_CALL(*host_, Focus()); |
| rwhv_mac_->SetActive(true); |
| testing::Mock::VerifyAndClearExpectations(host_.get()); |
| |
| // Unsetting first responder should blur. |
| EXPECT_CALL(*host_, Blur()); |
| [window_ makeFirstResponder:nil]; |
| testing::Mock::VerifyAndClearExpectations(host_.get()); |
| |
| // |SetActive()| shoud not focus if view is not first responder. |
| EXPECT_CALL(*host_, Focus()).Times(0); |
| rwhv_mac_->SetActive(true); |
| testing::Mock::VerifyAndClearExpectations(host_.get()); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, LastWheelEventLatencyInfoExists) { |
| 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); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:wheelEvent1]; |
| ASSERT_TRUE(host_->lastWheelEventLatencyInfo.FindLatency( |
| ui::INPUT_EVENT_LATENCY_UI_COMPONENT, nullptr)); |
| |
| // 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); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:wheelEvent2]; |
| ASSERT_TRUE(host_->lastWheelEventLatencyInfo.FindLatency( |
| ui::INPUT_EVENT_LATENCY_UI_COMPONENT, nullptr)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, SourceEventTypeExistsInLatencyInfo) { |
| 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); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:wheelEvent]; |
| ASSERT_TRUE(host_->lastWheelEventLatencyInfo.source_event_type() == |
| ui::SourceEventType::WHEEL); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, ScrollWheelEndEventDelivery) { |
| // Send an initial wheel event with NSEventPhaseBegan to the view. |
| NSEvent* event1 = MockScrollWheelEventWithPhase(@selector(phaseBegan), 0); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:event1]; |
| |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| // Send an ACK for the first wheel event, so that the queue will be flushed. |
| events[0]->ToEvent()->CallCallback(INPUT_EVENT_ACK_STATE_CONSUMED); |
| |
| // 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(); |
| events = host_->GetAndResetDispatchedMessages(); |
| // 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, events.size()); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithEraserType) { |
| // Send a NSEvent of NSTabletProximity type which has a device type of eraser. |
| NSEvent* event = MockTabletEventWithParams(kCGEventTabletProximity, true, |
| NSEraserPointingDevice); |
| [rwhv_mac_->GetInProcessNSView() tabletEvent:event]; |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| |
| event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kEraser, |
| GetPointerType(events)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithPenType) { |
| // Send a NSEvent of NSTabletProximity type which has a device type of pen. |
| NSEvent* event = MockTabletEventWithParams(kCGEventTabletProximity, true, |
| NSPenPointingDevice); |
| [rwhv_mac_->GetInProcessNSView() tabletEvent:event]; |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| |
| event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| GetPointerType(events)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithPenTypeNoTabletEvent) { |
| // Send a NSEvent of a mouse type with a subtype of |
| // NSTabletProximityEventSubtype, which indicates the input device is a pen. |
| NSEvent* event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletProximity, true); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| GetPointerType(events)); |
| events.clear(); |
| |
| event = cocoa_test_event_utils::EnterEvent({1, 1}, window_); |
| [rwhv_mac_->GetInProcessNSView() mouseEntered:event]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| static_cast<const blink::WebMouseEvent*>( |
| events[0]->ToEvent()->Event()->web_event.get()) |
| ->pointer_type); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithMouseType) { |
| // Send a NSEvent of a mouse type. |
| NSEvent* event = |
| MockMouseEventWithParams(kCGEventMouseMoved, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeDefault); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kMouse, |
| GetPointerType(events)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, PointerEventWithPenTypeSendAsTouch) { |
| // Send a NSEvent of NSTabletProximity type which has a device type of pen. |
| NSEvent* event = MockTabletEventWithParams(kCGEventTabletProximity, true, |
| NSPenPointingDevice); |
| [rwhv_mac_->GetInProcessNSView() tabletEvent:event]; |
| // Flush and clear other messages (e.g. begin frames) the RWHVMac also sends. |
| base::RunLoop().RunUntilIdle(); |
| static_cast<RenderWidgetHostImpl*>(rwhv_mac_->GetRenderWidgetHost()) |
| ->input_router() |
| ->ForceSetTouchActionAuto(); |
| |
| event = MockMouseEventWithParams( |
| kCGEventLeftMouseDown, {6, 9}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint, false, true); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("TouchStart", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| GetPointerType(events)); |
| events.clear(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| |
| event = MockMouseEventWithParams( |
| kCGEventLeftMouseDragged, {16, 29}, kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint, false, true); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("TouchMove", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| GetPointerType(events)); |
| |
| events.clear(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| |
| event = MockMouseEventWithParams(kCGEventLeftMouseUp, {16, 29}, |
| kCGMouseButtonLeft, |
| kCGEventMouseSubtypeTabletPoint, false); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("TouchEnd GestureScrollEnd", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kPen, |
| static_cast<const blink::WebTouchEvent*>( |
| events[0]->ToEvent()->Event()->web_event.get()) |
| ->touches[0] |
| .pointer_type); |
| |
| events.clear(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| |
| event = |
| MockMouseEventWithParams(kCGEventLeftMouseDown, {6, 9}, |
| kCGMouseButtonLeft, kCGEventMouseSubtypeDefault); |
| [rwhv_mac_->GetInProcessNSView() mouseEvent:event]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseDown", GetMessageNames(events)); |
| EXPECT_EQ(blink::WebPointerProperties::PointerType::kMouse, |
| GetPointerType(events)); |
| |
| events.clear(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, SendMouseMoveOnShowingContextMenu) { |
| rwhv_mac_->SetShowingContextMenu(true); |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| |
| events.clear(); |
| |
| rwhv_mac_->SetShowingContextMenu(false); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseMove", GetMessageNames(events)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, |
| IgnoreEmptyUnhandledWheelEventWithWheelGestures) { |
| // Add a delegate to the view. |
| base::scoped_nsobject<MockRenderWidgetHostViewMacDelegate> view_delegate( |
| [[MockRenderWidgetHostViewMacDelegate alloc] init]); |
| rwhv_mac_->SetDelegate(view_delegate.get()); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* event1 = MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:event1]; |
| base::RunLoop().RunUntilIdle(); |
| |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| |
| ASSERT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| // Indicate that the wheel event was unhandled. |
| events.clear(); |
| |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| |
| // GestureEventQueue allows multiple in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", GetMessageNames(events)); |
| events[0]->ToEvent()->CallCallback(INPUT_EVENT_ACK_STATE_CONSUMED); |
| |
| events.clear(); |
| |
| // 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); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:event2]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| events.clear(); |
| |
| // Check that the view delegate ignored the empty unhandled wheel event. |
| ASSERT_EQ(NO, view_delegate.get().unhandledWheelEventReceived); |
| |
| // Delete the view while |view_delegate| is still in scope. |
| rwhv_cocoa_.reset(); |
| } |
| |
| // Tests setting background transparency. See also (disabled on Mac) |
| // RenderWidgetHostTest.Background. This test has some additional checks for |
| // Mac. |
| TEST_F(RenderWidgetHostViewMacTest, Background) { |
| // If no color has been specified then background_color is not set yet. |
| ASSERT_FALSE(rwhv_mac_->GetBackgroundColor()); |
| |
| { |
| // Set the color to red. The background is initially assumed to be opaque, |
| // so no opacity message change should be sent. |
| EXPECT_CALL(mock_owner_delegate_, SetBackgroundOpaque(_)).Times(0); |
| rwhv_mac_->SetBackgroundColor(SK_ColorRED); |
| EXPECT_EQ(unsigned{SK_ColorRED}, *rwhv_mac_->GetBackgroundColor()); |
| } |
| { |
| // Set the color to blue. This should not send an opacity message. |
| EXPECT_CALL(mock_owner_delegate_, SetBackgroundOpaque(_)).Times(0); |
| rwhv_mac_->SetBackgroundColor(SK_ColorBLUE); |
| EXPECT_EQ(unsigned{SK_ColorBLUE}, *rwhv_mac_->GetBackgroundColor()); |
| } |
| { |
| // 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. |
| EXPECT_CALL(mock_owner_delegate_, SetBackgroundOpaque(false)); |
| rwhv_mac_->SetBackgroundColor(SK_ColorTRANSPARENT); |
| EXPECT_EQ(unsigned{SK_ColorWHITE}, *rwhv_mac_->GetBackgroundColor()); |
| } |
| { |
| // Set the color to blue. This should send an opacity message. |
| EXPECT_CALL(mock_owner_delegate_, SetBackgroundOpaque(true)); |
| rwhv_mac_->SetBackgroundColor(SK_ColorBLUE); |
| EXPECT_EQ(unsigned{SK_ColorBLUE}, *rwhv_mac_->GetBackgroundColor()); |
| } |
| } |
| |
| // Scrolling with a mouse wheel device on Mac won't give phase information. |
| // MouseWheelPhaseHandler adds timer based phase information to wheel events |
| // generated from this type of devices. |
| TEST_F(RenderWidgetHostViewMacTest, TimerBasedPhaseInfo) { |
| rwhv_mac_->set_mouse_wheel_wheel_phase_handler_timeout( |
| base::TimeDelta::FromMilliseconds(100)); |
| |
| // Send a wheel event without phase information for scrolling by 3 lines. |
| NSEvent* wheelEvent = MockScrollWheelEventWithoutPhase(3); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:wheelEvent]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| events.clear(); |
| events = host_->GetAndResetDispatchedMessages(); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", GetMessageNames(events)); |
| ASSERT_TRUE(static_cast<const blink::WebGestureEvent*>( |
| events[0]->ToEvent()->Event()->web_event.get()) |
| ->data.scroll_begin.synthetic); |
| events.clear(); |
| |
| // Wait for the mouse_wheel_end_dispatch_timer_ to expire, the pending wheel |
| // event gets dispatched. |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitWhenIdleClosure(), |
| base::TimeDelta::FromMilliseconds(100)); |
| run_loop.Run(); |
| |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel GestureScrollEnd", GetMessageNames(events)); |
| ASSERT_TRUE(static_cast<const blink::WebGestureEvent*>( |
| events[1]->ToEvent()->Event()->web_event.get()) |
| ->data.scroll_end.synthetic); |
| } |
| |
| // With wheel scroll latching 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(RenderWidgetHostViewMacTest, |
| 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); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* wheelEvent1 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->GetInProcessNSView() scrollWheel:wheelEvent1]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| events.clear(); |
| events = host->GetAndResetDispatchedMessages(); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", GetMessageNames(events)); |
| |
| // Send a wheel event with phaseEnded. The event will be dropped and the |
| // mouse_wheel_end_dispatch_timer_ will start. |
| NSEvent* wheelEvent2 = |
| MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [view->GetInProcessNSView() scrollWheel:wheelEvent2]; |
| base::RunLoop().RunUntilIdle(); |
| events = host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ(0U, events.size()); |
| DCHECK(view->HasPendingWheelEndEventForTesting()); |
| |
| // Get the max time here before |view| is destroyed in the |
| // ShutdownAndDestroyWidget call below. |
| const base::TimeDelta max_time_between_phase_ended_and_momentum_phase_began = |
| view->max_time_between_phase_ended_and_momentum_phase_began_for_test(); |
| |
| 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(), |
| max_time_between_phase_ended_and_momentum_phase_began); |
| run_loop.Run(); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, |
| 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); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* wheelEvent1 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->GetInProcessNSView() scrollWheel:wheelEvent1]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| // Indicate that the wheel event was unhandled. |
| events.clear(); |
| events = host->GetAndResetDispatchedMessages(); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", GetMessageNames(events)); |
| |
| // Send a wheel event with phaseEnded. The event will be dropped and the |
| // mouse_wheel_end_dispatch_timer_ will start. |
| NSEvent* wheelEvent2 = |
| MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [view->GetInProcessNSView() scrollWheel:wheelEvent2]; |
| base::RunLoop().RunUntilIdle(); |
| events = host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ(0U, events.size()); |
| DCHECK(view->HasPendingWheelEndEventForTesting()); |
| |
| // 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->GetInProcessNSView() scrollWheel:wheelEvent3]; |
| base::RunLoop().RunUntilIdle(); |
| events = host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel GestureScrollUpdate", GetMessageNames(events)); |
| DCHECK(!view->HasPendingWheelEndEventForTesting()); |
| |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, |
| 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); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Send an initial wheel event for scrolling by 3 lines. |
| NSEvent* wheelEvent1 = |
| MockScrollWheelEventWithPhase(@selector(phaseBegan), 3); |
| [view->GetInProcessNSView() scrollWheel:wheelEvent1]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| // Indicate that the wheel event was unhandled. |
| events.clear(); |
| // Both GSB and GSU will be sent since GestureEventQueue allows multiple |
| // in-flight events. |
| events = host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("GestureScrollBegin GestureScrollUpdate", GetMessageNames(events)); |
| |
| // Send a wheel event with phaseEnded. The event will be dropped and the |
| // mouse_wheel_end_dispatch_timer_ will start. |
| NSEvent* wheelEvent2 = |
| MockScrollWheelEventWithPhase(@selector(phaseEnded), 0); |
| [view->GetInProcessNSView() scrollWheel:wheelEvent2]; |
| base::RunLoop().RunUntilIdle(); |
| events = host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ(0U, events.size()); |
| DCHECK(view->HasPendingWheelEndEventForTesting()); |
| |
| // 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->GetInProcessNSView() scrollWheel:wheelEvent3]; |
| base::RunLoop().RunUntilIdle(); |
| events = host->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("MouseWheel GestureScrollEnd MouseWheel", GetMessageNames(events)); |
| DCHECK(!view->HasPendingWheelEndEventForTesting()); |
| |
| host->ShutdownAndDestroyWidget(true); |
| } |
| |
| class RenderWidgetHostViewMacPinchTest |
| : public RenderWidgetHostViewMacTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| RenderWidgetHostViewMacPinchTest() : async_events_enabled_(GetParam()) { |
| if (async_events_enabled_) { |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kTouchpadAsyncPinchEvents); |
| } else { |
| scoped_feature_list_.InitAndDisableFeature( |
| features::kTouchpadAsyncPinchEvents); |
| } |
| } |
| |
| 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 SendBeginPinchEvent() { |
| NSEvent* pinchBeginEvent = MockPinchEvent(NSEventPhaseBegan, 0); |
| if (ShouldSendGestureEvents()) |
| [rwhv_cocoa_ beginGestureWithEvent:pinchBeginEvent]; |
| [rwhv_cocoa_ magnifyWithEvent:pinchBeginEvent]; |
| } |
| |
| void SendEndPinchEvent() { |
| NSEvent* pinchEndEvent = MockPinchEvent(NSEventPhaseEnded, 0); |
| [rwhv_cocoa_ magnifyWithEvent:pinchEndEvent]; |
| if (ShouldSendGestureEvents()) |
| [rwhv_cocoa_ endGestureWithEvent:pinchEndEvent]; |
| } |
| |
| const bool async_events_enabled_; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewMacPinchTest); |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(, RenderWidgetHostViewMacPinchTest, testing::Bool()); |
| |
| TEST_P(RenderWidgetHostViewMacPinchTest, PinchThresholding) { |
| // Do a gesture that crosses the threshold. |
| { |
| NSEvent* pinchUpdateEvents[3] = { |
| MockPinchEvent(NSEventPhaseChanged, 0.25), |
| MockPinchEvent(NSEventPhaseChanged, 0.25), |
| MockPinchEvent(NSEventPhaseChanged, 0.25), |
| }; |
| |
| SendBeginPinchEvent(); |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| |
| EXPECT_EQ(0U, events.size()); |
| |
| // No zoom is sent for the first update event. |
| [rwhv_cocoa_ magnifyWithEvent:pinchUpdateEvents[0]]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| // After acking the synthetic mouse wheel, no GesturePinch events are |
| // produced. |
| events[0]->ToEvent()->CallCallback( |
| INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ(0U, events.size()); |
| |
| // The second update event crosses the threshold of 0.4, and so zoom is no |
| // longer disabled. |
| [rwhv_cocoa_ magnifyWithEvent:pinchUpdateEvents[1]]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| |
| if (async_events_enabled_) { |
| EXPECT_EQ("MouseWheel GesturePinchBegin GesturePinchUpdate", |
| GetMessageNames(events)); |
| } else { |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| // Now acking the synthetic mouse wheel does produce GesturePinch events. |
| events[0]->ToEvent()->CallCallback( |
| INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("GesturePinchBegin GesturePinchUpdate", |
| GetMessageNames(events)); |
| } |
| |
| // The third update still has zoom enabled. |
| [rwhv_cocoa_ magnifyWithEvent:pinchUpdateEvents[2]]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| if (async_events_enabled_) { |
| EXPECT_EQ("MouseWheel GesturePinchUpdate", GetMessageNames(events)); |
| } else { |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| events[0]->ToEvent()->CallCallback( |
| INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("GesturePinchUpdate", GetMessageNames(events)); |
| } |
| |
| SendEndPinchEvent(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel GesturePinchEnd", GetMessageNames(events)); |
| } |
| |
| // 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 = MockPinchEvent(NSEventPhaseChanged, 0.25); |
| |
| rwhv_mac_->page_at_minimum_scale_ = false; |
| |
| SendBeginPinchEvent(); |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ(0U, events.size()); |
| |
| // Expect that a zoom happen because the time threshold has not passed. |
| [rwhv_cocoa_ magnifyWithEvent:pinchUpdateEvent]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| events[0]->ToEvent()->CallCallback( |
| INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("GesturePinchBegin GesturePinchUpdate", GetMessageNames(events)); |
| |
| SendEndPinchEvent(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel GesturePinchEnd", GetMessageNames(events)); |
| } |
| |
| // Do a gesture again, after the page scale is no longer at one, and ensure |
| // that it is thresholded again. |
| { |
| NSEvent* pinchUpdateEvent = MockPinchEvent(NSEventTypeMagnify, 0.25); |
| |
| rwhv_mac_->page_at_minimum_scale_ = true; |
| |
| SendBeginPinchEvent(); |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ(0U, events.size()); |
| |
| // 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). |
| rwhv_mac_->page_at_minimum_scale_ = false; |
| |
| // Expect that zoom be disabled because the time threshold has passed. |
| [rwhv_cocoa_ magnifyWithEvent:pinchUpdateEvent]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| events[0]->ToEvent()->CallCallback( |
| INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ(0U, events.size()); |
| |
| // Since no GesturePinchBegin was sent by the time we reach the pinch end, |
| // the GesturePinchBegin and GesturePinchEnd are elided. |
| SendEndPinchEvent(); |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| } |
| } |
| |
| // Tests that the NSEventTypeSmartMagnify event is first offered as a mouse |
| // wheel event and is then sent as a GestureDoubleTap to invoke the double-tap |
| // to zoom logic. |
| TEST_F(RenderWidgetHostViewMacTest, DoubleTapZoom) { |
| NSEvent* smartMagnifyEvent = MockSmartMagnifyEvent(); |
| [rwhv_cocoa_ smartMagnifyWithEvent:smartMagnifyEvent]; |
| base::RunLoop().RunUntilIdle(); |
| |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| events[0]->ToEvent()->CallCallback(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("GestureDoubleTap", GetMessageNames(events)); |
| } |
| |
| // Tests that the NSEventTypeSmartMagnify event may be consumed by a wheel |
| // listener to prevent the scale change. |
| TEST_F(RenderWidgetHostViewMacTest, DoubleTapZoomConsumed) { |
| NSEvent* smartMagnifyEvent = MockSmartMagnifyEvent(); |
| [rwhv_cocoa_ smartMagnifyWithEvent:smartMagnifyEvent]; |
| base::RunLoop().RunUntilIdle(); |
| |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("MouseWheel", GetMessageNames(events)); |
| |
| events[0]->ToEvent()->CallCallback(INPUT_EVENT_ACK_STATE_CONSUMED); |
| |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ(0U, events.size()); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, EventLatencyOSMouseWheelHistogram) { |
| base::HistogramTester histogram_tester; |
| |
| // 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); |
| [rwhv_mac_->GetInProcessNSView() scrollWheel:wheelEvent]; |
| histogram_tester.ExpectTotalCount("Event.Latency.OS.MOUSE_WHEEL", 1); |
| } |
| |
| // 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_browser_context_ = std::make_unique<TestBrowserContext>(); |
| child_process_host_ = |
| new MockRenderProcessHost(child_browser_context_.get()); |
| child_process_host_->Init(); |
| child_widget_ = MockRenderWidgetHostImpl::Create( |
| &delegate_, child_process_host_, |
| child_process_host_->GetNextRoutingID()); |
| child_view_ = new TestRenderWidgetHostView(child_widget_); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void TearDown() override { |
| child_widget_->ShutdownAndDestroyWidget(true); |
| child_browser_context_.reset(); |
| 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 delegate_.GetTextInputManager(); |
| } |
| RenderWidgetHostViewMac* tab_view() { return rwhv_mac_; } |
| RenderWidgetHostImpl* tab_widget() { return host_.get(); } |
| RenderWidgetHostViewCocoa* tab_GetInProcessNSView() { |
| return rwhv_cocoa_.get(); |
| } |
| |
| API_AVAILABLE(macos(10.12.2)) |
| NSCandidateListTouchBarItem* candidate_list_item() { |
| return [tab_GetInProcessNSView().touchBar |
| itemForIdentifier:NSTouchBarItemIdentifierCandidateList]; |
| } |
| |
| protected: |
| MockRenderProcessHost* child_process_host_; |
| MockRenderWidgetHostImpl* child_widget_; |
| TestRenderWidgetHostView* child_view_; |
| |
| private: |
| std::unique_ptr<TestBrowserContext> child_browser_context_; |
| |
| 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()); |
| [tab_GetInProcessNSView() unmarkText]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| child_widget_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("FinishComposingText", GetMessageNames(events)); |
| |
| // 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_GetInProcessNSView() unmarkText]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("FinishComposingText", GetMessageNames(events)); |
| } |
| |
| // 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()); |
| [tab_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| child_widget_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("SetComposition", GetMessageNames(events)); |
| |
| // 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_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("SetComposition", GetMessageNames(events)); |
| } |
| |
| // 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()); |
| [tab_GetInProcessNSView() insertText:text replacementRange:replacementRange]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| child_widget_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("CommitText", GetMessageNames(events)); |
| |
| // 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_GetInProcessNSView() insertText:text replacementRange:replacementRange]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("CommitText", GetMessageNames(events)); |
| } |
| |
| // 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()); |
| // 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. |
| [tab_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| [tab_GetInProcessNSView() finishComposingText]; |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| child_widget_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("SetComposition FinishComposingText", GetMessageNames(events)); |
| |
| // 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_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| [tab_GetInProcessNSView() finishComposingText]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("SetComposition FinishComposingText", GetMessageNames(events)); |
| } |
| |
| TEST_F(InputMethodMacTest, SecurePasswordInput) { |
| ASSERT_FALSE(ui::ScopedPasswordInputEnabler::IsPasswordInputEnabled()); |
| ASSERT_EQ(text_input_manager(), tab_view()->GetTextInputManager()); |
| |
| // RenderWidgetHostViewMacTest.BlurAndFocusOnSetActive checks the |
| // Focus()/Blur() rules, just silence the warnings here. |
| EXPECT_CALL(*host_, Focus()).Times(::testing::AnyNumber()); |
| EXPECT_CALL(*host_, Blur()).Times(::testing::AnyNumber()); |
| |
| [window_ makeFirstResponder:tab_view()->GetInProcessNSView()]; |
| |
| // Shouldn't enable secure input if it's not a password textfield. |
| tab_view()->SetActive(true); |
| EXPECT_FALSE(ui::ScopedPasswordInputEnabler::IsPasswordInputEnabled()); |
| |
| SetTextInputType(child_view_, ui::TEXT_INPUT_TYPE_PASSWORD); |
| ASSERT_EQ(child_widget_, text_input_manager()->GetActiveWidget()); |
| ASSERT_EQ(text_input_manager(), tab_view()->GetTextInputManager()); |
| ASSERT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, tab_view()->GetTextInputType()); |
| |
| // Single matched calls immediately update IsPasswordInputEnabled(). |
| tab_view()->SetActive(true); |
| EXPECT_TRUE(ui::ScopedPasswordInputEnabler::IsPasswordInputEnabled()); |
| |
| tab_view()->SetActive(false); |
| EXPECT_FALSE(ui::ScopedPasswordInputEnabler::IsPasswordInputEnabled()); |
| } |
| |
| // 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. |
| [tab_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE([tab_GetInProcessNSView() hasMarkedText]); |
| child_view_->ImeCancelComposition(); |
| EXPECT_FALSE([tab_GetInProcessNSView() hasMarkedText]); |
| |
| // Repeat for the tab's view. |
| [tab_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE([tab_GetInProcessNSView() hasMarkedText]); |
| tab_view()->ImeCancelComposition(); |
| EXPECT_FALSE([tab_GetInProcessNSView() hasMarkedText]); |
| } |
| |
| // This test verifies that calling FocusedNodeChanged() on |
| // RenderWidgetHostViewMac calls cancelComposition on the Cocoa view. |
| TEST_F(InputMethodMacTest, FocusedNodeChanged) { |
| // 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); |
| |
| [tab_GetInProcessNSView() setMarkedText:text |
| selectedRange:selectedRange |
| replacementRange:replacementRange]; |
| EXPECT_TRUE([tab_GetInProcessNSView() hasMarkedText]); |
| tab_view()->FocusedNodeChanged(true, gfx::Rect()); |
| EXPECT_FALSE([tab_GetInProcessNSView() 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. |
| [window_ makeFirstResponder:tab_GetInProcessNSView()]; |
| EXPECT_TRUE(tab_view()->HasFocus()); |
| |
| TextInputState state; |
| state.type = ui::TEXT_INPUT_TYPE_TEXT; |
| |
| // Make the tab's widget active. |
| tab_view()->TextInputStateChanged(state); |
| |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| // The tab's widget must have received an IPC regarding composition updates. |
| EXPECT_EQ("SetFocus RequestCompositionUpdates", GetMessageNames(events)); |
| |
| // The message should ask for monitoring updates, but no immediate update. |
| MockWidgetInputHandler::DispatchedRequestCompositionUpdatesMessage* message = |
| events.at(1)->ToRequestCompositionUpdates(); |
| EXPECT_FALSE(message->immediate_request()); |
| EXPECT_TRUE(message->monitor_request()); |
| |
| // Now make the child view active. |
| child_view_->TextInputStateChanged(state); |
| |
| // The tab should receive another IPC for composition updates. |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| // The tab's widget must have received an IPC regarding composition updates. |
| EXPECT_EQ("RequestCompositionUpdates", GetMessageNames(events)); |
| // This time, the tab should have been asked to stop monitoring (and no |
| // immediate updates). |
| message = events.at(0)->ToRequestCompositionUpdates(); |
| EXPECT_FALSE(message->immediate_request()); |
| EXPECT_FALSE(message->monitor_request()); |
| |
| // The child too must have received an IPC for composition updates. |
| events = child_widget_->GetAndResetDispatchedMessages(); |
| EXPECT_EQ("RequestCompositionUpdates", GetMessageNames(events)); |
| |
| // Verify that the message is asking for monitoring to start; but no immediate |
| // updates. |
| message = events.at(0)->ToRequestCompositionUpdates(); |
| EXPECT_FALSE(message->immediate_request()); |
| EXPECT_TRUE(message->monitor_request()); |
| |
| // Make the tab view active again. |
| tab_view()->TextInputStateChanged(state); |
| |
| base::RunLoop().RunUntilIdle(); |
| events = child_widget_->GetAndResetDispatchedMessages(); |
| |
| // Verify that the child received another IPC for composition updates. |
| EXPECT_EQ("RequestCompositionUpdates", GetMessageNames(events)); |
| |
| // Verify that this IPC is asking for no monitoring or immediate updates. |
| message = events.at(0)->ToRequestCompositionUpdates(); |
| EXPECT_FALSE(message->immediate_request()); |
| EXPECT_FALSE(message->monitor_request()); |
| } |
| |
| TEST_F(InputMethodMacTest, TouchBarTextSuggestionsPresence) { |
| base::test::ScopedFeatureList feature_list; |
| if (@available(macOS 10.12.2, *)) { |
| EXPECT_NSEQ(nil, candidate_list_item()); |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_NSNE(nil, candidate_list_item()); |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_NSNE(nil, candidate_list_item()); |
| } |
| } |
| |
| TEST_F(InputMethodMacTest, TouchBarTextSuggestionsReplacement) { |
| base::test::ScopedFeatureList feature_list; |
| if (@available(macOS 10.12.2, *)) { |
| base::scoped_nsobject<FakeSpellChecker> spellChecker( |
| [[FakeSpellChecker alloc] init]); |
| tab_GetInProcessNSView().spellCheckerForTesting = |
| static_cast<NSSpellChecker*>(spellChecker.get()); |
| |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_NSNE(nil, candidate_list_item()); |
| candidate_list_item().allowsCollapsing = NO; |
| |
| FakeTextCheckingResult* fakeResult = |
| [FakeTextCheckingResult resultWithRange:NSMakeRange(0, 3) |
| replacementString:@"foo"]; |
| |
| const base::string16 kOriginalString = base::UTF8ToUTF16("abcxxxghi"); |
| |
| // Change the selection once; requests completions from the spell checker. |
| tab_view()->SelectionChanged(kOriginalString, 3, gfx::Range(3, 3)); |
| |
| NSInteger firstSequenceNumber = [spellChecker sequenceNumber]; |
| auto firstCompletionHandler = [spellChecker takeCompletionHandler]; |
| |
| EXPECT_NE(nil, (id)firstCompletionHandler.get()); |
| EXPECT_EQ(0U, candidate_list_item().candidates.count); |
| |
| // Instead of replying right away, change the selection again! |
| tab_view()->SelectionChanged(kOriginalString, 3, gfx::Range(5, 5)); |
| |
| EXPECT_NE(firstSequenceNumber, [spellChecker sequenceNumber]); |
| |
| // Make sure that calling the stale completion handler is a no-op. |
| firstCompletionHandler.get()( |
| firstSequenceNumber, |
| @[ static_cast<NSTextCheckingResult*>(fakeResult) ]); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(0U, candidate_list_item().candidates.count); |
| |
| // But calling the current handler should work. |
| [spellChecker takeCompletionHandler].get()( |
| [spellChecker sequenceNumber], |
| @[ static_cast<NSTextCheckingResult*>(fakeResult) ]); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(1U, candidate_list_item().candidates.count); |
| |
| base::RunLoop().RunUntilIdle(); |
| MockWidgetInputHandler::MessageVector events = |
| host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("", GetMessageNames(events)); |
| |
| // Now, select that result. |
| [tab_GetInProcessNSView() candidateListTouchBarItem:candidate_list_item() |
| endSelectingCandidateAtIndex:0]; |
| base::RunLoop().RunUntilIdle(); |
| events = host_->GetAndResetDispatchedMessages(); |
| ASSERT_EQ("CommitText", GetMessageNames(events)); |
| } |
| } |
| |
| TEST_F(InputMethodMacTest, TouchBarTextSuggestionsNotRequestedForPasswords) { |
| base::test::ScopedFeatureList feature_list; |
| if (@available(macOS 10.12.2, *)) { |
| base::scoped_nsobject<FakeSpellChecker> spellChecker( |
| [[FakeSpellChecker alloc] init]); |
| tab_GetInProcessNSView().spellCheckerForTesting = |
| static_cast<NSSpellChecker*>(spellChecker.get()); |
| |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_NSNE(nil, candidate_list_item()); |
| candidate_list_item().allowsCollapsing = NO; |
| |
| const base::string16 kOriginalString = base::UTF8ToUTF16("abcxxxghi"); |
| |
| // Change the selection once; completions should *not* be requested. |
| tab_view()->SelectionChanged(kOriginalString, 3, gfx::Range(3, 3)); |
| |
| EXPECT_EQ(0U, [spellChecker sequenceNumber]); |
| } |
| } |
| |
| // https://crbug.com/893038: There exist code paths which set the selection |
| // range to gfx::Range::InvalidRange(). I'm not sure how to exercise them in |
| // practice, but this has caused crashes in the field. |
| TEST_F(InputMethodMacTest, TouchBarTextSuggestionsInvalidSelection) { |
| base::test::ScopedFeatureList feature_list; |
| if (@available(macOS 10.12.2, *)) { |
| base::scoped_nsobject<FakeSpellChecker> spellChecker( |
| [[FakeSpellChecker alloc] init]); |
| tab_GetInProcessNSView().spellCheckerForTesting = |
| static_cast<NSSpellChecker*>(spellChecker.get()); |
| |
| SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT); |
| candidate_list_item().allowsCollapsing = NO; |
| |
| if (auto completionHandler = [spellChecker takeCompletionHandler]) { |
| completionHandler.get()([spellChecker sequenceNumber], @[]); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| FakeTextCheckingResult* fakeResult = |
| [FakeTextCheckingResult resultWithRange:NSMakeRange(0, 3) |
| replacementString:@"foo"]; |
| |
| const base::string16 kOriginalString = base::UTF8ToUTF16("abcxxxghi"); |
| |
| tab_view()->SelectionChanged(kOriginalString, 3, |
| gfx::Range::InvalidRange()); |
| |
| if (auto completionHandler = [spellChecker takeCompletionHandler]) { |
| completionHandler.get()( |
| [spellChecker sequenceNumber], |
| @[ static_cast<NSTextCheckingResult*>(fakeResult) ]); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| EXPECT_NE(nil, candidate_list_item().candidates); |
| EXPECT_EQ(0U, candidate_list_item().candidates.count); |
| } |
| } |
| |
| // This test verifies that in AutoResize mode a child-allocated |
| // viz::LocalSurfaceId will be properly routed and stored in the parent. |
| TEST_F(RenderWidgetHostViewMacTest, ChildAllocationAcceptedInParent) { |
| viz::LocalSurfaceId local_surface_id1( |
| rwhv_mac_->GetLocalSurfaceIdAllocation().local_surface_id()); |
| EXPECT_TRUE(local_surface_id1.is_valid()); |
| |
| host_->SetAutoResize(true, gfx::Size(50, 50), gfx::Size(100, 100)); |
| |
| viz::ChildLocalSurfaceIdAllocator child_allocator; |
| child_allocator.UpdateFromParent(rwhv_mac_->GetLocalSurfaceIdAllocation()); |
| child_allocator.GenerateId(); |
| viz::LocalSurfaceId local_surface_id2 = |
| child_allocator.GetCurrentLocalSurfaceIdAllocation().local_surface_id(); |
| cc::RenderFrameMetadata metadata; |
| metadata.viewport_size_in_pixels = gfx::Size(75, 75); |
| metadata.local_surface_id_allocation = |
| child_allocator.GetCurrentLocalSurfaceIdAllocation(); |
| host_->DidUpdateVisualProperties(metadata); |
| |
| viz::LocalSurfaceId local_surface_id3( |
| rwhv_mac_->GetLocalSurfaceIdAllocation().local_surface_id()); |
| EXPECT_NE(local_surface_id1, local_surface_id3); |
| EXPECT_EQ(local_surface_id2, local_surface_id3); |
| } |
| |
| // This test verifies that when the child and parent both allocate their own |
| // viz::LocalSurfaceId the resulting conflict is resolved. |
| TEST_F(RenderWidgetHostViewMacTest, ConflictingAllocationsResolve) { |
| viz::LocalSurfaceId local_surface_id1( |
| rwhv_mac_->GetLocalSurfaceIdAllocation().local_surface_id()); |
| EXPECT_TRUE(local_surface_id1.is_valid()); |
| |
| host_->SetAutoResize(true, gfx::Size(50, 50), gfx::Size(100, 100)); |
| viz::ChildLocalSurfaceIdAllocator child_allocator; |
| child_allocator.UpdateFromParent(rwhv_mac_->GetLocalSurfaceIdAllocation()); |
| child_allocator.GenerateId(); |
| viz::LocalSurfaceId local_surface_id2 = |
| child_allocator.GetCurrentLocalSurfaceIdAllocation().local_surface_id(); |
| cc::RenderFrameMetadata metadata; |
| metadata.viewport_size_in_pixels = gfx::Size(75, 75); |
| metadata.local_surface_id_allocation = |
| child_allocator.GetCurrentLocalSurfaceIdAllocation(); |
| host_->DidUpdateVisualProperties(metadata); |
| |
| // Cause a conflicting viz::LocalSurfaceId allocation |
| BrowserCompositorMac* browser_compositor = rwhv_mac_->BrowserCompositor(); |
| EXPECT_TRUE(browser_compositor->ForceNewSurfaceForTesting()); |
| viz::LocalSurfaceId local_surface_id3( |
| rwhv_mac_->GetLocalSurfaceIdAllocation().local_surface_id()); |
| EXPECT_NE(local_surface_id1, local_surface_id3); |
| |
| // RenderWidgetHostImpl has delayed auto-resize processing. Yield here to |
| // let it complete. |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| viz::LocalSurfaceId local_surface_id4( |
| rwhv_mac_->GetLocalSurfaceIdAllocation().local_surface_id()); |
| EXPECT_NE(local_surface_id1, local_surface_id4); |
| EXPECT_NE(local_surface_id2, local_surface_id4); |
| viz::LocalSurfaceId merged_local_surface_id( |
| local_surface_id2.parent_sequence_number() + 1, |
| local_surface_id2.child_sequence_number(), |
| local_surface_id2.embed_token()); |
| EXPECT_EQ(local_surface_id4, merged_local_surface_id); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, TransformToRootNoParentLayer) { |
| gfx::PointF point(10, 20); |
| rwhv_mac_->TransformPointToRootSurface(&point); |
| EXPECT_EQ(point, gfx::PointF(10, 20)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, TransformToRootWithParentLayer) { |
| std::unique_ptr<ui::RecyclableCompositorMac> compositor = |
| ui::RecyclableCompositorMacFactory::Get()->CreateCompositor( |
| ImageTransportFactory::GetInstance()->GetContextFactory(), |
| ImageTransportFactory::GetInstance()->GetContextFactoryPrivate()); |
| std::unique_ptr<ui::Layer> root_surface_layer = |
| std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR); |
| std::unique_ptr<ui::Layer> parent_layer = |
| std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR); |
| |
| compositor->compositor()->SetRootLayer(root_surface_layer.get()); |
| root_surface_layer->SetBounds(gfx::Rect(-5, -10, 1000, 2000)); |
| parent_layer->SetBounds(gfx::Rect(100, 300, 500, 400)); |
| root_surface_layer->Add(parent_layer.get()); |
| gfx::PointF point(10, 20); |
| rwhv_mac_->SetParentUiLayer(parent_layer.get()); |
| rwhv_mac_->TransformPointToRootSurface(&point); |
| EXPECT_EQ(point, gfx::PointF(105, 310)); |
| } |
| |
| TEST_F(RenderWidgetHostViewMacTest, AccessibilityParentTest) { |
| NSView* view = rwhv_mac_->GetInProcessNSView(); |
| |
| // NSBox so it participates in the a11y hierarchy. |
| base::scoped_nsobject<NSView> parent_view([[NSBox alloc] init]); |
| base::scoped_nsobject<NSView> accessibility_parent([[NSView alloc] init]); |
| base::scoped_nsobject<NSWindow> window([[NSWindow alloc] |
| initWithContentRect:NSMakeRect(0, 0, 512, 512) |
| styleMask:NSWindowStyleMaskResizable | NSWindowStyleMaskTitled |
| backing:NSBackingStoreBuffered |
| defer:NO]); |
| [[window contentView] addSubview:accessibility_parent]; |
| |
| [parent_view addSubview:view]; |
| EXPECT_NSEQ([view accessibilityParent], parent_view); |
| |
| rwhv_mac_->SetParentAccessibilityElement(accessibility_parent); |
| EXPECT_NSEQ([view accessibilityParent], |
| NSAccessibilityUnignoredAncestor(accessibility_parent)); |
| EXPECT_NSNE(nil, [view accessibilityParent]); |
| |
| rwhv_mac_->SetParentAccessibilityElement(nil); |
| EXPECT_NSEQ([view accessibilityParent], parent_view); |
| } |
| |
| } // namespace content |