[macOS] FlutterView should not override platform view cursor (#52159)

This fixes an edge case that I've overlooked in
https://github.com/flutter/engine/pull/43101.

It is possible to configure `FlutterViewController` tracking area with
`FlutterMouseTrackingModeAlways` option, in which case the tracking area
will call `[FlutterView cursorUpdate:]` unconditionally even if the
mouse cursor is over platform view and the platform view has already
handled the mouse update.

The solution is to prevent the `FlutterView` from updating the cursor
when the cursor is over a platform view. This can be accomplished by
doing a hitTest inside `[FlutterView cursorUpdate:]`.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm
index 29eec73..bafe698 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm
@@ -106,6 +106,11 @@
 // - When context menu above FlutterView is closed. Context menu will change current cursor to arrow
 // and will not restore it back.
 - (void)cursorUpdate:(NSEvent*)event {
+  // Make sure to not override cursor when over a platform view.
+  NSView* hitTestView = [self hitTest:[self convertPoint:event.locationInWindow fromView:nil]];
+  if (hitTestView != self) {
+    return;
+  }
   [_lastCursor set];
   // It is possible that there is a platform view with NSTrackingArea below flutter content.
   // This could override the mouse cursor as a result of mouse move event. There is no good way
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm
index 65620a5..8e80077 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm
@@ -37,3 +37,124 @@
                                                       viewId:kImplicitViewId];
   EXPECT_EQ([view layer:view.layer shouldInheritContentsScale:3.0 fromWindow:view.window], YES);
 }
+
+@interface TestFlutterView : FlutterView
+
+@property(readwrite, nonatomic) NSView* (^onHitTest)(NSPoint point);
+
+@end
+
+@implementation TestFlutterView
+
+@synthesize onHitTest;
+
+- (NSView*)hitTest:(NSPoint)point {
+  return self.onHitTest(point);
+}
+
+- (void)reshaped {
+  // Disable resize synchronization for testing.
+}
+
+@end
+
+@interface TestCursor : NSCursor
+@property(readwrite, nonatomic) BOOL setCalled;
+@end
+
+@implementation TestCursor
+
+- (void)set {
+  self.setCalled = YES;
+}
+
+@end
+
+TEST(FlutterView, CursorUpdateDoesHitTest) {
+  id<MTLDevice> device = MTLCreateSystemDefaultDevice();
+  id<MTLCommandQueue> queue = [device newCommandQueue];
+  TestFlutterViewDelegate* delegate = [[TestFlutterViewDelegate alloc] init];
+  FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init];
+  TestFlutterView* view = [[TestFlutterView alloc] initWithMTLDevice:device
+                                                        commandQueue:queue
+                                                            delegate:delegate
+                                                  threadSynchronizer:threadSynchronizer
+                                                              viewId:kImplicitViewId];
+  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
+                                                 styleMask:NSBorderlessWindowMask
+                                                   backing:NSBackingStoreBuffered
+                                                     defer:NO];
+
+  TestCursor* cursor = [[TestCursor alloc] init];
+
+  window.contentView = view;
+  __weak NSView* weakView = view;
+  __block BOOL hitTestCalled = NO;
+  __block NSPoint hitTestCoordinate = NSZeroPoint;
+  view.onHitTest = ^NSView*(NSPoint point) {
+    hitTestCalled = YES;
+    hitTestCoordinate = point;
+    return weakView;
+  };
+  NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSEventTypeMouseMoved
+                                           location:NSMakePoint(100, 100)
+                                      modifierFlags:0
+                                          timestamp:0
+                                       windowNumber:0
+                                            context:nil
+                                        eventNumber:0
+                                         clickCount:0
+                                           pressure:0];
+  [view didUpdateMouseCursor:cursor];
+  [view cursorUpdate:mouseEvent];
+
+  EXPECT_TRUE(hitTestCalled);
+  // The hit test coordinate should be in the window coordinate system.
+  EXPECT_TRUE(CGPointEqualToPoint(hitTestCoordinate, CGPointMake(100, 500)));
+  EXPECT_TRUE(cursor.setCalled);
+}
+
+TEST(FlutterView, CursorUpdateDoesNotOverridePlatformView) {
+  id<MTLDevice> device = MTLCreateSystemDefaultDevice();
+  id<MTLCommandQueue> queue = [device newCommandQueue];
+  TestFlutterViewDelegate* delegate = [[TestFlutterViewDelegate alloc] init];
+  FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init];
+  TestFlutterView* view = [[TestFlutterView alloc] initWithMTLDevice:device
+                                                        commandQueue:queue
+                                                            delegate:delegate
+                                                  threadSynchronizer:threadSynchronizer
+                                                              viewId:kImplicitViewId];
+  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
+                                                 styleMask:NSBorderlessWindowMask
+                                                   backing:NSBackingStoreBuffered
+                                                     defer:NO];
+
+  TestCursor* cursor = [[TestCursor alloc] init];
+
+  NSView* platformView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
+
+  window.contentView = view;
+  __block BOOL hitTestCalled = NO;
+  __block NSPoint hitTestCoordinate = NSZeroPoint;
+  view.onHitTest = ^NSView*(NSPoint point) {
+    hitTestCalled = YES;
+    hitTestCoordinate = point;
+    return platformView;
+  };
+  NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSEventTypeMouseMoved
+                                           location:NSMakePoint(100, 100)
+                                      modifierFlags:0
+                                          timestamp:0
+                                       windowNumber:0
+                                            context:nil
+                                        eventNumber:0
+                                         clickCount:0
+                                           pressure:0];
+  [view didUpdateMouseCursor:cursor];
+  [view cursorUpdate:mouseEvent];
+
+  EXPECT_TRUE(hitTestCalled);
+  // The hit test coordinate should be in the window coordinate system.
+  EXPECT_TRUE(CGPointEqualToPoint(hitTestCoordinate, CGPointMake(100, 500)));
+  EXPECT_FALSE(cursor.setCalled);
+}