| // Copyright 2019 The Fuchsia 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 <fuchsia/sys/cpp/fidl.h> | |
| #include <fuchsia/ui/app/cpp/fidl.h> | |
| #include <fuchsia/ui/policy/cpp/fidl.h> | |
| #include <fuchsia/ui/scenic/cpp/fidl.h> | |
| #include <fuchsia/ui/views/cpp/fidl.h> | |
| #include <lib/fdio/spawn.h> | |
| #include <lib/fit/function.h> | |
| #include <lib/gtest/real_loop_fixture.h> | |
| #include <lib/sys/cpp/component_context.h> | |
| #include <lib/sys/cpp/service_directory.h> | |
| #include <lib/syslog/cpp/macros.h> | |
| #include <lib/ui/scenic/cpp/view_token_pair.h> | |
| #include <lib/zx/clock.h> | |
| #include <lib/zx/time.h> | |
| #include <zircon/status.h> | |
| #include <iomanip> | |
| #include <map> | |
| #include <string> | |
| #include <vector> | |
| #include <gtest/gtest.h> | |
| #include "src/chromium/web_runner_tests/mock_get.h" | |
| #include "src/chromium/web_runner_tests/test_server.h" | |
| #include "src/chromium/web_runner_tests/web_context.h" | |
| #include "src/lib/fsl/vmo/vector.h" | |
| #include "src/lib/fxl/strings/string_printf.h" | |
| #include "src/lib/ui/base_view/embedded_view_utils.h" | |
| #include "src/ui/testing/views/embedder_view.h" | |
| namespace { | |
| // Max time to wait in failure cases before bailing. | |
| constexpr zx::duration kTimeout = zx::sec(15); | |
| constexpr uint32_t kBlankColor = 0x00000000; | |
| constexpr zx::duration kTestTimeout = zx::sec(60); | |
| std::map<uint32_t, size_t> Histogram(const fuchsia::ui::scenic::ScreenshotData& screenshot) { | |
| EXPECT_GT(screenshot.info.width, 0u); | |
| EXPECT_GT(screenshot.info.height, 0u); | |
| std::vector<uint8_t> data; | |
| EXPECT_TRUE(fsl::VectorFromVmo(screenshot.data, &data)) << "Failed to read screenshot"; | |
| std::map<uint32_t, size_t> histogram; | |
| const uint32_t* bitmap = reinterpret_cast<const uint32_t*>(data.data()); | |
| const size_t size = screenshot.info.width * screenshot.info.height; | |
| EXPECT_EQ(size * sizeof(uint32_t), data.size()); | |
| for (size_t i = 0; i < size; ++i) { | |
| ++histogram[bitmap[i]]; | |
| } | |
| return histogram; | |
| } | |
| // Invokes the input tool for input injection. | |
| // See src/ui/tools/input/README.md or `input --help` for usage details. | |
| // Commands used here: | |
| // * tap <x> <y> (scaled out of 1000) | |
| // TODO(fxbug.dev/24462): Expose as a FIDL service. | |
| void Input(std::vector<const char*> args) { | |
| // start with proc name, end with nullptr | |
| args.insert(args.begin(), "input"); | |
| args.push_back(nullptr); | |
| zx_handle_t proc; | |
| zx_status_t status = | |
| fdio_spawn(ZX_HANDLE_INVALID, FDIO_SPAWN_CLONE_ALL, "/bin/input", args.data(), &proc); | |
| FX_CHECK(status == ZX_OK) << "fdio_spawn: " << zx_status_get_string(status); | |
| status = zx_object_wait_one(proc, ZX_PROCESS_TERMINATED, | |
| (zx::clock::get_monotonic() + kTimeout).get(), nullptr); | |
| FX_CHECK(status == ZX_OK) << "zx_object_wait_one: " << zx_status_get_string(status); | |
| zx_info_process_t info; | |
| status = zx_object_get_info(proc, ZX_INFO_PROCESS, &info, sizeof(info), nullptr, nullptr); | |
| FX_CHECK(status == ZX_OK) << "zx_object_get_info: " << zx_status_get_string(status); | |
| FX_CHECK(info.return_code == 0) << info.return_code; | |
| } | |
| // Base fixture for pixel tests, containing Scenic and presentation setup, and | |
| // screenshot utilities. | |
| class PixelTest : public gtest::RealLoopFixture { | |
| protected: | |
| PixelTest() : context_(sys::ComponentContext::CreateAndServeOutgoingDirectory()) { | |
| scenic_ = context_->svc()->Connect<fuchsia::ui::scenic::Scenic>(); | |
| scenic_.set_error_handler([](zx_status_t status) { | |
| FAIL() << "Lost connection to Scenic: " << zx_status_get_string(status); | |
| }); | |
| // TODO(fxbug.dev/40933) | |
| // | |
| // These tests can flake when a screenshot captures a frame from the previous test, which can | |
| // advance the test logic early. This is a temporary solution that waits for a blank on setup. | |
| // Better solutions include hermetic Scenic (using src/ui/scenic/lib/gfx/tests/pixel_test.h) or | |
| // refactoring to use view state events (probably the best solution; greatly improves | |
| // determinism at the expense of added harness complexity). | |
| // | |
| // WebRunnerPixelTest.Static below is factored to use view state events, but the others are not. | |
| FX_CHECK(WaitForBlank()); | |
| } | |
| sys::ComponentContext* context() { return context_.get(); } | |
| fuchsia::ui::scenic::Scenic* scenic() { return scenic_.get(); } | |
| // Gets a view token for presentation by |RootPresenter|. See also | |
| // garnet/examples/ui/hello_base_view | |
| fuchsia::ui::views::ViewToken CreatePresentationViewToken() { | |
| auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); | |
| auto presenter = context_->svc()->Connect<fuchsia::ui::policy::Presenter>(); | |
| presenter.set_error_handler( | |
| [](zx_status_t status) { FAIL() << "presenter: " << zx_status_get_string(status); }); | |
| presenter->PresentOrReplaceView(std::move(view_holder_token), nullptr); | |
| return std::move(view_token); | |
| } | |
| bool ScreenshotUntil(fit::function<bool(fuchsia::ui::scenic::ScreenshotData, bool)> condition, | |
| zx::duration timeout = kTimeout) { | |
| zx::time start = zx::clock::get_monotonic(); | |
| while (zx::clock::get_monotonic() - start <= timeout) { | |
| fuchsia::ui::scenic::ScreenshotData screenshot; | |
| bool ok = false; | |
| scenic_->TakeScreenshot( | |
| [this, &screenshot, &ok](fuchsia::ui::scenic::ScreenshotData screenshot_in, bool status) { | |
| ok = status; | |
| screenshot = std::move(screenshot_in); | |
| QuitLoop(); | |
| }); | |
| if (!RunLoopWithTimeout(timeout) && condition(std::move(screenshot), ok)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Blank can manifest as invalid screenshots or blackness. | |
| // TODO(fxbug.dev/40933): remove | |
| bool WaitForBlank() { | |
| return ScreenshotUntil([](fuchsia::ui::scenic::ScreenshotData screenshot, bool status) { | |
| return !status || Histogram(screenshot)[kBlankColor] > 0u; | |
| }); | |
| } | |
| void ExpectSolidColor(uint32_t argb) { | |
| std::map<uint32_t, size_t> histogram; | |
| FX_LOGS(INFO) << "Looking for color " << std::hex << argb << "."; | |
| EXPECT_TRUE(ScreenshotUntil( | |
| [argb, &histogram](fuchsia::ui::scenic::ScreenshotData screenshot, bool status) { | |
| if (!status) | |
| return false; | |
| histogram = Histogram(screenshot); | |
| FX_LOGS(INFO) << "Looking for color " << std::hex << argb << ": found " << std::dec | |
| << histogram[argb] << " px."; | |
| return histogram[argb] > 0u; | |
| })); | |
| histogram.erase(argb); | |
| EXPECT_EQ((std::map<uint32_t, size_t>){}, histogram) << "Unexpected colors"; | |
| } | |
| void ExpectPrimaryColor(uint32_t color) { | |
| std::multimap<size_t, uint32_t> inverse_histogram; | |
| FX_LOGS(INFO) << "Looking for color " << std::hex << color; | |
| EXPECT_TRUE(ScreenshotUntil([color, &inverse_histogram]( | |
| fuchsia::ui::scenic::ScreenshotData screenshot, bool status) { | |
| if (!status) | |
| return false; | |
| std::map<uint32_t, size_t> histogram = Histogram(screenshot); | |
| FX_LOGS(INFO) << histogram[color] << " px"; | |
| inverse_histogram.clear(); | |
| for (const auto entry : histogram) { | |
| inverse_histogram.emplace(entry.second, entry.first); | |
| } | |
| return (--inverse_histogram.end())->second == color; | |
| })) << "Primary color: " | |
| << std::hex << (--inverse_histogram.end())->second; | |
| } | |
| private: | |
| std::unique_ptr<sys::ComponentContext> context_; | |
| fuchsia::sys::ComponentControllerPtr runner_ctrl_; | |
| fuchsia::ui::scenic::ScenicPtr scenic_; | |
| }; | |
| using WebRunnerPixelTest = PixelTest; | |
| // Loads a static page with a solid color via the component framework and | |
| // verifies that the color is the only color onscreen. | |
| // TODO(fxbug.dev/72369): Investigate why this fails on femu by showing artifacts and calling | |
| // SetScale(0, 0) before reenabling. | |
| TEST_F(WebRunnerPixelTest, DISABLED_Static) { | |
| static constexpr uint32_t kTargetColor = 0xffff00ff; | |
| web_runner_tests::TestServer server; | |
| FX_CHECK(server.FindAndBindPort()); | |
| // Chromium and the Fuchsia network package loader both send us requests. This | |
| // may go away after fxbug.dev/17641; although the race seems to be in Modular, the | |
| // fix may remove the unnecessary net request in component framework. | |
| auto serve = server.ServeAsync([&server] { | |
| while (server.Accept()) { | |
| web_runner_tests::MockHttpGetResponse(&server, "static.html"); | |
| } | |
| }); | |
| fuchsia::sys::ComponentControllerPtr controller; | |
| fuchsia::sys::LauncherPtr launcher; | |
| context()->svc()->Connect(launcher.NewRequest()); | |
| auto info = scenic::LaunchComponentAndCreateView( | |
| launcher, fxl::StringPrintf("http://localhost:%d/static.html", server.port()), {}); | |
| info.controller.events().OnTerminated = [](int64_t code, fuchsia::sys::TerminationReason reason) { | |
| FAIL(); | |
| }; | |
| // Present the view. | |
| scenic::EmbedderView embedder_view({ | |
| .session_and_listener_request = scenic::CreateScenicSessionPtrAndListenerRequest(scenic()), | |
| .view_token = CreatePresentationViewToken(), | |
| }); | |
| embedder_view.EmbedView( | |
| std::move(info), | |
| /*view_state_changed_callback=*/[this](fuchsia::ui::gfx::ViewState view_state) { | |
| EXPECT_TRUE(view_state.is_rendering); | |
| QuitLoop(); | |
| }); | |
| // Wait to get a signal that the view is being rendered. | |
| EXPECT_FALSE(RunLoopWithTimeout(kTestTimeout)) | |
| << "Timed out waiting for a ViewStateChanged event."; | |
| ExpectSolidColor(kTargetColor); | |
| } | |
| // This fixture uses fuchsia.web FIDL services to interact with the WebEngine. | |
| class WebPixelTest : public PixelTest { | |
| protected: | |
| WebPixelTest() | |
| : web_context_(context(), fuchsia::web::ContextFeatureFlags::VULKAN | | |
| fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER), | |
| embedder_view_({ | |
| .session_and_listener_request = | |
| scenic::CreateScenicSessionPtrAndListenerRequest(scenic()), | |
| .view_token = CreatePresentationViewToken(), | |
| }) { | |
| auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); | |
| web_context_.web_frame()->CreateView(std::move(view_token)); | |
| scenic::EmbeddedViewInfo embedded_view_info; | |
| embedded_view_info.view_holder_token = std::move(view_holder_token); | |
| embedder_view_.EmbedView( | |
| std::move(embedded_view_info), | |
| /*view_state_changed_callback=*/[this](fuchsia::ui::gfx::ViewState view_state) { | |
| EXPECT_TRUE(view_state.is_rendering); | |
| QuitLoop(); | |
| }); | |
| // Wait to get a signal that the view is being rendered. | |
| EXPECT_FALSE(RunLoopWithTimeout(kTestTimeout)) | |
| << "Timed out waiting for a ViewStateChanged event."; | |
| } | |
| WebContext* web_context() { return &web_context_; } | |
| private: | |
| WebContext web_context_; | |
| scenic::EmbedderView embedder_view_; | |
| }; | |
| // Loads a static page with a solid color via fuchsia.web interfaces and | |
| // verifies that the color is the only color onscreen. | |
| // TODO(fxbug.dev/72369): Investigate why this fails on femu and reenable. | |
| TEST_F(WebPixelTest, DISABLED_Static) { | |
| static constexpr uint32_t kTargetColor = 0xffff00ff; | |
| web_runner_tests::TestServer server; | |
| FX_CHECK(server.FindAndBindPort()); | |
| auto serve = server.ServeAsync([&server] { | |
| FX_LOGS(INFO) << "Waiting for HTTP request from Chromium"; | |
| ASSERT_TRUE(server.Accept()) << "Did not receive HTTP request from Chromium"; | |
| web_runner_tests::MockHttpGetResponse(&server, "static.html"); | |
| }); | |
| web_context()->Navigate(fxl::StringPrintf("http://localhost:%d/static.html", server.port())); | |
| ExpectSolidColor(kTargetColor); | |
| } | |
| // Loads a dynamic page that starts with a Fuchsia background. This test verifies the initial color, | |
| // taps on the view, and verifies that the color changed. | |
| TEST_F(WebPixelTest, Dynamic) { | |
| static constexpr uint32_t kBeforeColor = 0xffff00ff; | |
| static constexpr uint32_t kAfterColor = 0xff40e0d0; | |
| web_runner_tests::TestServer server; | |
| FX_CHECK(server.FindAndBindPort()); | |
| auto serve = server.ServeAsync([&server] { | |
| ASSERT_TRUE(server.Accept()); | |
| web_runner_tests::MockHttpGetResponse(&server, "dynamic.html"); | |
| }); | |
| web_context()->Navigate(fxl::StringPrintf("http://localhost:%d/dynamic.html", server.port())); | |
| ExpectPrimaryColor(kBeforeColor); | |
| Input({"tap", "500", "125"}); // centered in top quarter of screen | |
| ExpectPrimaryColor(kAfterColor); | |
| } | |
| // Loads a static page with a video. | |
| TEST_F(WebPixelTest, Video) { | |
| web_runner_tests::TestServer server; | |
| FX_CHECK(server.FindAndBindPort()); | |
| auto serve = server.ServeAsync([&server] { | |
| FX_LOGS(INFO) << "Waiting for HTTP request from Chromium"; | |
| ASSERT_TRUE(server.Accept()) << "Did not receive HTTP request from Chromium"; | |
| web_runner_tests::MockHttpGetResponse(&server, "video.html"); | |
| web_runner_tests::MockHttpGetResponse(&server, "blackwhite_yuv420p.webm"); | |
| }); | |
| web_context()->Navigate(fxl::StringPrintf("http://localhost:%d/video.html", server.port())); | |
| static constexpr uint32_t kBlackColor = 0xff010001; | |
| static constexpr uint32_t kWhiteColor = 0xfffffeff; | |
| static constexpr uint32_t kVideoBackgroundColor = 0xffff00ff; | |
| std::map<uint32_t, size_t> histogram; | |
| EXPECT_TRUE( | |
| ScreenshotUntil([&histogram](fuchsia::ui::scenic::ScreenshotData screenshot, bool status) { | |
| if (!status) | |
| return false; | |
| histogram = Histogram(screenshot); | |
| return histogram[kBlackColor] > 0u; | |
| })); | |
| // We expect 240x240 <video> with black and white quadrants with grey on top. <video> background | |
| // color should not be visible at all. | |
| EXPECT_GT(histogram[kBlackColor], 10000u) << "Unexpected colors"; | |
| EXPECT_GT(histogram[kWhiteColor], 10000u) << "Unexpected colors"; | |
| EXPECT_EQ(histogram[kVideoBackgroundColor], 0u) << "Unexpected colors"; | |
| } | |
| } // namespace |