| // Copyright 2013 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. |
| |
| #include "flutter/shell/platform/windows/public/flutter_windows.h" |
| |
| #include <dxgi.h> |
| #include <wrl/client.h> |
| #include <thread> |
| |
| #include "flutter/fml/synchronization/count_down_latch.h" |
| #include "flutter/fml/synchronization/waitable_event.h" |
| #include "flutter/shell/platform/common/app_lifecycle_state.h" |
| #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" |
| #include "flutter/shell/platform/windows/egl/manager.h" |
| #include "flutter/shell/platform/windows/testing/engine_modifier.h" |
| #include "flutter/shell/platform/windows/testing/windows_test.h" |
| #include "flutter/shell/platform/windows/testing/windows_test_config_builder.h" |
| #include "flutter/shell/platform/windows/testing/windows_test_context.h" |
| #include "flutter/shell/platform/windows/windows_lifecycle_manager.h" |
| #include "flutter/testing/stream_capture.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "third_party/tonic/converter/dart_converter.h" |
| |
| namespace flutter { |
| namespace testing { |
| |
| namespace { |
| |
| // An EGL manager that initializes EGL but fails to create surfaces. |
| class HalfBrokenEGLManager : public egl::Manager { |
| public: |
| HalfBrokenEGLManager() : egl::Manager(/*enable_impeller = */ false) {} |
| |
| std::unique_ptr<egl::WindowSurface> |
| CreateWindowSurface(HWND hwnd, size_t width, size_t height) override { |
| return nullptr; |
| } |
| }; |
| |
| class MockWindowsLifecycleManager : public WindowsLifecycleManager { |
| public: |
| MockWindowsLifecycleManager(FlutterWindowsEngine* engine) |
| : WindowsLifecycleManager(engine) {} |
| |
| MOCK_METHOD(void, SetLifecycleState, (AppLifecycleState), (override)); |
| }; |
| |
| } // namespace |
| |
| // Verify that we can fetch a texture registrar. |
| // Prevent regression: https://github.com/flutter/flutter/issues/86617 |
| TEST(WindowsNoFixtureTest, GetTextureRegistrar) { |
| FlutterDesktopEngineProperties properties = {}; |
| properties.assets_path = L""; |
| properties.icu_data_path = L"icudtl.dat"; |
| auto engine = FlutterDesktopEngineCreate(&properties); |
| ASSERT_NE(engine, nullptr); |
| auto texture_registrar = FlutterDesktopEngineGetTextureRegistrar(engine); |
| EXPECT_NE(texture_registrar, nullptr); |
| FlutterDesktopEngineDestroy(engine); |
| } |
| |
| // Verify we can successfully launch main(). |
| TEST_F(WindowsTest, LaunchMain) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| } |
| |
| // Verify there is no unexpected output from launching main. |
| TEST_F(WindowsTest, LaunchMainHasNoOutput) { |
| // Replace stdout & stderr stream buffers with our own. |
| StreamCapture stdout_capture(&std::cout); |
| StreamCapture stderr_capture(&std::cerr); |
| |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| |
| stdout_capture.Stop(); |
| stderr_capture.Stop(); |
| |
| // Verify stdout & stderr have no output. |
| EXPECT_TRUE(stdout_capture.GetOutput().empty()); |
| EXPECT_TRUE(stderr_capture.GetOutput().empty()); |
| } |
| |
| // Verify we can successfully launch a custom entry point. |
| TEST_F(WindowsTest, LaunchCustomEntrypoint) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("customEntrypoint"); |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| } |
| |
| // Verify that engine launches with the custom entrypoint specified in the |
| // FlutterDesktopEngineRun parameter when no entrypoint is specified in |
| // FlutterDesktopEngineProperties.dart_entrypoint. |
| // |
| // TODO(cbracken): https://github.com/flutter/flutter/issues/109285 |
| TEST_F(WindowsTest, LaunchCustomEntrypointInEngineRunInvocation) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| EnginePtr engine{builder.InitializeEngine()}; |
| ASSERT_NE(engine, nullptr); |
| |
| ASSERT_TRUE(FlutterDesktopEngineRun(engine.get(), "customEntrypoint")); |
| } |
| |
| // Verify that the engine can launch in headless mode. |
| TEST_F(WindowsTest, LaunchHeadlessEngine) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| EnginePtr engine{builder.RunHeadless()}; |
| ASSERT_NE(engine, nullptr); |
| } |
| |
| // Verify that accessibility features are initialized when a view is created. |
| TEST_F(WindowsTest, LaunchRefreshesAccessibility) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| EnginePtr engine{builder.InitializeEngine()}; |
| EngineModifier modifier{ |
| reinterpret_cast<FlutterWindowsEngine*>(engine.get())}; |
| |
| auto called = false; |
| modifier.embedder_api().UpdateAccessibilityFeatures = MOCK_ENGINE_PROC( |
| UpdateAccessibilityFeatures, ([&called](auto engine, auto flags) { |
| called = true; |
| return kSuccess; |
| })); |
| |
| ViewControllerPtr controller{ |
| FlutterDesktopViewControllerCreate(0, 0, engine.release())}; |
| |
| ASSERT_TRUE(called); |
| } |
| |
| // Verify that engine fails to launch when a conflicting entrypoint in |
| // FlutterDesktopEngineProperties.dart_entrypoint and the |
| // FlutterDesktopEngineRun parameter. |
| // |
| // TODO(cbracken): https://github.com/flutter/flutter/issues/109285 |
| TEST_F(WindowsTest, LaunchConflictingCustomEntrypoints) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("customEntrypoint"); |
| EnginePtr engine{builder.InitializeEngine()}; |
| ASSERT_NE(engine, nullptr); |
| |
| ASSERT_FALSE(FlutterDesktopEngineRun(engine.get(), "conflictingEntrypoint")); |
| } |
| |
| // Verify that native functions can be registered and resolved. |
| TEST_F(WindowsTest, VerifyNativeFunction) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("verifyNativeFunction"); |
| |
| fml::AutoResetWaitableEvent latch; |
| auto native_entry = |
| CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); }); |
| context.AddNativeFunction("Signal", native_entry); |
| |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| |
| // Wait until signal has been called. |
| latch.Wait(); |
| } |
| |
| // Verify that native functions that pass parameters can be registered and |
| // resolved. |
| TEST_F(WindowsTest, VerifyNativeFunctionWithParameters) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("verifyNativeFunctionWithParameters"); |
| |
| bool bool_value = false; |
| fml::AutoResetWaitableEvent latch; |
| auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { |
| auto handle = Dart_GetNativeBooleanArgument(args, 0, &bool_value); |
| ASSERT_FALSE(Dart_IsError(handle)); |
| latch.Signal(); |
| }); |
| context.AddNativeFunction("SignalBoolValue", native_entry); |
| |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| |
| // Wait until signalBoolValue has been called. |
| latch.Wait(); |
| EXPECT_TRUE(bool_value); |
| } |
| |
| // Verify that Platform.executable returns the executable name. |
| TEST_F(WindowsTest, PlatformExecutable) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("readPlatformExecutable"); |
| |
| std::string executable_name; |
| fml::AutoResetWaitableEvent latch; |
| auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { |
| auto handle = Dart_GetNativeArgument(args, 0); |
| ASSERT_FALSE(Dart_IsError(handle)); |
| executable_name = tonic::DartConverter<std::string>::FromDart(handle); |
| latch.Signal(); |
| }); |
| context.AddNativeFunction("SignalStringValue", native_entry); |
| |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| |
| // Wait until signalStringValue has been called. |
| latch.Wait(); |
| EXPECT_EQ(executable_name, "flutter_windows_unittests.exe"); |
| } |
| |
| // Verify that native functions that return values can be registered and |
| // resolved. |
| TEST_F(WindowsTest, VerifyNativeFunctionWithReturn) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("verifyNativeFunctionWithReturn"); |
| |
| bool bool_value_to_return = true; |
| fml::CountDownLatch latch(2); |
| auto bool_return_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { |
| Dart_SetBooleanReturnValue(args, bool_value_to_return); |
| latch.CountDown(); |
| }); |
| context.AddNativeFunction("SignalBoolReturn", bool_return_entry); |
| |
| bool bool_value_passed = false; |
| auto bool_pass_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { |
| auto handle = Dart_GetNativeBooleanArgument(args, 0, &bool_value_passed); |
| ASSERT_FALSE(Dart_IsError(handle)); |
| latch.CountDown(); |
| }); |
| context.AddNativeFunction("SignalBoolValue", bool_pass_entry); |
| |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| |
| // Wait until signalBoolReturn and signalBoolValue have been called. |
| latch.Wait(); |
| EXPECT_TRUE(bool_value_passed); |
| } |
| |
| // Verify the next frame callback is executed. |
| TEST_F(WindowsTest, NextFrameCallback) { |
| struct Captures { |
| fml::AutoResetWaitableEvent frame_scheduled_latch; |
| fml::AutoResetWaitableEvent frame_drawn_latch; |
| std::thread::id thread_id; |
| }; |
| Captures captures; |
| |
| CreateNewThread("test_platform_thread")->PostTask([&]() { |
| captures.thread_id = std::this_thread::get_id(); |
| |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| builder.SetDartEntrypoint("drawHelloWorld"); |
| |
| auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { |
| ASSERT_FALSE(captures.frame_drawn_latch.IsSignaledForTest()); |
| captures.frame_scheduled_latch.Signal(); |
| }); |
| context.AddNativeFunction("NotifyFirstFrameScheduled", native_entry); |
| |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| |
| auto engine = FlutterDesktopViewControllerGetEngine(controller.get()); |
| |
| FlutterDesktopEngineSetNextFrameCallback( |
| engine, |
| [](void* user_data) { |
| auto captures = static_cast<Captures*>(user_data); |
| |
| ASSERT_TRUE(captures->frame_scheduled_latch.IsSignaledForTest()); |
| |
| // Callback should execute on platform thread. |
| ASSERT_EQ(std::this_thread::get_id(), captures->thread_id); |
| |
| // Signal the test passed and end the Windows message loop. |
| captures->frame_drawn_latch.Signal(); |
| ::PostQuitMessage(0); |
| }, |
| &captures); |
| |
| // Pump messages for the Windows platform task runner. |
| ::MSG msg; |
| while (::GetMessage(&msg, nullptr, 0, 0)) { |
| ::TranslateMessage(&msg); |
| ::DispatchMessage(&msg); |
| } |
| }); |
| |
| captures.frame_drawn_latch.Wait(); |
| } |
| |
| // Implicit view has the implicit view ID. |
| TEST_F(WindowsTest, GetViewId) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| FlutterDesktopViewId view_id = |
| FlutterDesktopViewControllerGetViewId(controller.get()); |
| |
| ASSERT_EQ(view_id, static_cast<FlutterDesktopViewId>(kImplicitViewId)); |
| } |
| |
| TEST_F(WindowsTest, GetGraphicsAdapter) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| ViewControllerPtr controller{builder.Run()}; |
| ASSERT_NE(controller, nullptr); |
| auto view = FlutterDesktopViewControllerGetView(controller.get()); |
| |
| Microsoft::WRL::ComPtr<IDXGIAdapter> dxgi_adapter; |
| dxgi_adapter = FlutterDesktopViewGetGraphicsAdapter(view); |
| ASSERT_NE(dxgi_adapter, nullptr); |
| DXGI_ADAPTER_DESC desc{}; |
| ASSERT_TRUE(SUCCEEDED(dxgi_adapter->GetDesc(&desc))); |
| } |
| |
| // Verify the app does not crash if EGL initializes successfully but |
| // the rendering surface cannot be created. |
| TEST_F(WindowsTest, SurfaceOptional) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| EnginePtr engine{builder.InitializeEngine()}; |
| EngineModifier modifier{ |
| reinterpret_cast<FlutterWindowsEngine*>(engine.get())}; |
| |
| auto egl_manager = std::make_unique<HalfBrokenEGLManager>(); |
| ASSERT_TRUE(egl_manager->IsValid()); |
| modifier.SetEGLManager(std::move(egl_manager)); |
| |
| ViewControllerPtr controller{ |
| FlutterDesktopViewControllerCreate(0, 0, engine.release())}; |
| |
| ASSERT_NE(controller, nullptr); |
| } |
| |
| // Verify the app produces the expected lifecycle events. |
| TEST_F(WindowsTest, Lifecycle) { |
| auto& context = GetContext(); |
| WindowsConfigBuilder builder(context); |
| EnginePtr engine{builder.InitializeEngine()}; |
| auto windows_engine = reinterpret_cast<FlutterWindowsEngine*>(engine.get()); |
| EngineModifier modifier{windows_engine}; |
| |
| auto lifecycle_manager = |
| std::make_unique<MockWindowsLifecycleManager>(windows_engine); |
| auto lifecycle_manager_ptr = lifecycle_manager.get(); |
| modifier.SetLifecycleManager(std::move(lifecycle_manager)); |
| |
| EXPECT_CALL(*lifecycle_manager_ptr, |
| SetLifecycleState(AppLifecycleState::kResumed)) |
| .WillOnce([lifecycle_manager_ptr](AppLifecycleState state) { |
| lifecycle_manager_ptr->WindowsLifecycleManager::SetLifecycleState( |
| state); |
| }); |
| |
| EXPECT_CALL(*lifecycle_manager_ptr, |
| SetLifecycleState(AppLifecycleState::kHidden)) |
| .WillOnce([lifecycle_manager_ptr](AppLifecycleState state) { |
| lifecycle_manager_ptr->WindowsLifecycleManager::SetLifecycleState( |
| state); |
| }); |
| |
| // Create a controller. This launches the engine and sets the app lifecycle |
| // to the "resumed" state. |
| ViewControllerPtr controller{ |
| FlutterDesktopViewControllerCreate(0, 0, engine.release())}; |
| |
| FlutterDesktopViewRef view = |
| FlutterDesktopViewControllerGetView(controller.get()); |
| ASSERT_NE(view, nullptr); |
| |
| HWND hwnd = FlutterDesktopViewGetHWND(view); |
| ASSERT_NE(hwnd, nullptr); |
| |
| // Give the window a non-zero size to show it. This does not change the app |
| // lifecycle directly. However, destroying the view will now result in a |
| // "hidden" app lifecycle event. |
| ::MoveWindow(hwnd, /* X */ 0, /* Y */ 0, /* nWidth*/ 100, /* nHeight*/ 100, |
| /* bRepaint*/ false); |
| } |
| |
| } // namespace testing |
| } // namespace flutter |