Add a flutter app that can resize itself as integration test app. (#112297)

diff --git a/.ci.yaml b/.ci.yaml
index 031f7a5..e647fd5 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -2642,6 +2642,16 @@
         ["devicelab", "hostonly"]
       task_name: hello_world_macos__compile
 
+  - name: Mac integration_ui_test_test_macos
+    bringup: true
+    recipe: devicelab/devicelab_drone
+    presubmit: false
+    timeout: 60
+    properties:
+      tags: >
+        ["devicelab", "mac"]
+      task_name: integration_ui_test_test_macos
+
   - name: Mac module_custom_host_app_name_test
     recipe: devicelab/devicelab_drone
     timeout: 60
diff --git a/TESTOWNERS b/TESTOWNERS
index f30f239..9ae5008 100644
--- a/TESTOWNERS
+++ b/TESTOWNERS
@@ -219,10 +219,10 @@
 /dev/devicelab/bin/tasks/flutter_gallery_macos__start_up.dart @a-wallen @flutter/desktop
 /dev/devicelab/bin/tasks/flutter_gallery_win_desktop__compile.dart @yaakovschectman @flutter/desktop
 /dev/devicelab/bin/tasks/flutter_gallery_win_desktop__start_up.dart @yaakovschectman @flutter/desktop
-/dev/devicelab/bin/tasks/flutter_view_macos__start_up.dart @a-wallen @flutter/desktop
-/dev/devicelab/bin/tasks/flutter_tool_startup__windows.dart @jensjoha @flutter/tool
 /dev/devicelab/bin/tasks/flutter_tool_startup__linux.dart @jensjoha @flutter/tool
 /dev/devicelab/bin/tasks/flutter_tool_startup__macos.dart @jensjoha @flutter/tool
+/dev/devicelab/bin/tasks/flutter_tool_startup__windows.dart @jensjoha @flutter/tool
+/dev/devicelab/bin/tasks/flutter_view_macos__start_up.dart @a-wallen @flutter/desktop
 /dev/devicelab/bin/tasks/flutter_view_win_desktop__start_up.dart @yaakovschectman @flutter/desktop
 /dev/devicelab/bin/tasks/gradle_desugar_classes_test.dart @zanderso @flutter/tool
 /dev/devicelab/bin/tasks/gradle_non_android_plugin_test.dart @stuartmorgan @flutter/plugin
@@ -236,6 +236,7 @@
 /dev/devicelab/bin/tasks/hello_world_macos__compile.dart @a-wallen @flutter/desktop
 /dev/devicelab/bin/tasks/hello_world_win_desktop__compile.dart @yaakovschectman @flutter/desktop
 /dev/devicelab/bin/tasks/hot_mode_dev_cycle_win_target__benchmark.dart @cbracken @flutter/desktop
+/dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart @a-wallen @flutter/desktop
 /dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart @zanderso @flutter/tool
 /dev/devicelab/bin/tasks/module_host_with_custom_build_test.dart @zanderso @flutter/tool
 /dev/devicelab/bin/tasks/module_test_ios.dart @jmagman @flutter/tool
diff --git a/dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart b/dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart
new file mode 100644
index 0000000..0026817
--- /dev/null
+++ b/dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart
@@ -0,0 +1,12 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/framework/devices.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
+
+Future<void> main() async {
+  deviceOperatingSystem = DeviceOperatingSystem.macos;
+  await task(createEndToEndIntegrationTest());
+}
diff --git a/dev/integration_tests/ui/integration_test/resize_integration_test.dart b/dev/integration_tests/ui/integration_test/resize_integration_test.dart
new file mode 100644
index 0000000..6dedbc1
--- /dev/null
+++ b/dev/integration_tests/ui/integration_test/resize_integration_test.dart
@@ -0,0 +1,57 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart' as widgets show Container, Size, runApp;
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:integration_ui/resize.dart' as app;
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('end-to-end test', () {
+    testWidgets('Use button to resize window',
+        timeout: const Timeout(Duration(seconds: 5)),
+        (WidgetTester tester) async {
+      const app.ResizeApp resizeApp = app.ResizeApp();
+
+      widgets.runApp(resizeApp);
+      await tester.pumpAndSettle();
+
+      final Finder fab = find.byKey(app.ResizeApp.extendedFab);
+      expect(fab, findsOneWidget);
+
+      final Finder root = find.byWidget(resizeApp);
+      final widgets.Size sizeBefore = tester.getSize(root);
+
+      await tester.tap(fab);
+      await tester.pumpAndSettle();
+
+      final widgets.Size sizeAfter = tester.getSize(root);
+      expect(sizeAfter.width, equals(sizeBefore.width + app.ResizeApp.resizeBy));
+      expect(sizeAfter.height, equals(sizeBefore.height + app.ResizeApp.resizeBy));
+
+      final Finder widthLabel = find.byKey(app.ResizeApp.widthLabel);
+      expect(widthLabel, findsOneWidget);
+      expect(find.text('width: ${sizeAfter.width}'), findsOneWidget);
+
+      final Finder heightLabel = find.byKey(app.ResizeApp.heightLabel);
+      expect(heightLabel, findsOneWidget);
+      expect(find.text('height: ${sizeAfter.height}'), findsOneWidget);
+    });
+  });
+
+  testWidgets('resize window after calling runApp twice, the second with no content',
+      timeout: const Timeout(Duration(seconds: 5)),
+      (WidgetTester tester) async {
+    const app.ResizeApp root = app.ResizeApp();
+    widgets.runApp(root);
+    widgets.runApp(widgets.Container());
+
+    await tester.pumpAndSettle();
+
+    const widgets.Size expectedSize = widgets.Size(100, 100);
+    await app.ResizeApp.resize(expectedSize);
+  });
+}
diff --git a/dev/integration_tests/ui/lib/resize.dart b/dev/integration_tests/ui/lib/resize.dart
new file mode 100644
index 0000000..86359cf
--- /dev/null
+++ b/dev/integration_tests/ui/lib/resize.dart
@@ -0,0 +1,76 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+void main() async {
+  runApp(const ResizeApp());
+}
+
+class ResizeApp extends StatefulWidget {
+  const ResizeApp({super.key});
+
+  static const double resizeBy = 10.0;
+  static const Key heightLabel = Key('height label');
+  static const Key widthLabel = Key('width label');
+  static const Key extendedFab = Key('extended FAB');
+
+  static const MethodChannel platform =
+    MethodChannel('samples.flutter.dev/resize');
+
+  static Future<void> resize(Size size) async {
+    await ResizeApp.platform.invokeMethod<void>(
+      'resize',
+      <String, dynamic>{
+        'width': size.width,
+        'height': size.height,
+      }
+    );
+  }
+
+  @override
+  State<ResizeApp> createState() => _ResizeAppState();
+}
+
+class _ResizeAppState extends State<ResizeApp> {
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      home: Builder(
+        builder: (BuildContext context) {
+          final Size currentSize = MediaQuery.of(context).size;
+          return Scaffold(
+            floatingActionButton: FloatingActionButton.extended(
+              key: ResizeApp.extendedFab,
+              label: const Text('Resize'),
+              onPressed: () {
+                final Size nextSize = Size(
+                  currentSize.width + ResizeApp.resizeBy,
+                  currentSize.height + ResizeApp.resizeBy,
+                );
+                ResizeApp.resize(nextSize);
+              },
+            ),
+            body: Center(
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: <Widget>[
+                  Text(
+                    key: ResizeApp.widthLabel,
+                    'width: ${currentSize.width}'
+                  ),
+                  Text(
+                    key: ResizeApp.heightLabel,
+                    'height: ${currentSize.height}',
+                  ),
+                ],
+              ),
+            ),
+          );
+        }
+      ),
+    );
+  }
+}
diff --git a/dev/integration_tests/ui/macos/Runner/MainFlutterWindow.swift b/dev/integration_tests/ui/macos/Runner/MainFlutterWindow.swift
index a97a962..a52288a 100644
--- a/dev/integration_tests/ui/macos/Runner/MainFlutterWindow.swift
+++ b/dev/integration_tests/ui/macos/Runner/MainFlutterWindow.swift
@@ -5,6 +5,12 @@
 import Cocoa
 import FlutterMacOS
 
+extension NSWindow {
+  var titlebarHeight: CGFloat {
+    frame.height - contentRect(forFrameRect: frame).height
+  }
+}
+
 class MainFlutterWindow: NSWindow {
   override func awakeFromNib() {
     let flutterViewController = FlutterViewController.init()
@@ -12,8 +18,35 @@
     self.contentViewController = flutterViewController
     self.setFrame(windowFrame, display: true)
 
+    RegisterMethodChannel(registry: flutterViewController)
     RegisterGeneratedPlugins(registry: flutterViewController)
 
     super.awakeFromNib()
   }
+
+    func RegisterMethodChannel(registry: FlutterPluginRegistry) {
+    let registrar = registry.registrar(forPlugin: "resize")
+    let channel = FlutterMethodChannel(name: "samples.flutter.dev/resize",
+                                       binaryMessenger: registrar.messenger)
+    channel.setMethodCallHandler({ (call, result) in
+      if call.method == "resize" {
+        if let args = call.arguments as? Dictionary<String, Any>,
+          let width = args["width"] as? Double,
+          var height = args["height"] as? Double {
+          height += self.titlebarHeight
+          let currentFrame: NSRect = self.frame
+          let nextFrame: NSRect = NSMakeRect(
+            currentFrame.minX - (width - currentFrame.width) / 2,
+            currentFrame.minY - (height - currentFrame.height) / 2,
+            width,
+            height
+          )
+          self.setFrame(nextFrame, display: true, animate: false)
+          result(true)
+        } else {
+          result(FlutterError.init(code: "bad args", message: nil, details: nil))
+        }
+      }
+    })
+  }
 }