blob: 6d0befe60c62809344852c725753a4315d6b5005 [file] [log] [blame]
// 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));
};
// Process the next win32 message if there is one. This can be used to
// pump the Windows platform thread task runner.
void PumpMessage() {
::MSG msg;
if (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
} // 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);
builder.SetDartEntrypoint("signalViewIds");
EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);
std::string view_ids;
fml::AutoResetWaitableEvent latch;
context.AddNativeFunction(
"SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
auto handle = Dart_GetNativeArgument(args, 0);
ASSERT_FALSE(Dart_IsError(handle));
view_ids = tonic::DartConverter<std::string>::FromDart(handle);
latch.Signal();
}));
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);
// Verify a headless app has the implicit view.
latch.Wait();
EXPECT_EQ(view_ids, "View IDs: [0]");
}
// Verify that the engine can return to headless mode.
TEST_F(WindowsTest, EngineCanTransitionToHeadless) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);
// Create and then destroy a view controller that does not own its engine.
// This causes the engine to transition back to headless mode.
{
FlutterDesktopViewControllerProperties properties = {};
ViewControllerPtr controller{
FlutterDesktopEngineCreateViewController(engine.get(), &properties)};
ASSERT_NE(controller, nullptr);
}
// The engine is back in headless mode now.
ASSERT_NE(engine, nullptr);
auto engine_ptr = reinterpret_cast<FlutterWindowsEngine*>(engine.get());
ASSERT_TRUE(engine_ptr->running());
}
// 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;
bool done = false;
};
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->done = true;
captures->frame_drawn_latch.Signal();
},
&captures);
// Pump messages for the Windows platform task runner.
while (!captures.done) {
PumpMessage();
}
});
captures.frame_drawn_latch.Wait();
}
// Verify the embedder ignores presents to the implicit view when there is no
// implicit view.
TEST_F(WindowsTest, PresentHeadless) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetDartEntrypoint("renderImplicitView");
EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);
bool done = false;
FlutterDesktopEngineSetNextFrameCallback(
engine.get(),
[](void* user_data) {
// This executes on the platform thread.
auto done = reinterpret_cast<std::atomic<bool>*>(user_data);
*done = true;
},
&done);
// This app is in headless mode, however, the engine assumes the implicit
// view always exists. Send window metrics for the implicit view, causing
// the engine to present to the implicit view. The embedder must not crash.
auto engine_ptr = reinterpret_cast<FlutterWindowsEngine*>(engine.get());
FlutterWindowMetricsEvent metrics = {};
metrics.struct_size = sizeof(FlutterWindowMetricsEvent);
metrics.width = 100;
metrics.height = 100;
metrics.pixel_ratio = 1.0;
metrics.view_id = kImplicitViewId;
engine_ptr->SendWindowMetricsEvent(metrics);
// Pump messages for the Windows platform task runner.
while (!done) {
PumpMessage();
}
}
// 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)));
}
// Implicit view has the implicit view ID.
TEST_F(WindowsTest, PluginRegistrarGetImplicitView) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);
FlutterDesktopEngineRef engine =
FlutterDesktopViewControllerGetEngine(controller.get());
FlutterDesktopPluginRegistrarRef registrar =
FlutterDesktopEngineGetPluginRegistrar(engine, "foo_bar");
FlutterDesktopViewRef implicit_view =
FlutterDesktopPluginRegistrarGetView(registrar);
ASSERT_NE(implicit_view, nullptr);
}
TEST_F(WindowsTest, PluginRegistrarGetView) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);
FlutterDesktopEngineRef engine =
FlutterDesktopViewControllerGetEngine(controller.get());
FlutterDesktopPluginRegistrarRef registrar =
FlutterDesktopEngineGetPluginRegistrar(engine, "foo_bar");
FlutterDesktopViewId view_id =
FlutterDesktopViewControllerGetViewId(controller.get());
FlutterDesktopViewRef view =
FlutterDesktopPluginRegistrarGetViewById(registrar, view_id);
FlutterDesktopViewRef view_123 = FlutterDesktopPluginRegistrarGetViewById(
registrar, static_cast<FlutterDesktopViewId>(123));
ASSERT_NE(view, nullptr);
ASSERT_EQ(view_123, nullptr);
}
TEST_F(WindowsTest, PluginRegistrarGetViewHeadless) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);
FlutterDesktopPluginRegistrarRef registrar =
FlutterDesktopEngineGetPluginRegistrar(engine.get(), "foo_bar");
FlutterDesktopViewRef implicit_view =
FlutterDesktopPluginRegistrarGetView(registrar);
FlutterDesktopViewRef view_123 = FlutterDesktopPluginRegistrarGetViewById(
registrar, static_cast<FlutterDesktopViewId>(123));
ASSERT_EQ(implicit_view, nullptr);
ASSERT_EQ(view_123, nullptr);
}
// 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);
}
TEST_F(WindowsTest, GetKeyboardStateHeadless) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetDartEntrypoint("sendGetKeyboardState");
std::atomic<bool> done = false;
context.AddNativeFunction(
"SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
auto handle = Dart_GetNativeArgument(args, 0);
ASSERT_FALSE(Dart_IsError(handle));
auto value = tonic::DartConverter<std::string>::FromDart(handle);
EXPECT_EQ(value, "Success");
done = true;
}));
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);
// Pump messages for the Windows platform task runner.
::MSG msg;
while (!done) {
PumpMessage();
}
}
// Verify the embedder can add and remove views.
TEST_F(WindowsTest, AddRemoveView) {
std::mutex mutex;
std::string view_ids;
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetDartEntrypoint("onMetricsChangedSignalViewIds");
fml::AutoResetWaitableEvent ready_latch;
context.AddNativeFunction(
"Signal", CREATE_NATIVE_ENTRY(
[&](Dart_NativeArguments args) { ready_latch.Signal(); }));
context.AddNativeFunction(
"SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
auto handle = Dart_GetNativeArgument(args, 0);
ASSERT_FALSE(Dart_IsError(handle));
std::scoped_lock lock{mutex};
view_ids = tonic::DartConverter<std::string>::FromDart(handle);
}));
// Create the implicit view.
ViewControllerPtr first_controller{builder.Run()};
ASSERT_NE(first_controller, nullptr);
ready_latch.Wait();
// Create a second view.
FlutterDesktopEngineRef engine =
FlutterDesktopViewControllerGetEngine(first_controller.get());
FlutterDesktopViewControllerProperties properties = {};
properties.width = 100;
properties.height = 100;
ViewControllerPtr second_controller{
FlutterDesktopEngineCreateViewController(engine, &properties)};
ASSERT_NE(second_controller, nullptr);
// Pump messages for the Windows platform task runner until the view is added.
while (true) {
PumpMessage();
std::scoped_lock lock{mutex};
if (view_ids == "View IDs: [0, 1]") {
break;
}
}
// Delete the second view and pump messages for the Windows platform task
// runner until the view is removed.
second_controller.reset();
while (true) {
PumpMessage();
std::scoped_lock lock{mutex};
if (view_ids == "View IDs: [0]") {
break;
}
}
}
} // namespace testing
} // namespace flutter