| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <chromium/cast/cpp/fidl.h> |
| #include <fuchsia/camera3/cpp/fidl.h> |
| #include <fuchsia/legacymetrics/cpp/fidl.h> |
| #include <fuchsia/media/cpp/fidl.h> |
| #include <fuchsia/ui/views/cpp/fidl.h> |
| #include <fuchsia/web/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <lib/sys/cpp/component_context.h> |
| #include <lib/zx/eventpair.h> |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/base_paths.h" |
| #include "base/files/file_util.h" |
| #include "base/fuchsia/file_utils.h" |
| #include "base/fuchsia/fuchsia_logging.h" |
| #include "base/fuchsia/mem_buffer_util.h" |
| #include "base/fuchsia/scoped_service_binding.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_piece.h" |
| #include "base/test/bind.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/uuid.h" |
| #include "build/build_config.h" |
| #include "build/chromecast_buildflags.h" |
| #include "components/fuchsia_component_support/dynamic_component_host.h" |
| #include "fuchsia_web/common/string_util.h" |
| #include "fuchsia_web/common/test/fit_adapter.h" |
| #include "fuchsia_web/common/test/frame_for_test.h" |
| #include "fuchsia_web/common/test/frame_test_util.h" |
| #include "fuchsia_web/common/test/test_debug_listener.h" |
| #include "fuchsia_web/common/test/test_devtools_list_fetcher.h" |
| #include "fuchsia_web/common/test/test_navigation_listener.h" |
| #include "fuchsia_web/common/test/url_request_rewrite_test_util.h" |
| #include "fuchsia_web/runners/cast/cast_runner.h" |
| #include "fuchsia_web/runners/cast/cast_runner_switches.h" |
| #include "fuchsia_web/runners/cast/test/cast_runner_features.h" |
| #include "fuchsia_web/runners/cast/test/cast_runner_launcher.h" |
| #include "fuchsia_web/runners/cast/test/fake_api_bindings.h" |
| #include "net/test/embedded_test_server/default_handlers.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| constexpr char kTestAppId[] = "00000000"; |
| constexpr char kSecondTestAppId[] = "FFFFFFFF"; |
| |
| constexpr char kBlankAppUrl[] = "/defaultresponse"; |
| constexpr char kEchoHeaderPath[] = "/echoheader?Test"; |
| |
| chromium::cast::ApplicationConfig CreateAppConfigWithTestData( |
| base::StringPiece app_id, |
| GURL url) { |
| fuchsia::web::ContentDirectoryProvider provider; |
| provider.set_name("testdata"); |
| |
| base::FilePath pkg_path; |
| CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &pkg_path)); |
| |
| provider.set_directory(base::OpenDirectoryHandle( |
| pkg_path.AppendASCII("fuchsia_web/runners/cast/testdata"))); |
| std::vector<fuchsia::web::ContentDirectoryProvider> providers; |
| providers.emplace_back(std::move(provider)); |
| |
| auto app_config = FakeApplicationConfigManager::CreateConfig(app_id, url); |
| app_config.set_content_directories_for_isolated_application( |
| std::move(providers)); |
| return app_config; |
| } |
| |
| class FakeUrlRequestRewriteRulesProvider final |
| : public chromium::cast::UrlRequestRewriteRulesProvider { |
| public: |
| FakeUrlRequestRewriteRulesProvider() = default; |
| ~FakeUrlRequestRewriteRulesProvider() override = default; |
| |
| FakeUrlRequestRewriteRulesProvider( |
| const FakeUrlRequestRewriteRulesProvider&) = delete; |
| FakeUrlRequestRewriteRulesProvider& operator=( |
| const FakeUrlRequestRewriteRulesProvider&) = delete; |
| |
| private: |
| void GetUrlRequestRewriteRules( |
| GetUrlRequestRewriteRulesCallback callback) override { |
| // Only send the rules once. They do not expire |
| if (rules_sent_) { |
| return; |
| } |
| rules_sent_ = true; |
| |
| std::vector<fuchsia::web::UrlRequestRewrite> rewrites; |
| rewrites.push_back(CreateRewriteAddHeaders("Test", "TestHeaderValue")); |
| fuchsia::web::UrlRequestRewriteRule rule; |
| rule.set_rewrites(std::move(rewrites)); |
| std::vector<fuchsia::web::UrlRequestRewriteRule> rules; |
| rules.push_back(std::move(rule)); |
| callback(std::move(rules)); |
| } |
| |
| bool rules_sent_ = false; |
| }; |
| |
| class FakeApplicationContext final : public chromium::cast::ApplicationContext { |
| public: |
| FakeApplicationContext() = default; |
| ~FakeApplicationContext() override = default; |
| |
| FakeApplicationContext(const FakeApplicationContext&) = delete; |
| FakeApplicationContext& operator=(const FakeApplicationContext&) = delete; |
| |
| chromium::cast::ApplicationController* application_controller() { |
| if (!application_controller_) { |
| return nullptr; |
| } |
| |
| return application_controller_.get(); |
| } |
| |
| void WaitForSetApplicationController() { |
| if (application_controller_) { |
| return; |
| } |
| base::RunLoop loop; |
| on_set_application_controller_ = loop.QuitClosure(); |
| loop.Run(); |
| } |
| |
| absl::optional<int64_t> WaitForApplicationTerminated() { |
| if (application_exit_code_.has_value()) { |
| return application_exit_code_; |
| } |
| base::RunLoop loop; |
| on_application_terminated_ = loop.QuitClosure(); |
| loop.Run(); |
| return application_exit_code_; |
| } |
| |
| private: |
| // chromium::cast::ApplicationContext implementation. |
| void GetMediaSessionId(GetMediaSessionIdCallback callback) override { |
| callback(1); |
| } |
| void SetApplicationController( |
| fidl::InterfaceHandle<chromium::cast::ApplicationController> |
| application_controller) override { |
| application_controller_ = application_controller.Bind(); |
| if (on_set_application_controller_) { |
| std::move(on_set_application_controller_).Run(); |
| } |
| } |
| void OnApplicationExit(int64_t exit_code) override { |
| application_exit_code_ = exit_code; |
| if (on_application_terminated_) { |
| std::move(on_application_terminated_).Run(); |
| } |
| } |
| |
| chromium::cast::ApplicationControllerPtr application_controller_; |
| base::OnceClosure on_set_application_controller_; |
| |
| absl::optional<int64_t> application_exit_code_; |
| base::OnceClosure on_application_terminated_; |
| }; |
| |
| class TestCastComponent { |
| public: |
| // `test_realm_services` is used to connect to the test `Realm` exposed by |
| // the CastRunnerLauncher, that contains a collection capable of resolving |
| // and running Cast components. |
| explicit TestCastComponent(const sys::ServiceDirectory& test_realm_services) |
| : test_realm_services_(test_realm_services) { |
| // Instantiate the per-instance service directory and fakes by default, |
| // for tests to configure & add expectations to. |
| services_.emplace(); |
| } |
| |
| ~TestCastComponent() { ShutdownComponent(); } |
| |
| TestCastComponent(const TestCastComponent&) = delete; |
| TestCastComponent& operator=(const TestCastComponent&) = delete; |
| |
| void disable_offer_services() { offer_services_ = false; } |
| void offer_closed_services() { offer_closed_services_ = true; } |
| |
| // Attempts to start the Cast activity identified by `app_id`, and to inject |
| // script bindings to support querying JavaScript/DOM state via the |
| // the `ExecuteJavaScript()` API (see below). |
| // Note that this function will not return until the activity has actually |
| // launched. |
| void StartCastComponentWithQueryApi(base::StringPiece app_id = kTestAppId) { |
| auto component_url = base::StrCat({"cast:", app_id}); |
| InjectQueryApi(); |
| StartCastComponent(component_url); |
| WaitQueryApiConnected(); |
| } |
| |
| // Attempts to start the Cast activity identified by `app_id`. |
| // Note that activity launch is asynchronous, tests that need to wait until |
| // the activity has actually started (e.g. to interact with its |
| // `ApplicationController`, etc), should normally use the |
| // `StartCastComponentWithQueryApi()` call instead. |
| void StartCastComponent(base::StringPiece component_url) { |
| ASSERT_FALSE(component_) << "Component may only be started once"; |
| |
| fidl::InterfaceHandle<fuchsia::io::Directory> services; |
| if (offer_services_) { |
| if (offer_closed_services_) { |
| // Create the `services` channel, and immediately close the "request" |
| // end, to simulate a missing service directory. |
| std::ignore = services.NewRequest(); |
| } else { |
| // Create a `fuchsia.io.Directory` connected to the directory of fake |
| // services. |
| zx_status_t status = services_->services.Serve( |
| fuchsia::io::OpenFlags::RIGHT_READABLE | |
| fuchsia::io::OpenFlags::RIGHT_WRITABLE | |
| fuchsia::io::OpenFlags::DIRECTORY, |
| services.NewRequest().TakeChannel()); |
| ZX_CHECK(status == ZX_OK, status) << "Serve()"; |
| } |
| } |
| |
| // Create the new dynamic component in the test component collection for |
| // the `cast_runner`. The component is created with an Id chosen |
| // at random to uniquely identify it, and supplied with `services` as |
| // configured above. |
| component_.emplace( |
| test_realm_services_.Connect<fuchsia::component::Realm>(), |
| test::CastRunnerLauncher::kTestCollectionName, |
| base::Uuid::GenerateRandomV4().AsLowercaseString(), component_url, |
| base::BindOnce(&TestCastComponent::OnComponentTeardown, |
| base::Unretained(this)), |
| std::move(services)); |
| } |
| |
| // Executes |code| in the context of the test application and then returns |
| // the result serialized as string. If the code evaluates to a promise then |
| // execution is blocked until the promise is complete and the result of the |
| // promise is returned. |
| std::string ExecuteJavaScript(const std::string& code) { |
| CHECK(test_port_); |
| |
| fuchsia::web::WebMessage message; |
| message.set_data(base::MemBufferFromString(code, "test-msg")); |
| test_port_->PostMessage( |
| std::move(message), |
| [](fuchsia::web::MessagePort_PostMessage_Result result) { |
| EXPECT_TRUE(result.is_response()); |
| }); |
| |
| base::test::TestFuture<fuchsia::web::WebMessage> response; |
| test_port_->ReceiveMessage(CallbackToFitFunction(response.GetCallback())); |
| EXPECT_TRUE(response.Wait()); |
| |
| absl::optional<std::string> response_string = |
| base::StringFromMemBuffer(response.Get().data()); |
| EXPECT_TRUE(response_string.has_value()); |
| |
| return response_string.value_or(std::string()); |
| } |
| |
| std::string QueryAppUrl() { |
| return ExecuteJavaScript("window.location.href"); |
| } |
| |
| // Closes active connections to all services offered to the component, |
| // to simulate the controlling agent tearing-down unexpectedly. |
| void DisconnectServices() { services_.reset(); } |
| |
| // Destroys the component and runs until it is observed to have torn-down. |
| void ShutdownComponent() { |
| if (component_) { |
| component_->Destroy(); |
| WaitForComponentDestroyed(); |
| } |
| } |
| |
| // Runs until `component_` is observed to have torn-down. |
| // Note that this may return before connections to services used by |
| // the component have been observed to have closed. |
| // This should be used after triggering component teardown, e.g. via an |
| // explicit ComponentController.Kill() call, to wait for it to take effect. |
| void WaitForComponentDestroyed() { |
| ASSERT_TRUE(component_); |
| base::RunLoop state_loop; |
| base::AutoReset reset_callback(&on_component_destroyed_, |
| state_loop.QuitClosure()); |
| state_loop.Run(); |
| } |
| |
| // Disables treatment of `component_` teardown as a test failure. This is |
| // useful for test of teardown-time API behaviours. |
| void SetIgnoreComponentDestroyed() { |
| on_component_destroyed_ = base::DoNothing(); |
| } |
| |
| FakeApiBindingsImpl& api_bindings() { return services_->api_bindings; } |
| FakeApplicationContext& application_context() { |
| return services_->application_context; |
| } |
| |
| sys::ServiceDirectory& exposed_by_component() { |
| return component_->exposed(); |
| } |
| |
| private: |
| // Used to manage fake services offered to each Cast component individually. |
| // Most of the FIDL services used by Cast activities are ambient, provided by |
| // the CastRunner itself. The services provided here are only those offered |
| // directly to Cast activities by their owning agent. |
| struct FakeComponentServices { |
| FakeComponentServices() |
| : api_bindings_binding(&services, &api_bindings), |
| url_request_rewrite_rules_provider_binding( |
| &services, |
| &url_request_rewrite_rules_provider), |
| context_binding(&services, &application_context) {} |
| |
| // Directory of services to offer to the Cast component. |
| vfs::PseudoDir services; |
| |
| FakeApiBindingsImpl api_bindings; |
| base::ScopedServiceBinding<chromium::cast::ApiBindings> |
| api_bindings_binding; |
| |
| FakeUrlRequestRewriteRulesProvider url_request_rewrite_rules_provider; |
| base::ScopedServiceBinding<chromium::cast::UrlRequestRewriteRulesProvider> |
| url_request_rewrite_rules_provider_binding; |
| |
| FakeApplicationContext application_context; |
| base::ScopedServiceBinding<chromium::cast::ApplicationContext> |
| context_binding; |
| }; |
| |
| void InjectQueryApi() { |
| // Inject an API which can be used to evaluate arbitrary Javascript and |
| // return the results over a MessagePort. |
| std::vector<chromium::cast::ApiBinding> binding_list; |
| chromium::cast::ApiBinding eval_js_binding; |
| eval_js_binding.set_before_load_script(base::MemBufferFromString( |
| "function valueOrUndefinedString(value) {" |
| " return (typeof(value) == 'undefined') ? 'undefined' : value;" |
| "}" |
| "window.addEventListener('DOMContentLoaded', (event) => {" |
| " var port = cast.__platform__.PortConnector.bind('testport');" |
| " port. => {" |
| " var result = eval(e.data);" |
| " if (result && typeof(result.then) == 'function') {" |
| " result" |
| " .then(result =>" |
| " port.postMessage(valueOrUndefinedString(result)))" |
| " .catch(e => port.postMessage(JSON.stringify(e)));" |
| " } else {" |
| " port.postMessage(valueOrUndefinedString(result));" |
| " }" |
| " };" |
| "});", |
| "test")); |
| binding_list.emplace_back(std::move(eval_js_binding)); |
| services_->api_bindings.set_bindings(std::move(binding_list)); |
| } |
| |
| void WaitQueryApiConnected() { |
| EXPECT_FALSE(test_port_); |
| test_port_ = |
| services_->api_bindings.RunAndReturnConnectedPort("testport").Bind(); |
| } |
| |
| void OnComponentTeardown() { |
| component_.reset(); |
| |
| if (on_component_destroyed_) { |
| std::move(on_component_destroyed_).Run(); |
| } else { |
| ADD_FAILURE() << "Unexpected TestCastComponent teardown."; |
| } |
| } |
| |
| const sys::ServiceDirectory& test_realm_services_; |
| |
| // True if the Cast component should be offered a service directory channel |
| // that has already been closed, to simulate the providing agent having |
| // torn-down the directory before the component Connect()s through it. |
| bool offer_closed_services_ = false; |
| |
| // False if the Cast component should not be offered any service directory. |
| bool offer_services_ = true; |
| |
| // Holds the service directory and fake services offered to `component_`. |
| absl::optional<FakeComponentServices> services_; |
| |
| absl::optional<fuchsia_component_support::DynamicComponentHost> component_; |
| |
| fuchsia::web::MessagePortPtr test_port_; |
| |
| base::OnceClosure on_component_destroyed_; |
| }; |
| |
| // Base class for all integration tests, parameterized on the set of |
| // "features" to enable in the `cast_runner` under test. |
| class CastRunnerIntegrationTest : public testing::Test { |
| protected: |
| CastRunnerIntegrationTest() |
| : CastRunnerIntegrationTest(test::kCastRunnerFeaturesNone) {} |
| explicit CastRunnerIntegrationTest(test::CastRunnerFeatures runner_features) |
| : cast_runner_(runner_features) {} |
| |
| ~CastRunnerIntegrationTest() override = default; |
| |
| CastRunnerIntegrationTest(const CastRunnerIntegrationTest&) = delete; |
| CastRunnerIntegrationTest& operator=(const CastRunnerIntegrationTest&) = |
| delete; |
| |
| // testing::Test overrides. |
| void SetUp() override { |
| static constexpr base::StringPiece kTestServerRoot( |
| "fuchsia_web/runners/cast/testdata"); |
| test_server_.ServeFilesFromSourceDirectory(kTestServerRoot); |
| net::test_server::RegisterDefaultHandlers(&test_server_); |
| ASSERT_TRUE(test_server_.Start()); |
| } |
| |
| // Returns the services exposed by the `CastRunnerLauncher` test Realm, |
| // including those exposed by the `cast_runner` component under test. |
| const sys::ServiceDirectory& test_realm_services() { |
| return cast_runner_.exposed_services(); |
| } |
| |
| test::CastRunnerLauncher& cast_runner_launcher() { return cast_runner_; } |
| |
| // Returns the HTTP server used to serve fake content for Cast components. |
| net::EmbeddedTestServer& test_server() { return test_server_; } |
| |
| // Convenience accessors for elements managed by the launcher. |
| FakeApplicationConfigManager& app_config_manager() { |
| return cast_runner_.fake_cast_agent().app_config_manager(); |
| } |
| |
| private: |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::SingleThreadTaskEnvironment::MainThreadType::IO}; |
| |
| // TODO(https://crbug.com/1168538): Override the RunLoop timeout set by |
| // |task_environment_| to allow for the very high variability in web.Context |
| // launch times. |
| const base::test::ScopedRunLoopTimeout scoped_timeout_{ |
| FROM_HERE, TestTimeouts::action_max_timeout()}; |
| |
| test::CastRunnerLauncher cast_runner_; |
| net::EmbeddedTestServer test_server_; |
| }; |
| |
| } // namespace |
| |
| // A basic integration test ensuring a basic cast request launches the right |
| // URL in the Chromium service. |
| TEST_F(CastRunnerIntegrationTest, BasicRequest) { |
| TestCastComponent component(test_realm_services()); |
| |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, app_url); |
| component.StartCastComponentWithQueryApi(); |
| |
| // Verify that the app has navigated to the expected URL. |
| EXPECT_EQ(component.QueryAppUrl(), app_url.spec()); |
| } |
| |
| // Verify that the Runner can continue to be used even after its Context has |
| // crashed. Regression test for https://crbug.com/1066826. |
| // TODO(crbug.com/1066833): Replace this with a WebRunner test, ideally a |
| // unit-test, which can simulate Context disconnection more simply. |
| TEST_F(CastRunnerIntegrationTest, CanRecreateContext) { |
| TestCastComponent component(test_realm_services()); |
| const GURL app_url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, app_url); |
| |
| // Create a Cast component and verify that it has loaded. |
| component.StartCastComponentWithQueryApi(kTestAppId); |
| |
| // Connect to the CastRunner's Realm, and request to enumerate the contents |
| // of the "web_instances" collection. |
| fidl::SynchronousInterfacePtr<fuchsia::component::Realm> runner_realm; |
| test_realm_services().Connect( |
| runner_realm.NewRequest(), |
| test::CastRunnerLauncher::kCastRunnerRealmProtocol); |
| fidl::SynchronousInterfacePtr<fuchsia::component::ChildIterator> |
| instance_iterator; |
| fuchsia::component::Realm_ListChildren_Result list_children_result; |
| zx_status_t status = runner_realm->ListChildren( |
| fuchsia::component::decl::CollectionRef{.name = "web_instances"}, |
| instance_iterator.NewRequest(), &list_children_result); |
| ASSERT_EQ(status, ZX_OK); |
| ASSERT_FALSE(list_children_result.is_err()); |
| |
| // The CastRunner's "web_instances" collection should contain exactly one |
| // child component. |
| std::vector<fuchsia::component::decl::ChildRef> web_instance_refs; |
| status = instance_iterator->Next(&web_instance_refs); |
| ASSERT_EQ(status, ZX_OK); |
| ASSERT_EQ(web_instance_refs.size(), 1u); |
| |
| // Verify that no further children remain in "web_instances". |
| std::vector<fuchsia::component::decl::ChildRef> empty_web_instance_refs; |
| status = instance_iterator->Next(&empty_web_instance_refs); |
| ASSERT_EQ(status, ZX_OK); |
| ASSERT_TRUE(empty_web_instance_refs.empty()); |
| |
| // Destroy the one child of the "web_instances" collection. |
| fuchsia::component::Realm_DestroyChild_Result destroy_child_result; |
| status = runner_realm->DestroyChild(std::move(web_instance_refs[0]), |
| &destroy_child_result); |
| ASSERT_EQ(status, ZX_OK); |
| ASSERT_FALSE(destroy_child_result.is_err()); |
| |
| // Expect that the Cast component goes away, since its container is gone. |
| component.WaitForComponentDestroyed(); |
| |
| // Create a second Cast component and verify that it has loaded. |
| // There is no guarantee that the CastRunner has detected the old web.Context |
| // disconnecting yet, so attempts to launch Cast components could fail. |
| // WebContentRunner::CreateFrameWithParams() will synchronously verify that |
| // the web.Context is not-yet-closed, to work-around that. |
| TestCastComponent second_component(test_realm_services()); |
| second_component.StartCastComponentWithQueryApi(kTestAppId); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, ApiBindings) { |
| TestCastComponent component(test_realm_services()); |
| app_config_manager().AddApp(kTestAppId, test_server().GetURL(kBlankAppUrl)); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| // Verify that we can communicate with the query-API binding added by |
| // `StartCastComponentWithQueryApi()`. |
| EXPECT_EQ(component.ExecuteJavaScript("1+2+\"\""), "3"); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, UnknownCastAppId_Fails) { |
| TestCastComponent component(test_realm_services()); |
| const char kUnknownComponentUrl[] = "cast:99999999"; |
| |
| component.StartCastComponent(kUnknownComponentUrl); |
| |
| // Run the loop until the ComponentController is dropped. |
| component.WaitForComponentDestroyed(); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, UrlRequestRewriteRulesProvider) { |
| TestCastComponent component(test_realm_services()); |
| GURL echo_app_url = test_server().GetURL(kEchoHeaderPath); |
| app_config_manager().AddApp(kTestAppId, echo_app_url); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| EXPECT_EQ(component.ExecuteJavaScript("document.body.innerText"), |
| "TestHeaderValue"); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, ApplicationControllerBound) { |
| TestCastComponent component(test_realm_services()); |
| app_config_manager().AddApp(kTestAppId, test_server().GetURL(kBlankAppUrl)); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| // Run until the application calls SetApplicationController(). |
| component.application_context().WaitForSetApplicationController(); |
| EXPECT_TRUE(component.application_context().application_controller()); |
| } |
| |
| // Verify an App launched with remote debugging enabled is properly reachable. |
| TEST_F(CastRunnerIntegrationTest, RemoteDebugging) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| auto app_config = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| app_config.set_enable_remote_debugging(true); |
| app_config_manager().AddAppConfig(std::move(app_config)); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| // Connect to the debug service and ensure we get the proper response. |
| base::Value::List devtools_list = |
| GetDevToolsListFromPort(CastRunner::kRemoteDebuggingPort); |
| EXPECT_EQ(devtools_list.size(), 1u); |
| |
| const auto* devtools_url = devtools_list[0].GetDict().FindString("url"); |
| ASSERT_TRUE(devtools_url); |
| EXPECT_EQ(*devtools_url, app_url.spec()); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, IsolatedContext) { |
| TestCastComponent component(test_realm_services()); |
| const GURL kContentDirectoryUrl("fuchsia-dir://testdata/empty.html"); |
| |
| app_config_manager().AddAppConfig( |
| CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl)); |
| component.StartCastComponentWithQueryApi(); |
| |
| // Verify that the app was navigated to the isolated content URL. |
| EXPECT_EQ(component.QueryAppUrl(), kContentDirectoryUrl.spec()); |
| } |
| |
| // Verify that the component fails to start if no service directory is offered. |
| TEST_F(CastRunnerIntegrationTest, ServiceDirectoryMissing_FailToStart) { |
| TestCastComponent component(test_realm_services()); |
| component.disable_offer_services(); |
| app_config_manager().AddApp(kTestAppId, |
| test_server().GetURL(kEchoHeaderPath)); |
| |
| component.StartCastComponent(base::StrCat({"cast:", kTestAppId})); |
| |
| // Expect that the component stops. |
| component.WaitForComponentDestroyed(); |
| } |
| |
| // Verify that the component fails to start if the offered service directory |
| // channel has already been closed, such that Connect() calls will result in |
| // service request channels being dropped. |
| TEST_F(CastRunnerIntegrationTest, ServiceDirectoryEmpty_FailToStart) { |
| TestCastComponent component(test_realm_services()); |
| component.offer_closed_services(); |
| app_config_manager().AddApp(kTestAppId, |
| test_server().GetURL(kEchoHeaderPath)); |
| |
| component.StartCastComponent(base::StrCat({"cast:", kTestAppId})); |
| |
| // Expect that the component stops. |
| component.WaitForComponentDestroyed(); |
| } |
| |
| // Simulate an Agent crash by tearing down `services_`, resulting in the |
| // service-directory and bindings passed to the Cast activity itself being |
| // closed. This should cause the component to terminate. |
| TEST_F(CastRunnerIntegrationTest, ServicesClose_TerminatesComponent) { |
| TestCastComponent component(test_realm_services()); |
| app_config_manager().AddApp(kTestAppId, |
| test_server().GetURL(kEchoHeaderPath)); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| // Disconnect all service bindings. |
| component.DisconnectServices(); |
| |
| component.WaitForComponentDestroyed(); |
| } |
| |
| class AudioCastRunnerIntegrationTest : public CastRunnerIntegrationTest { |
| public: |
| AudioCastRunnerIntegrationTest() |
| : CastRunnerIntegrationTest( |
| test::kCastRunnerFeaturesFakeAudioDeviceEnumerator) {} |
| }; |
| |
| TEST_F(AudioCastRunnerIntegrationTest, Microphone) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL("/microphone.html"); |
| auto app_config = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| |
| fuchsia::web::PermissionDescriptor mic_permission; |
| mic_permission.set_type(fuchsia::web::PermissionType::MICROPHONE); |
| app_config.mutable_permissions()->push_back(std::move(mic_permission)); |
| app_config_manager().AddAppConfig(std::move(app_config)); |
| |
| // Expect fuchsia.media.Audio connection to be requested. |
| base::RunLoop run_loop; |
| cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure( |
| fuchsia::media::Audio::Name_, run_loop.QuitClosure()); |
| |
| component.StartCastComponentWithQueryApi(); |
| component.ExecuteJavaScript("connectMicrophone();"); |
| |
| // Will quit once AudioCapturer is connected. |
| run_loop.Run(); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, Camera) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL("/camera.html"); |
| auto app_config = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| |
| fuchsia::web::PermissionDescriptor camera_permission; |
| camera_permission.set_type(fuchsia::web::PermissionType::CAMERA); |
| app_config.mutable_permissions()->push_back(std::move(camera_permission)); |
| app_config_manager().AddAppConfig(std::move(app_config)); |
| |
| // Expect fuchsia.camera3.DeviceWatcher connection to be requested. |
| cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure( |
| fuchsia::camera3::DeviceWatcher::Name_, |
| base::MakeExpectedRunAtLeastOnceClosure(FROM_HERE)); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| component.ExecuteJavaScript("connectCamera();"); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, CameraAccessAfterComponentShutdown) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL("/camera.html"); |
| |
| // First app with camera permission. |
| auto app_config = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| fuchsia::web::PermissionDescriptor camera_permission; |
| camera_permission.set_type(fuchsia::web::PermissionType::CAMERA); |
| app_config.mutable_permissions()->push_back(std::move(camera_permission)); |
| app_config_manager().AddAppConfig(std::move(app_config)); |
| |
| // Second app without camera permission (but it will still try to access |
| // fuchsia.camera3.DeviceWatcher service to enumerate devices). |
| TestCastComponent second_component(test_realm_services()); |
| auto app_config_2 = |
| FakeApplicationConfigManager::CreateConfig(kSecondTestAppId, app_url); |
| app_config_manager().AddAppConfig(std::move(app_config_2)); |
| |
| // Start and then shutdown the first app. |
| component.StartCastComponentWithQueryApi(kTestAppId); |
| component.ShutdownComponent(); |
| |
| // Start the second app and try to connect the camera. It's expected to fail |
| // to initialize the camera without crashing CastRunner. |
| second_component.StartCastComponentWithQueryApi(kSecondTestAppId); |
| EXPECT_EQ(second_component.ExecuteJavaScript("connectCamera();"), |
| "getUserMediaFailed"); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, MultipleComponentsUsingCamera) { |
| TestCastComponent first_component(test_realm_services()); |
| TestCastComponent second_component(test_realm_services()); |
| |
| GURL app_url = test_server().GetURL("/camera.html"); |
| |
| // Expect fuchsia.camera3.DeviceWatcher connection to be requested. |
| cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure( |
| fuchsia::camera3::DeviceWatcher::Name_, |
| base::MakeExpectedRunAtLeastOnceClosure(FROM_HERE)); |
| |
| // Start two apps, both with camera permission. |
| auto app_config1 = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| fuchsia::web::PermissionDescriptor camera_permission1; |
| camera_permission1.set_type(fuchsia::web::PermissionType::CAMERA); |
| app_config1.mutable_permissions()->push_back(std::move(camera_permission1)); |
| app_config_manager().AddAppConfig(std::move(app_config1)); |
| first_component.StartCastComponentWithQueryApi(kTestAppId); |
| |
| auto app_config2 = |
| FakeApplicationConfigManager::CreateConfig(kSecondTestAppId, app_url); |
| fuchsia::web::PermissionDescriptor camera_permission2; |
| camera_permission2.set_type(fuchsia::web::PermissionType::CAMERA); |
| app_config2.mutable_permissions()->push_back(std::move(camera_permission2)); |
| app_config_manager().AddAppConfig(std::move(app_config2)); |
| second_component.StartCastComponentWithQueryApi(kSecondTestAppId); |
| |
| // Shut down the first component. |
| first_component.ShutdownComponent(); |
| |
| second_component.ExecuteJavaScript("connectCamera();"); |
| } |
| |
| class HeadlessCastRunnerIntegrationTest : public CastRunnerIntegrationTest { |
| public: |
| HeadlessCastRunnerIntegrationTest() |
| : CastRunnerIntegrationTest(test::kCastRunnerFeaturesHeadless) {} |
| }; |
| |
| // A basic integration test ensuring a basic cast request launches the right |
| // URL in the Chromium service. |
| TEST_F(HeadlessCastRunnerIntegrationTest, Headless) { |
| TestCastComponent component(test_realm_services()); |
| |
| const char kAnimationPath[] = "/css_animation.html"; |
| const GURL animation_url = test_server().GetURL(kAnimationPath); |
| app_config_manager().AddApp(kTestAppId, animation_url); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| fuchsia::ui::views::ViewToken view_token; |
| fuchsia::ui::views::ViewHolderToken view_holder_token; |
| auto status = |
| zx::eventpair::create(0u, &view_token.value, &view_holder_token.value); |
| CHECK_EQ(ZX_OK, status); |
| |
| fuchsia::ui::views::ViewRefControl view_ref_control; |
| fuchsia::ui::views::ViewRef view_ref; |
| status = zx::eventpair::create( |
| /*options*/ 0u, &view_ref_control.reference, &view_ref.reference); |
| CHECK_EQ(ZX_OK, status); |
| view_ref_control.reference.replace( |
| ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE), |
| &view_ref_control.reference); |
| view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference); |
| |
| // Create a view. |
| auto view_provider = component.exposed_by_component() |
| .Connect<fuchsia::ui::app::ViewProvider>(); |
| view_provider->CreateViewWithViewRef(std::move(view_holder_token.value), |
| std::move(view_ref_control), |
| std::move(view_ref)); |
| |
| component.api_bindings().RunAndReturnConnectedPort("animation_finished"); |
| |
| // Verify that dropped "view" EventPair is handled properly. |
| view_token.value.reset(); |
| component.api_bindings().RunAndReturnConnectedPort("view_hidden"); |
| } |
| |
| // Isolated *and* headless? Doesn't sound like much fun! |
| TEST_F(HeadlessCastRunnerIntegrationTest, IsolatedAndHeadless) { |
| TestCastComponent component(test_realm_services()); |
| const GURL kContentDirectoryUrl("fuchsia-dir://testdata/empty.html"); |
| |
| app_config_manager().AddAppConfig( |
| CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl)); |
| component.StartCastComponentWithQueryApi(); |
| |
| // Verify that the app was able to navigate to the isolated content URL. |
| EXPECT_EQ(component.QueryAppUrl(), kContentDirectoryUrl.spec()); |
| } |
| |
| // Verifies that the Context can establish a connection to the Agent's |
| // MetricsRecorder service. |
| TEST_F(CastRunnerIntegrationTest, LegacyMetricsRedirect) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, app_url); |
| |
| bool connected_to_metrics_recorder_service = false; |
| |
| cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure( |
| fuchsia::legacymetrics::MetricsRecorder::Name_, |
| base::BindLambdaForTesting([&connected_to_metrics_recorder_service]() { |
| connected_to_metrics_recorder_service = true; |
| })); |
| |
| // If the Component is going to connect to the MetricsRecorder service, it |
| // will have done so by the time the Component is responding. |
| component.StartCastComponentWithQueryApi(); |
| ASSERT_EQ(connected_to_metrics_recorder_service, |
| #if BUILDFLAG(ENABLE_CAST_RECEIVER) |
| true |
| #else |
| false |
| #endif |
| ); |
| } |
| |
| // Verifies that the ApplicationContext::OnApplicationTerminated() is notified |
| // with the component exit code if the web content closes itself. |
| TEST_F(CastRunnerIntegrationTest, OnApplicationTerminated_WindowClose) { |
| TestCastComponent component(test_realm_services()); |
| const GURL url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, url); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| // It is possible to observe the component teardown before |
| // OnApplicationTerminated() is received, so ignore that. |
| component.SetIgnoreComponentDestroyed(); |
| |
| // Have the web content close itself, and wait for OnApplicationTerminated(). |
| EXPECT_EQ(component.ExecuteJavaScript("window.close()"), "undefined"); |
| absl::optional<zx_status_t> exit_code = |
| component.application_context().WaitForApplicationTerminated(); |
| ASSERT_TRUE(exit_code); |
| EXPECT_EQ(exit_code.value(), ZX_OK); |
| } |
| |
| // Verifies that the ApplicationContext::OnApplicationTerminated() is notified |
| // with the component exit code if the component is requested to stop. |
| TEST_F(CastRunnerIntegrationTest, OnApplicationTerminated_ComponentStop) { |
| TestCastComponent component(test_realm_services()); |
| const GURL url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, url); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| // It is possible to observe the component teardown before |
| // OnApplicationTerminated() is received, so ignore that. |
| component.SetIgnoreComponentDestroyed(); |
| |
| // Request that the component be destroyed, and wait for |
| // OnApplicationTerminated(). |
| component.ShutdownComponent(); |
| absl::optional<zx_status_t> exit_code = |
| component.application_context().WaitForApplicationTerminated(); |
| ASSERT_TRUE(exit_code); |
| EXPECT_EQ(exit_code.value(), ZX_OK); |
| } |
| |
| // Ensures that CastRunner handles the value not being specified. |
| // TODO(https://crrev.com/c/2516246): Check for no logging. |
| TEST_F(CastRunnerIntegrationTest, InitialMinConsoleLogSeverity_NotSet) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| auto app_config = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| |
| EXPECT_FALSE(app_config.has_initial_min_console_log_severity()); |
| app_config_manager().AddAppConfig(std::move(app_config)); |
| |
| component.StartCastComponentWithQueryApi(); |
| } |
| |
| // TODO(https://crrev.com/c/2516246): Check for logging. |
| TEST_F(CastRunnerIntegrationTest, InitialMinConsoleLogSeverity_DEBUG) { |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| auto app_config = |
| FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url); |
| |
| *app_config.mutable_initial_min_console_log_severity() = |
| fuchsia::diagnostics::Severity::DEBUG; |
| app_config_manager().AddAppConfig(std::move(app_config)); |
| |
| component.StartCastComponentWithQueryApi(); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, WebGLContextAbsentWithoutVulkanFeature) { |
| TestCastComponent component(test_realm_services()); |
| const char kTestPath[] = "/webgl_presence.html"; |
| const GURL test_url = test_server().GetURL(kTestPath); |
| app_config_manager().AddApp(kTestAppId, test_url); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| EXPECT_EQ(component.ExecuteJavaScript("document.title"), "absent"); |
| } |
| |
| TEST_F(CastRunnerIntegrationTest, |
| WebGLContextAbsentWithoutVulkanFeature_IsolatedRunner) { |
| TestCastComponent component(test_realm_services()); |
| const GURL kContentDirectoryUrl("fuchsia-dir://testdata/webgl_presence.html"); |
| |
| app_config_manager().AddAppConfig( |
| CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl)); |
| component.StartCastComponentWithQueryApi(); |
| |
| EXPECT_EQ(component.ExecuteJavaScript("document.title"), "absent"); |
| } |
| |
| // Verifies that starting a component fails if CORS exempt headers cannot be |
| // fetched. |
| TEST_F(CastRunnerIntegrationTest, MissingCorsExemptHeaderProvider) { |
| // Prevent the FakeCastAgent from publishing the |
| // chromium.cast.CorsExemptHeaderProvider service. |
| cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure( |
| chromium::cast::CorsExemptHeaderProvider::Name_, base::DoNothing()); |
| |
| // Start the Cast component, and wait for it to be destroyed. |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, app_url); |
| component.StartCastComponent(base::StrCat({"cast:", kTestAppId})); |
| |
| // Expect it to be more or less immediately torn-down. |
| component.WaitForComponentDestroyed(); |
| } |
| |
| // Verifies that CastRunner offers a chromium.cast.DataReset service. |
| // Verifies that after the DeletePersistentData() API is invoked, no further |
| // component-start requests are honoured. |
| // TODO(crbug.com/1146474): Expand the test to verify that the persisted data is |
| // correctly cleared (e.g. using a custom test HTML app that uses persisted |
| // data). |
| TEST_F(CastRunnerIntegrationTest, DataReset_Service) { |
| base::RunLoop loop; |
| auto data_reset = test_realm_services().Connect<chromium::cast::DataReset>(); |
| data_reset.set_error_handler([quit_loop = loop.QuitClosure()](zx_status_t) { |
| quit_loop.Run(); |
| ADD_FAILURE(); |
| }); |
| bool succeeded = false; |
| data_reset->DeletePersistentData([&succeeded, &loop](bool result) { |
| succeeded = result; |
| loop.Quit(); |
| }); |
| loop.Run(); |
| |
| EXPECT_TRUE(succeeded); |
| |
| // Verify that it is no longer possible to launch a component. |
| TestCastComponent component(test_realm_services()); |
| GURL app_url = test_server().GetURL(kBlankAppUrl); |
| app_config_manager().AddApp(kTestAppId, app_url); |
| component.StartCastComponent(base::StrCat({"cast:", kTestAppId})); |
| component.WaitForComponentDestroyed(); |
| } |
| |
| // Verifies that the CastRunner exposes a fuchsia.web.FrameHost protocol |
| // capability, without requiring any special configuration. |
| TEST_F(CastRunnerIntegrationTest, FrameHost_Service) { |
| // Connect to the fuchsia.web.FrameHost service and create a Frame. |
| auto frame_host = test_realm_services().Connect<fuchsia::web::FrameHost>(); |
| fuchsia::web::FramePtr frame; |
| frame_host->CreateFrameWithParams(fuchsia::web::CreateFrameParams(), |
| frame.NewRequest()); |
| |
| // Verify that a response is received for a LoadUrl() request to the frame. |
| fuchsia::web::NavigationControllerPtr controller; |
| frame->GetNavigationController(controller.NewRequest()); |
| const GURL url = test_server().GetURL(kBlankAppUrl); |
| EXPECT_TRUE(LoadUrlAndExpectResponse( |
| controller.get(), fuchsia::web::LoadUrlParams(), url.spec())); |
| } |
| |
| // Check that connecting and disconnecting to the FrameHost service does not |
| // trigger shutdown of the devtools service. |
| TEST_F(CastRunnerIntegrationTest, FrameHostDebugging) { |
| // Before triggering the launch of any `web_instance` by the `cast_runner`, |
| // attach a `TestDebugListener`, to be notified when the DevTools port becomes |
| // available. |
| TestDebugListener dev_tools_listener; |
| fidl::Binding<fuchsia::web::DevToolsListener> dev_tools_listener_binding( |
| &dev_tools_listener); |
| auto debug = test_realm_services().Connect<fuchsia::web::Debug>(); |
| debug.set_error_handler([](zx_status_t status) { |
| ZX_LOG(ERROR, status) << "Failed to use debug protocol"; |
| ADD_FAILURE(); |
| }); |
| base::RunLoop dev_tools_enabled; |
| debug->EnableDevTools( |
| dev_tools_listener_binding.NewBinding(), |
| [done = dev_tools_enabled.QuitClosure()]() { done.Run(); }); |
| dev_tools_enabled.Run(); |
| |
| // Connect a `FrameHost` client, create a `Frame`, and navigate it to some |
| // test content. Loading the content will result in the DevTools port becoming |
| // available for the test to connect to. |
| auto frame_host = test_realm_services().Connect<fuchsia::web::FrameHost>(); |
| fuchsia::web::CreateFrameParams create_frame_params; |
| create_frame_params.set_enable_remote_debugging(true); |
| auto frame = FrameForTest::Create(frame_host, std::move(create_frame_params)); |
| GURL url = test_server().GetURL("/defaultresponse"); |
| ASSERT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(), |
| fuchsia::web::LoadUrlParams(), |
| url.spec())); |
| frame.navigation_listener().RunUntilUrlEquals(url); |
| |
| // Wait for the DevTools port to become available, and then connect to it to |
| // verify that there is a single debuggable page listed. |
| dev_tools_listener.RunUntilNumberOfPortsIs(1); |
| uint16_t remote_debugging_port = *(dev_tools_listener.debug_ports().begin()); |
| |
| base::Value::List devtools_list = |
| GetDevToolsListFromPort(remote_debugging_port); |
| EXPECT_EQ(devtools_list.size(), 1u); |
| { |
| const auto* devtools_url = devtools_list[0].GetDict().FindString("url"); |
| ASSERT_TRUE(devtools_url); |
| EXPECT_EQ(*devtools_url, url); |
| } |
| |
| // Create a new `FrameHost` client, and immediately close it. The DevTools |
| // port should remain open regardless. |
| auto frame_host_2 = test_realm_services().Connect<fuchsia::web::FrameHost>(); |
| frame_host_2.Unbind(); |
| |
| // Navigate to a different page. The devtools service should still be active |
| // and report the new page. |
| GURL url2 = test_server().GetURL("/title1.html"); |
| ASSERT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(), |
| fuchsia::web::LoadUrlParams(), |
| url2.spec())); |
| frame.navigation_listener().RunUntilUrlEquals(url2); |
| |
| devtools_list = GetDevToolsListFromPort(remote_debugging_port); |
| EXPECT_EQ(devtools_list.size(), 1u); |
| { |
| const auto* devtools_url = devtools_list[0].GetDict().FindString("url"); |
| ASSERT_TRUE(devtools_url); |
| EXPECT_EQ(*devtools_url, url2); |
| } |
| } |
| |
| #if defined(ARCH_CPU_ARM_FAMILY) |
| // TODO(crbug.com/1377994): Enable on ARM64 when bots support Vulkan. |
| #define MAYBE_VulkanCastRunnerIntegrationTest \ |
| DISABLED_VulkanCastRunnerIntegrationTest |
| #else |
| #define MAYBE_VulkanCastRunnerIntegrationTest VulkanCastRunnerIntegrationTest |
| #endif |
| |
| class MAYBE_VulkanCastRunnerIntegrationTest : public CastRunnerIntegrationTest { |
| public: |
| MAYBE_VulkanCastRunnerIntegrationTest() |
| : CastRunnerIntegrationTest(test::kCastRunnerFeaturesVulkan) {} |
| }; |
| |
| TEST_F(MAYBE_VulkanCastRunnerIntegrationTest, |
| WebGLContextPresentWithVulkanFeature) { |
| TestCastComponent component(test_realm_services()); |
| const char kTestPath[] = "/webgl_presence.html"; |
| const GURL test_url = test_server().GetURL(kTestPath); |
| app_config_manager().AddApp(kTestAppId, test_url); |
| |
| component.StartCastComponentWithQueryApi(); |
| |
| EXPECT_EQ(component.ExecuteJavaScript("document.title"), "present"); |
| } |
| |
| TEST_F(MAYBE_VulkanCastRunnerIntegrationTest, |
| WebGLContextPresentWithVulkanFeature_IsolatedRunner) { |
| TestCastComponent component(test_realm_services()); |
| const GURL kContentDirectoryUrl("fuchsia-dir://testdata/webgl_presence.html"); |
| |
| app_config_manager().AddAppConfig( |
| CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl)); |
| component.StartCastComponentWithQueryApi(); |
| |
| EXPECT_EQ(component.ExecuteJavaScript("document.title"), "present"); |
| } |