| // 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_v2_t info; |
| status = zx_object_get_info(proc, ZX_INFO_PROCESS_V2, &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; |
| 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. |
| TEST_F(WebRunnerPixelTest, 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 |