| // 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. |
| |
| // Included first as it collides with the X11 headers. |
| #include "gtest/gtest.h" |
| |
| #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" |
| #include "flutter/shell/platform/linux/fl_engine_private.h" |
| #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" |
| #include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h" |
| #include "flutter/shell/platform/linux/testing/fl_test.h" |
| |
| // MOCK_ENGINE_PROC is leaky by design |
| // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) |
| |
| // Checks sending window metrics events works. |
| TEST(FlEngineTest, WindowMetrics) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->SendWindowMetricsEvent = MOCK_ENGINE_PROC( |
| SendWindowMetricsEvent, |
| ([&called](auto engine, const FlutterWindowMetricsEvent* event) { |
| called = true; |
| EXPECT_EQ(event->width, static_cast<size_t>(3840)); |
| EXPECT_EQ(event->height, static_cast<size_t>(2160)); |
| EXPECT_EQ(event->pixel_ratio, 2.0); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| fl_engine_send_window_metrics_event(engine, 3840, 2160, 2.0); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| // Checks sending mouse pointer events works. |
| TEST(FlEngineTest, MousePointer) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->SendPointerEvent = MOCK_ENGINE_PROC( |
| SendPointerEvent, |
| ([&called](auto engine, const FlutterPointerEvent* events, |
| size_t events_count) { |
| called = true; |
| EXPECT_EQ(events_count, static_cast<size_t>(1)); |
| EXPECT_EQ(events[0].phase, kDown); |
| EXPECT_EQ(events[0].timestamp, static_cast<size_t>(1234567890)); |
| EXPECT_EQ(events[0].x, 800); |
| EXPECT_EQ(events[0].y, 600); |
| EXPECT_EQ(events[0].device, static_cast<int32_t>(0)); |
| EXPECT_EQ(events[0].signal_kind, kFlutterPointerSignalKindScroll); |
| EXPECT_EQ(events[0].scroll_delta_x, 1.2); |
| EXPECT_EQ(events[0].scroll_delta_y, -3.4); |
| EXPECT_EQ(events[0].device_kind, kFlutterPointerDeviceKindMouse); |
| EXPECT_EQ(events[0].buttons, kFlutterPointerButtonMouseSecondary); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| fl_engine_send_mouse_pointer_event(engine, kDown, 1234567890, 800, 600, 1.2, |
| -3.4, kFlutterPointerButtonMouseSecondary); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| // Checks sending pan/zoom events works. |
| TEST(FlEngineTest, PointerPanZoom) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->SendPointerEvent = MOCK_ENGINE_PROC( |
| SendPointerEvent, |
| ([&called](auto engine, const FlutterPointerEvent* events, |
| size_t events_count) { |
| called = true; |
| EXPECT_EQ(events_count, static_cast<size_t>(1)); |
| EXPECT_EQ(events[0].phase, kPanZoomUpdate); |
| EXPECT_EQ(events[0].timestamp, static_cast<size_t>(1234567890)); |
| EXPECT_EQ(events[0].x, 800); |
| EXPECT_EQ(events[0].y, 600); |
| EXPECT_EQ(events[0].device, static_cast<int32_t>(1)); |
| EXPECT_EQ(events[0].signal_kind, kFlutterPointerSignalKindNone); |
| EXPECT_EQ(events[0].pan_x, 1.5); |
| EXPECT_EQ(events[0].pan_y, 2.5); |
| EXPECT_EQ(events[0].scale, 3.5); |
| EXPECT_EQ(events[0].rotation, 4.5); |
| EXPECT_EQ(events[0].device_kind, kFlutterPointerDeviceKindTrackpad); |
| EXPECT_EQ(events[0].buttons, 0); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| fl_engine_send_pointer_pan_zoom_event(engine, 1234567890, 800, 600, |
| kPanZoomUpdate, 1.5, 2.5, 3.5, 4.5); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| // Checks dispatching a semantics action works. |
| TEST(FlEngineTest, DispatchSemanticsAction) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->DispatchSemanticsAction = MOCK_ENGINE_PROC( |
| DispatchSemanticsAction, |
| ([&called](auto engine, uint64_t id, FlutterSemanticsAction action, |
| const uint8_t* data, size_t data_length) { |
| EXPECT_EQ(id, static_cast<uint64_t>(42)); |
| EXPECT_EQ(action, kFlutterSemanticsActionTap); |
| EXPECT_EQ(data_length, static_cast<size_t>(4)); |
| EXPECT_EQ(data[0], 't'); |
| EXPECT_EQ(data[1], 'e'); |
| EXPECT_EQ(data[2], 's'); |
| EXPECT_EQ(data[3], 't'); |
| called = true; |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| g_autoptr(GBytes) data = g_bytes_new_static("test", 4); |
| fl_engine_dispatch_semantics_action(engine, 42, kFlutterSemanticsActionTap, |
| data); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| // Checks sending platform messages works. |
| TEST(FlEngineTest, PlatformMessage) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| FlutterEngineSendPlatformMessageFnPtr old_handler = |
| embedder_api->SendPlatformMessage; |
| embedder_api->SendPlatformMessage = MOCK_ENGINE_PROC( |
| SendPlatformMessage, |
| ([&called, old_handler](auto engine, |
| const FlutterPlatformMessage* message) { |
| if (strcmp(message->channel, "test") != 0) { |
| return old_handler(engine, message); |
| } |
| |
| called = true; |
| |
| EXPECT_EQ(message->message_size, static_cast<size_t>(4)); |
| EXPECT_EQ(message->message[0], 't'); |
| EXPECT_EQ(message->message[1], 'e'); |
| EXPECT_EQ(message->message[2], 's'); |
| EXPECT_EQ(message->message[3], 't'); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| g_autoptr(GBytes) message = g_bytes_new_static("test", 4); |
| fl_engine_send_platform_message(engine, "test", message, nullptr, nullptr, |
| nullptr); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| // Checks sending platform message responses works. |
| TEST(FlEngineTest, PlatformMessageResponse) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->SendPlatformMessageResponse = MOCK_ENGINE_PROC( |
| SendPlatformMessageResponse, |
| ([&called](auto engine, |
| const FlutterPlatformMessageResponseHandle* handle, |
| const uint8_t* data, size_t data_length) { |
| called = true; |
| |
| EXPECT_EQ( |
| handle, |
| reinterpret_cast<const FlutterPlatformMessageResponseHandle*>(42)); |
| EXPECT_EQ(data_length, static_cast<size_t>(4)); |
| EXPECT_EQ(data[0], 't'); |
| EXPECT_EQ(data[1], 'e'); |
| EXPECT_EQ(data[2], 's'); |
| EXPECT_EQ(data[3], 't'); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| g_autoptr(GBytes) response = g_bytes_new_static("test", 4); |
| EXPECT_TRUE(fl_engine_send_platform_message_response( |
| engine, reinterpret_cast<const FlutterPlatformMessageResponseHandle*>(42), |
| response, &error)); |
| EXPECT_EQ(error, nullptr); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| // Checks settings plugin sends settings on startup. |
| TEST(FlEngineTest, SettingsPlugin) { |
| g_autoptr(FlEngine) engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->SendPlatformMessage = MOCK_ENGINE_PROC( |
| SendPlatformMessage, |
| ([&called](auto engine, const FlutterPlatformMessage* message) { |
| called = true; |
| |
| EXPECT_STREQ(message->channel, "flutter/settings"); |
| |
| g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new(); |
| g_autoptr(GBytes) data = |
| g_bytes_new(message->message, message->message_size); |
| g_autoptr(GError) error = nullptr; |
| g_autoptr(FlValue) settings = fl_message_codec_decode_message( |
| FL_MESSAGE_CODEC(codec), data, &error); |
| EXPECT_NE(settings, nullptr); |
| EXPECT_EQ(error, nullptr); |
| |
| FlValue* text_scale_factor = |
| fl_value_lookup_string(settings, "textScaleFactor"); |
| EXPECT_NE(text_scale_factor, nullptr); |
| EXPECT_EQ(fl_value_get_type(text_scale_factor), FL_VALUE_TYPE_FLOAT); |
| |
| FlValue* always_use_24hr_format = |
| fl_value_lookup_string(settings, "alwaysUse24HourFormat"); |
| EXPECT_NE(always_use_24hr_format, nullptr); |
| EXPECT_EQ(fl_value_get_type(always_use_24hr_format), |
| FL_VALUE_TYPE_BOOL); |
| |
| FlValue* platform_brightness = |
| fl_value_lookup_string(settings, "platformBrightness"); |
| EXPECT_NE(platform_brightness, nullptr); |
| EXPECT_EQ(fl_value_get_type(platform_brightness), FL_VALUE_TYPE_STRING); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| void on_pre_engine_restart_cb(FlEngine* engine, gpointer user_data) { |
| int* count = reinterpret_cast<int*>(user_data); |
| *count += 1; |
| } |
| |
| void on_pre_engine_restart_destroy_notify(gpointer user_data) { |
| int* count = reinterpret_cast<int*>(user_data); |
| *count += 10; |
| } |
| |
| // Checks restarting the engine invokes the correct callback. |
| TEST(FlEngineTest, OnPreEngineRestart) { |
| FlEngine* engine = make_mock_engine(); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| OnPreEngineRestartCallback callback; |
| void* callback_user_data; |
| |
| bool called = false; |
| embedder_api->Initialize = MOCK_ENGINE_PROC( |
| Initialize, ([&callback, &callback_user_data, &called]( |
| size_t version, const FlutterRendererConfig* config, |
| const FlutterProjectArgs* args, void* user_data, |
| FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { |
| called = true; |
| callback = args->on_pre_engine_restart_callback; |
| callback_user_data = user_data; |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| |
| EXPECT_TRUE(called); |
| EXPECT_NE(callback, nullptr); |
| |
| // The following call has no effect but should not crash. |
| callback(callback_user_data); |
| |
| int count = 0; |
| |
| // Set handler so that: |
| // |
| // * When the engine restarts, count += 1; |
| // * When the engine is freed, count += 10. |
| fl_engine_set_on_pre_engine_restart_handler( |
| engine, on_pre_engine_restart_cb, &count, |
| on_pre_engine_restart_destroy_notify); |
| |
| callback(callback_user_data); |
| EXPECT_EQ(count, 1); |
| |
| // Disposal should call the destroy notify. |
| g_object_unref(engine); |
| EXPECT_EQ(count, 11); |
| } |
| |
| TEST(FlEngineTest, DartEntrypointArgs) { |
| g_autoptr(FlDartProject) project = fl_dart_project_new(); |
| |
| GPtrArray* args_array = g_ptr_array_new(); |
| g_ptr_array_add(args_array, const_cast<char*>("arg_one")); |
| g_ptr_array_add(args_array, const_cast<char*>("arg_two")); |
| g_ptr_array_add(args_array, const_cast<char*>("arg_three")); |
| g_ptr_array_add(args_array, nullptr); |
| gchar** args = reinterpret_cast<gchar**>(g_ptr_array_free(args_array, false)); |
| |
| fl_dart_project_set_dart_entrypoint_arguments(project, args); |
| |
| g_autoptr(FlEngine) engine = make_mock_engine_with_project(project); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->Initialize = MOCK_ENGINE_PROC( |
| Initialize, ([&called, &set_args = args]( |
| size_t version, const FlutterRendererConfig* config, |
| const FlutterProjectArgs* args, void* user_data, |
| FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { |
| called = true; |
| EXPECT_NE(set_args, args->dart_entrypoint_argv); |
| EXPECT_EQ(args->dart_entrypoint_argc, 3); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| |
| EXPECT_TRUE(called); |
| } |
| |
| TEST(FlEngineTest, Locales) { |
| gchar* initial_language = g_strdup(g_getenv("LANGUAGE")); |
| g_setenv("LANGUAGE", "de:en_US", TRUE); |
| g_autoptr(FlDartProject) project = fl_dart_project_new(); |
| |
| g_autoptr(FlEngine) engine = make_mock_engine_with_project(project); |
| FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); |
| |
| bool called = false; |
| embedder_api->UpdateLocales = MOCK_ENGINE_PROC( |
| UpdateLocales, ([&called](auto engine, const FlutterLocale** locales, |
| size_t locales_count) { |
| called = true; |
| |
| EXPECT_EQ(locales_count, static_cast<size_t>(4)); |
| |
| EXPECT_STREQ(locales[0]->language_code, "de"); |
| EXPECT_STREQ(locales[0]->country_code, nullptr); |
| EXPECT_STREQ(locales[0]->script_code, nullptr); |
| EXPECT_STREQ(locales[0]->variant_code, nullptr); |
| |
| EXPECT_STREQ(locales[1]->language_code, "en"); |
| EXPECT_STREQ(locales[1]->country_code, "US"); |
| EXPECT_STREQ(locales[1]->script_code, nullptr); |
| EXPECT_STREQ(locales[1]->variant_code, nullptr); |
| |
| EXPECT_STREQ(locales[2]->language_code, "en"); |
| EXPECT_STREQ(locales[2]->country_code, nullptr); |
| EXPECT_STREQ(locales[2]->script_code, nullptr); |
| EXPECT_STREQ(locales[2]->variant_code, nullptr); |
| |
| EXPECT_STREQ(locales[3]->language_code, "C"); |
| EXPECT_STREQ(locales[3]->country_code, nullptr); |
| EXPECT_STREQ(locales[3]->script_code, nullptr); |
| EXPECT_STREQ(locales[3]->variant_code, nullptr); |
| |
| return kSuccess; |
| })); |
| |
| g_autoptr(GError) error = nullptr; |
| EXPECT_TRUE(fl_engine_start(engine, &error)); |
| EXPECT_EQ(error, nullptr); |
| |
| EXPECT_TRUE(called); |
| |
| if (initial_language) { |
| g_setenv("LANGUAGE", initial_language, TRUE); |
| } else { |
| g_unsetenv("LANGUAGE"); |
| } |
| g_free(initial_language); |
| } |
| |
| // NOLINTEND(clang-analyzer-core.StackAddressEscape) |