| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/public/test/browser_test_utils.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <set> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/no_destructor.h" |
| #include "base/process/kill.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/test_future.h" |
| #include "base/test/test_switches.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "base/types/optional_ref.h" |
| #include "base/types/optional_util.h" |
| #include "base/uuid.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "cc/test/pixel_test_utils.h" |
| #include "components/input/render_widget_host_input_event_router.h" |
| #include "components/viz/client/frame_evictor.h" |
| #include "content/browser/file_system/file_system_manager_impl.h" |
| #include "content/browser/file_system_access/file_system_access_manager_impl.h" |
| #include "content/browser/renderer_host/cross_process_frame_connector.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/media/media_stream_manager.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/navigation_throttle_runner.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/renderer_host/render_frame_metadata_provider_impl.h" |
| #include "content/browser/renderer_host/render_frame_proxy_host.h" |
| #include "content/browser/renderer_host/render_process_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_factory.h" |
| #include "content/browser/renderer_host/render_widget_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_view_child_frame.h" |
| #include "content/browser/screen_orientation/screen_orientation_provider.h" |
| #include "content/browser/storage_partition_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/web_contents/web_contents_view.h" |
| #include "content/common/frame.mojom.h" |
| #include "content/common/input/synthetic_touchscreen_pinch_gesture.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/child_process_termination_info.h" |
| #include "content/public/browser/histogram_fetcher.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host_observer.h" |
| #include "content/public/browser/screen_orientation_delegate.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/weak_document_ptr.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/isolated_world_ids.h" |
| #include "content/public/test/accessibility_notification_waiter.h" |
| #include "content/public/test/no_renderer_crashes_assertion.h" |
| #include "content/public/test/simple_url_loader_test_helper.h" |
| #include "content/public/test/synchronize_visual_properties_interceptor.h" |
| #include "content/public/test/test_devtools_protocol_client.h" |
| #include "content/public/test/test_fileapi_operation_waiter.h" |
| #include "content/public/test/test_frame_navigation_observer.h" |
| #include "content/public/test/test_launcher.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/test/did_commit_navigation_interceptor.h" |
| #include "content/test/mock_commit_deferring_condition.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/completion_once_callback.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/io_buffer.h" |
| #include "net/cookies/canonical_cookie.h" |
| #include "net/cookies/cookie_access_result.h" |
| #include "net/cookies/cookie_constants.h" |
| #include "net/cookies/cookie_util.h" |
| #include "net/filter/gzip_header.h" |
| #include "net/filter/gzip_source_stream.h" |
| #include "net/filter/mock_source_stream.h" |
| #include "net/filter/source_stream_type.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "net/test/python_utils.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/cookie_manager.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "services/network/public/mojom/network_service_test.mojom.h" |
| #include "storage/browser/blob/blob_url_registry.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/functional/overload.h" |
| #include "third_party/blink/public/common/chrome_debug_urls.h" |
| #include "third_party/blink/public/common/frame/frame_visual_properties.h" |
| #include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h" |
| #include "third_party/blink/public/mojom/filesystem/file_system.mojom.h" |
| #include "third_party/blink/public/mojom/keyboard_lock/keyboard_lock.mojom-shared.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/accessibility/platform/browser_accessibility.h" |
| #include "ui/accessibility/platform/browser_accessibility_manager.h" |
| #include "ui/base/clipboard/clipboard.h" |
| #include "ui/base/clipboard/scoped_clipboard_writer.h" |
| #include "ui/base/clipboard/test/test_clipboard.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/compositor/test/draw_waiter_for_test.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/gesture_detection/gesture_configuration.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/keycodes/dom/keycode_converter.h" |
| #include "ui/gfx/geometry/point_f.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/latency/latency_info.h" |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| #include "content/browser/media/captured_surface_controller.h" |
| #include "content/public/test/mock_captured_surface_controller.h" |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "ash/webui/grit/ash_webui_common_resources.h" |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| #include <combaseapi.h> |
| #include <wrl/client.h> |
| |
| #include "base/win/scoped_safearray.h" |
| #include "base/win/scoped_variant.h" |
| |
| #include <uiautomation.h> |
| #endif |
| |
| #if defined(USE_AURA) |
| #include "content/browser/renderer_host/delegated_frame_host.h" |
| #include "content/browser/renderer_host/render_widget_host_view_aura.h" |
| #include "ui/aura/test/window_event_dispatcher_test_api.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_event_dispatcher.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/events/event.h" |
| #endif // USE_AURA |
| |
| namespace content { |
| namespace { |
| |
| void BuildSimpleWebKeyEvent(blink::WebInputEvent::Type type, |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code, |
| input::NativeWebKeyboardEvent* event) { |
| event->dom_key = key; |
| event->dom_code = static_cast<int>(code); |
| event->native_key_code = ui::KeycodeConverter::DomCodeToNativeKeycode(code); |
| event->windows_key_code = key_code; |
| event->is_system_key = false; |
| event->skip_if_unhandled = true; |
| |
| if (type == blink::WebInputEvent::Type::kChar || |
| type == blink::WebInputEvent::Type::kRawKeyDown) { |
| // |key| is the only parameter that contains information about the case of |
| // the character. Use it to be able to generate lower case input. |
| if (key.IsCharacter()) { |
| event->text[0] = key.ToCharacter(); |
| event->unmodified_text[0] = key.ToCharacter(); |
| } else { |
| event->text[0] = key_code; |
| event->unmodified_text[0] = key_code; |
| } |
| } |
| } |
| |
| void InjectRawKeyEvent(WebContents* web_contents, |
| blink::WebInputEvent::Type type, |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code, |
| int modifiers) { |
| input::NativeWebKeyboardEvent event(type, modifiers, base::TimeTicks::Now()); |
| BuildSimpleWebKeyEvent(type, key, code, key_code, &event); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| RenderWidgetHostImpl* main_frame_rwh = |
| web_contents_impl->GetPrimaryMainFrame()->GetRenderWidgetHost(); |
| web_contents_impl->GetFocusedRenderWidgetHost(main_frame_rwh) |
| ->ForwardKeyboardEvent(event); |
| } |
| |
| int SimulateModifierKeysDown(WebContents* web_contents, |
| bool control, |
| bool shift, |
| bool alt, |
| bool command) { |
| int modifiers = 0; |
| |
| // The order of these key down events shouldn't matter for our simulation. |
| // For our simulation we can use either the left keys or the right keys. |
| if (control) { |
| modifiers |= blink::WebInputEvent::kControlKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kRawKeyDown, |
| ui::DomKey::CONTROL, ui::DomCode::CONTROL_LEFT, |
| ui::VKEY_CONTROL, modifiers); |
| } |
| if (shift) { |
| modifiers |= blink::WebInputEvent::kShiftKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kRawKeyDown, |
| ui::DomKey::SHIFT, ui::DomCode::SHIFT_LEFT, |
| ui::VKEY_SHIFT, modifiers); |
| } |
| if (alt) { |
| modifiers |= blink::WebInputEvent::kAltKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kRawKeyDown, |
| ui::DomKey::ALT, ui::DomCode::ALT_LEFT, ui::VKEY_MENU, |
| modifiers); |
| } |
| if (command) { |
| modifiers |= blink::WebInputEvent::kMetaKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kRawKeyDown, |
| ui::DomKey::META, ui::DomCode::META_LEFT, |
| ui::VKEY_COMMAND, modifiers); |
| } |
| return modifiers; |
| } |
| |
| int SimulateModifierKeysUp(WebContents* web_contents, |
| bool control, |
| bool shift, |
| bool alt, |
| bool command, |
| int modifiers) { |
| // The order of these key releases shouldn't matter for our simulation. |
| if (control) { |
| modifiers &= ~blink::WebInputEvent::kControlKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kKeyUp, |
| ui::DomKey::CONTROL, ui::DomCode::CONTROL_LEFT, |
| ui::VKEY_CONTROL, modifiers); |
| } |
| |
| if (shift) { |
| modifiers &= ~blink::WebInputEvent::kShiftKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kKeyUp, |
| ui::DomKey::SHIFT, ui::DomCode::SHIFT_LEFT, |
| ui::VKEY_SHIFT, modifiers); |
| } |
| |
| if (alt) { |
| modifiers &= ~blink::WebInputEvent::kAltKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kKeyUp, |
| ui::DomKey::ALT, ui::DomCode::ALT_LEFT, ui::VKEY_MENU, |
| modifiers); |
| } |
| |
| if (command) { |
| modifiers &= ~blink::WebInputEvent::kMetaKey; |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kKeyUp, |
| ui::DomKey::META, ui::DomCode::META_LEFT, |
| ui::VKEY_COMMAND, modifiers); |
| } |
| return modifiers; |
| } |
| |
| void SimulateKeyEvent(WebContents* web_contents, |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code, |
| bool send_char, |
| int modifiers) { |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kRawKeyDown, key, |
| code, key_code, modifiers); |
| if (send_char) { |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kChar, key, |
| code, key_code, modifiers); |
| } |
| InjectRawKeyEvent(web_contents, blink::WebInputEvent::Type::kKeyUp, key, code, |
| key_code, modifiers); |
| } |
| |
| void SimulateKeyPressImpl(WebContents* web_contents, |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code, |
| bool control, |
| bool shift, |
| bool alt, |
| bool command, |
| bool send_char) { |
| int modifiers = |
| SimulateModifierKeysDown(web_contents, control, shift, alt, command); |
| SimulateKeyEvent(web_contents, key, code, key_code, send_char, modifiers); |
| modifiers = SimulateModifierKeysUp(web_contents, control, shift, alt, command, |
| modifiers); |
| ASSERT_EQ(modifiers, 0); |
| } |
| |
| std::unique_ptr<net::test_server::HttpResponse> |
| CrossSiteRedirectResponseHandler(const net::EmbeddedTestServer* test_server, |
| const net::test_server::HttpRequest& request) { |
| net::HttpStatusCode http_status_code; |
| |
| // Inspect the prefix and extract the remainder of the url into |params|. |
| size_t length_of_chosen_prefix; |
| std::string prefix_302("/cross-site/"); |
| std::string prefix_307("/cross-site-307/"); |
| if (base::StartsWith(request.relative_url, prefix_302, |
| base::CompareCase::SENSITIVE)) { |
| http_status_code = net::HTTP_MOVED_PERMANENTLY; |
| length_of_chosen_prefix = prefix_302.length(); |
| } else if (base::StartsWith(request.relative_url, prefix_307, |
| base::CompareCase::SENSITIVE)) { |
| http_status_code = net::HTTP_TEMPORARY_REDIRECT; |
| length_of_chosen_prefix = prefix_307.length(); |
| } else { |
| // Unrecognized prefix - let somebody else handle this request. |
| return nullptr; |
| } |
| std::string params = request.relative_url.substr(length_of_chosen_prefix); |
| |
| // A hostname to redirect to must be included in the URL, therefore at least |
| // one '/' character is expected. |
| size_t slash = params.find('/'); |
| if (slash == std::string::npos) { |
| return nullptr; |
| } |
| |
| // Replace the host of the URL with the one passed in the URL. |
| GURL::Replacements replace_host; |
| replace_host.SetHostStr(std::string_view(params).substr(0, slash)); |
| GURL redirect_server = |
| test_server->base_url().ReplaceComponents(replace_host); |
| |
| // Append the real part of the path to the new URL. |
| std::string path = params.substr(slash + 1); |
| GURL redirect_target(redirect_server.Resolve(path)); |
| DCHECK(redirect_target.is_valid()); |
| |
| auto http_response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(http_status_code); |
| http_response->AddCustomHeader("Location", redirect_target.spec()); |
| return std::move(http_response); |
| } |
| |
| // Helper class used by the TestNavigationManager to pause navigations. |
| // Note: the throttle should be added to the *end* of the list of throttles, |
| // so all NavigationThrottles that should be attached observe the |
| // WillStartRequest callback. RegisterThrottleForTesting has this behavior. |
| class TestNavigationManagerThrottle : public NavigationThrottle { |
| public: |
| TestNavigationManagerThrottle( |
| NavigationThrottleRegistry& registry, |
| base::OnceClosure on_will_start_request_closure, |
| base::RepeatingClosure on_will_redirect_request_closure, |
| base::OnceClosure on_will_process_response_closure) |
| : NavigationThrottle(registry), |
| on_will_start_request_closure_( |
| std::move(on_will_start_request_closure)), |
| on_will_redirect_request_closure_( |
| std::move(on_will_redirect_request_closure)), |
| on_will_process_response_closure_( |
| std::move(on_will_process_response_closure)) {} |
| ~TestNavigationManagerThrottle() override {} |
| |
| const char* GetNameForLogging() override { |
| return "TestNavigationManagerThrottle"; |
| } |
| |
| private: |
| // NavigationThrottle: |
| NavigationThrottle::ThrottleCheckResult WillStartRequest() override { |
| DCHECK(on_will_start_request_closure_); |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, std::move(on_will_start_request_closure_)); |
| return NavigationThrottle::DEFER; |
| } |
| |
| NavigationThrottle::ThrottleCheckResult WillRedirectRequest() override { |
| CHECK(on_will_redirect_request_closure_); |
| GetUIThreadTaskRunner({})->PostTask(FROM_HERE, |
| on_will_redirect_request_closure_); |
| return NavigationThrottle::DEFER; |
| } |
| |
| NavigationThrottle::ThrottleCheckResult WillProcessResponse() override { |
| DCHECK(on_will_process_response_closure_); |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, std::move(on_will_process_response_closure_)); |
| return NavigationThrottle::DEFER; |
| } |
| |
| base::OnceClosure on_will_start_request_closure_; |
| base::RepeatingClosure on_will_redirect_request_closure_; |
| base::OnceClosure on_will_process_response_closure_; |
| }; |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| void AppendGzippedResource(const base::RefCountedMemory& encoded, |
| std::string* to_append) { |
| auto source_stream = std::make_unique<net::MockSourceStream>(); |
| source_stream->AddReadResult(base::span(encoded), net::OK, |
| net::MockSourceStream::SYNC); |
| // Add an EOF. |
| source_stream->AddReadResult(base::span<uint8_t>(), net::OK, |
| net::MockSourceStream::SYNC); |
| std::unique_ptr<net::GzipSourceStream> filter = net::GzipSourceStream::Create( |
| std::move(source_stream), net::SourceStreamType::kGzip); |
| scoped_refptr<net::IOBufferWithSize> dest_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(4096); |
| while (true) { |
| int rv = filter->Read(dest_buffer.get(), dest_buffer->size(), |
| net::CompletionOnceCallback()); |
| ASSERT_LE(0, rv); |
| if (rv <= 0) { |
| break; |
| } |
| to_append->append(dest_buffer->data(), rv); |
| } |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // Queries for video input devices on the current system using the getSources |
| // API. |
| // |
| // This does not guarantee that a getUserMedia with video will succeed, as the |
| // camera could be busy for instance. |
| // |
| // Returns has-video-input-device to the test if there is a webcam available, |
| // no-video-input-devices otherwise. |
| const char kHasVideoInputDeviceOnSystem[] = R"( |
| (function() { |
| return navigator.mediaDevices.enumerateDevices() |
| .then(function(devices) { |
| if (devices.some((device) => device.kind == 'videoinput')) { |
| return 'has-video-input-device'; |
| } |
| return 'no-video-input-devices'; |
| }); |
| })() |
| )"; |
| |
| const char kHasVideoInputDevice[] = "has-video-input-device"; |
| |
| // Interceptor that replaces params.url with |new_url| and params.origin with |
| // |new_origin| for any commits to |target_url|. |
| class CommitOriginInterceptor : public DidCommitNavigationInterceptor { |
| public: |
| CommitOriginInterceptor(WebContents* web_contents, |
| const GURL& target_url, |
| const GURL& new_url, |
| const url::Origin& new_origin) |
| : DidCommitNavigationInterceptor(web_contents), |
| target_url_(target_url), |
| new_url_(new_url), |
| new_origin_(new_origin) {} |
| |
| CommitOriginInterceptor(const CommitOriginInterceptor&) = delete; |
| CommitOriginInterceptor& operator=(const CommitOriginInterceptor&) = delete; |
| |
| ~CommitOriginInterceptor() override = default; |
| |
| // WebContentsObserver: |
| void WebContentsDestroyed() override { delete this; } |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| if ((**params).url == target_url_) { |
| (**params).url = new_url_; |
| (**params).origin = new_origin_; |
| } |
| return true; |
| } |
| |
| private: |
| GURL target_url_; |
| GURL new_url_; |
| url::Origin new_origin_; |
| }; |
| |
| // Observer which waits for a visual update in a RenderWidgetHost to meet some |
| // desired conditions. |
| class ResizeObserver : public RenderWidgetHostObserver { |
| public: |
| ResizeObserver(RenderWidgetHost* widget_host, |
| base::RepeatingCallback<bool()> is_complete_callback) |
| : widget_host_(widget_host), |
| is_complete_callback_(std::move(is_complete_callback)) { |
| widget_host_->AddObserver(this); |
| } |
| |
| ~ResizeObserver() override { widget_host_->RemoveObserver(this); } |
| |
| // RenderWidgetHostObserver: |
| void RenderWidgetHostDidUpdateVisualProperties( |
| RenderWidgetHost* widget_host) override { |
| if (is_complete_callback_.Run()) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| void Wait() { run_loop_.Run(); } |
| |
| private: |
| raw_ptr<RenderWidgetHost> widget_host_; |
| base::RunLoop run_loop_; |
| base::RepeatingCallback<bool()> is_complete_callback_; |
| }; |
| |
| // Observer for RenderFrameProxyHost by setting itself through |
| // RenderFrameProxyHost::SetObserverForTesting. |
| class ProxyHostObserver : public RenderFrameProxyHost::TestObserver { |
| public: |
| using CreatedCallback = base::RepeatingCallback<void(RenderFrameProxyHost*)>; |
| |
| ProxyHostObserver() = default; |
| ~ProxyHostObserver() override = default; |
| |
| void Reset() { created_callback_ = CreatedCallback(); } |
| |
| void set_created_callback(CreatedCallback callback) { |
| created_callback_ = std::move(callback); |
| } |
| |
| private: |
| // RenderFrameProxyHost::TestObserver: |
| void OnCreated(RenderFrameProxyHost* rfph) override { |
| if (created_callback_) { |
| created_callback_.Run(rfph); |
| } |
| } |
| |
| // Callback which runs on RenderFrameProxyHost is created. |
| CreatedCallback created_callback_; |
| }; |
| |
| ProxyHostObserver* GetProxyHostObserver() { |
| static base::NoDestructor<ProxyHostObserver> observer; |
| return observer.get(); |
| } |
| |
| bool IsRequestCompatibleWithSpeculativeRFH(NavigationRequest* request) { |
| return request->state() <= |
| NavigationRequest::NavigationState::WILL_PROCESS_RESPONSE && |
| request->GetAssociatedRFHType() == |
| NavigationRequest::AssociatedRenderFrameHostType::NONE; |
| } |
| |
| } // namespace |
| |
| bool WaiterHelper::WaitInternal() { |
| if (event_received_) { |
| return true; |
| } |
| run_loop_.Run(); |
| return event_received_; |
| } |
| |
| bool WaiterHelper::Wait() { |
| bool result = WaitInternal(); |
| event_received_ = false; |
| return result; |
| } |
| |
| void WaiterHelper::OnEvent() { |
| event_received_ = true; |
| run_loop_.Quit(); |
| } |
| |
| bool NavigateToURL(WebContents* web_contents, const GURL& url) { |
| return NavigateToURL(web_contents, url, url); |
| } |
| |
| bool NavigateToURL(WebContents* web_contents, |
| const GURL& url, |
| const GURL& expected_commit_url) { |
| NavigateToURLBlockUntilNavigationsComplete( |
| web_contents, url, 1, |
| /*ignore_uncommitted_navigations=*/false); |
| if (!IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL)) { |
| return false; |
| } |
| |
| bool is_same_url = web_contents->GetLastCommittedURL() == expected_commit_url; |
| if (!is_same_url) { |
| DLOG(WARNING) << "Expected URL " << expected_commit_url << " but observed " |
| << web_contents->GetLastCommittedURL(); |
| } |
| return is_same_url; |
| } |
| |
| bool NavigateToURLFromRenderer(const ToRenderFrameHost& adapter, |
| const GURL& url) { |
| return NavigateToURLFromRenderer(adapter, url, url); |
| } |
| |
| bool NavigateToURLFromRenderer(const ToRenderFrameHost& adapter, |
| const GURL& url, |
| const GURL& expected_commit_url) { |
| RenderFrameHost* rfh = adapter.render_frame_host(); |
| TestFrameNavigationObserver nav_observer(rfh); |
| if (!BeginNavigateToURLFromRenderer(adapter, url)) { |
| return false; |
| } |
| nav_observer.Wait(); |
| return nav_observer.last_committed_url() == expected_commit_url && |
| nav_observer.last_navigation_succeeded(); |
| } |
| |
| bool NavigateToURLFromRendererWithoutUserGesture( |
| const ToRenderFrameHost& adapter, |
| const GURL& url) { |
| return NavigateToURLFromRendererWithoutUserGesture(adapter, url, url); |
| } |
| |
| bool NavigateToURLFromRendererWithoutUserGesture( |
| const ToRenderFrameHost& adapter, |
| const GURL& url, |
| const GURL& expected_commit_url) { |
| RenderFrameHost* rfh = adapter.render_frame_host(); |
| TestFrameNavigationObserver nav_observer(rfh); |
| if (!ExecJs(rfh, JsReplace("location = $1", url), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)) { |
| return false; |
| } |
| nav_observer.Wait(); |
| return nav_observer.last_committed_url() == expected_commit_url; |
| } |
| |
| bool BeginNavigateToURLFromRenderer(const ToRenderFrameHost& adapter, |
| const GURL& url) { |
| ExecuteScriptAsync(adapter, JsReplace("location = $1", url)); |
| DidStartNavigationObserver observer( |
| WebContents::FromRenderFrameHost(adapter.render_frame_host())); |
| observer.Wait(); |
| return observer.observed(); |
| } |
| |
| bool NavigateIframeToURL(WebContents* web_contents, |
| std::string_view iframe_id, |
| const GURL& url) { |
| TestNavigationObserver load_observer(web_contents); |
| bool result = BeginNavigateIframeToURL(web_contents, iframe_id, url); |
| load_observer.Wait(); |
| return result; |
| } |
| |
| bool BeginNavigateIframeToURL(WebContents* web_contents, |
| std::string_view iframe_id, |
| const GURL& url) { |
| std::string script = |
| base::StrCat({"setTimeout(\"var iframes = document.getElementById('", |
| iframe_id, "');iframes.src='", url.spec(), "';\",0)"}); |
| return ExecJs(web_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE); |
| } |
| |
| void NavigateToURLBlockUntilNavigationsComplete( |
| WebContents* web_contents, |
| const GURL& url, |
| int number_of_navigations, |
| bool ignore_uncommitted_navigations) { |
| // This mimics behavior of Shell::LoadURL... |
| NavigationController::LoadURLParams params(url); |
| params.transition_type = ui::PageTransitionFromInt( |
| ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| |
| NavigateToURLBlockUntilNavigationsComplete(web_contents, params, |
| number_of_navigations, |
| ignore_uncommitted_navigations); |
| } |
| |
| void NavigateToURLBlockUntilNavigationsComplete( |
| WebContents* web_contents, |
| const NavigationController::LoadURLParams& params, |
| int number_of_navigations, |
| bool ignore_uncommitted_navigations) { |
| // Prepare for the navigation. |
| WaitForLoadStop(web_contents); |
| TestNavigationObserver same_tab_observer( |
| web_contents, number_of_navigations, |
| MessageLoopRunner::QuitMode::IMMEDIATE, |
| /*ignore_uncommitted_navigations=*/ignore_uncommitted_navigations); |
| if (!blink::IsRendererDebugURL(params.url) && number_of_navigations == 1) { |
| same_tab_observer.set_expected_initial_url(params.url); |
| } |
| |
| web_contents->GetController().LoadURLWithParams(params); |
| web_contents->GetOutermostWebContents()->Focus(); |
| |
| // Wait until the expected number of navigations finish. |
| same_tab_observer.Wait(); |
| } |
| |
| GURL GetFileUrlWithQuery(const base::FilePath& path, |
| std::string_view query_string) { |
| GURL url = net::FilePathToFileURL(path); |
| if (!query_string.empty()) { |
| GURL::Replacements replacements; |
| replacements.SetQueryStr(query_string); |
| return url.ReplaceComponents(replacements); |
| } |
| return url; |
| } |
| |
| void ResetTouchAction(RenderWidgetHost* host) { |
| static_cast<input::InputRouterImpl*>( |
| static_cast<RenderWidgetHostImpl*>(host)->input_router()) |
| ->ForceResetTouchActionForTest(); |
| } |
| |
| void RunUntilInputProcessed(RenderWidgetHost* host) { |
| base::RunLoop run_loop; |
| RenderWidgetHostImpl::From(host)->WaitForInputProcessed( |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| std::string ReferrerPolicyToString( |
| network::mojom::ReferrerPolicy referrer_policy) { |
| switch (referrer_policy) { |
| case network::mojom::ReferrerPolicy::kDefault: |
| return "no-meta"; |
| case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade: |
| return "no-referrer-when-downgrade"; |
| case network::mojom::ReferrerPolicy::kOrigin: |
| return "origin"; |
| case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin: |
| return "origin-when-crossorigin"; |
| case network::mojom::ReferrerPolicy::kSameOrigin: |
| return "same-origin"; |
| case network::mojom::ReferrerPolicy::kStrictOrigin: |
| return "strict-origin"; |
| case network::mojom::ReferrerPolicy::kAlways: |
| return "always"; |
| case network::mojom::ReferrerPolicy::kNever: |
| return "never"; |
| case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin: |
| return "strict-origin-when-cross-origin"; |
| } |
| NOTREACHED(); |
| } |
| |
| mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> |
| BindFakeFrameWidgetInterfaces(RenderFrameHost* frame) { |
| RenderWidgetHostImpl* render_widget_host_impl = |
| static_cast<RenderFrameHostImpl*>(frame)->GetRenderWidgetHost(); |
| |
| mojo::AssociatedRemote<blink::mojom::FrameWidgetHost> blink_frame_widget_host; |
| auto blink_frame_widget_host_receiver = |
| blink_frame_widget_host.BindNewEndpointAndPassDedicatedReceiver(); |
| |
| mojo::AssociatedRemote<blink::mojom::FrameWidget> blink_frame_widget; |
| auto blink_frame_widget_receiver = |
| blink_frame_widget.BindNewEndpointAndPassDedicatedReceiver(); |
| |
| render_widget_host_impl->BindFrameWidgetInterfaces( |
| std::move(blink_frame_widget_host_receiver), blink_frame_widget.Unbind()); |
| |
| return blink_frame_widget_receiver; |
| } |
| |
| void SimulateActiveStateForWidget(RenderFrameHost* frame, bool active) { |
| static_cast<RenderFrameHostImpl*>(frame) |
| ->GetRenderWidgetHost() |
| ->delegate() |
| ->SendActiveState(active); |
| } |
| |
| std::optional<uint64_t> GetVisitedLinkSaltForNavigation( |
| NavigationHandle* navigation_handle) { |
| return static_cast<NavigationRequest*>(navigation_handle) |
| ->commit_params() |
| .visited_link_salt; |
| } |
| |
| void WaitForLoadStopWithoutSuccessCheck(WebContents* web_contents) { |
| // In many cases, the load may have finished before we get here. Only wait if |
| // the tab still has a pending navigation. |
| if (web_contents->IsLoading()) { |
| LoadStopObserver load_stop_observer(web_contents); |
| load_stop_observer.Wait(); |
| } |
| } |
| |
| bool IsLastCommittedPageNormal(WebContents* web_contents) { |
| bool is_page_normal = |
| IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL); |
| if (!is_page_normal) { |
| NavigationEntry* last_entry = |
| web_contents->GetController().GetLastCommittedEntry(); |
| if (last_entry) { |
| LOG(ERROR) << "Http status code = " << last_entry->GetHttpStatusCode() |
| << ", page type = " << last_entry->GetPageType(); |
| } else { |
| LOG(ERROR) << "No committed entry."; |
| } |
| } |
| return is_page_normal; |
| } |
| |
| bool WaitForLoadStop(WebContents* web_contents) { |
| TRACE_EVENT0("test", "content::WaitForLoadStop"); |
| WebContentsDestroyedWatcher watcher(web_contents); |
| WaitForLoadStopWithoutSuccessCheck(web_contents); |
| if (watcher.IsDestroyed()) { |
| LOG(ERROR) << "WebContents was destroyed during waiting for load stop."; |
| return false; |
| } |
| return IsLastCommittedPageNormal(web_contents); |
| } |
| |
| bool WaitForNavigationFinished(WebContents* web_contents, |
| TestNavigationObserver& observer) { |
| TRACE_EVENT0("test", "content::WaitForNavigationFinished"); |
| WebContentsDestroyedWatcher watcher(web_contents); |
| observer.WaitForNavigationFinished(); |
| if (watcher.IsDestroyed()) { |
| LOG(ERROR) |
| << "WebContents was destroyed during waiting for navigation finished."; |
| return false; |
| } |
| return IsLastCommittedPageNormal(web_contents); |
| } |
| |
| void PrepContentsForBeforeUnloadTest(WebContents* web_contents, |
| bool trigger_user_activation) { |
| web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost( |
| [trigger_user_activation](RenderFrameHost* render_frame_host) { |
| if (trigger_user_activation) { |
| render_frame_host->ExecuteJavaScriptWithUserGestureForTests( |
| std::u16string(), base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL); |
| } |
| |
| // Disable the hang monitor, otherwise there will be a race between |
| // the beforeunload dialog and the beforeunload hang timer. |
| render_frame_host->DisableBeforeUnloadHangMonitorForTesting(); |
| }); |
| } |
| |
| bool IsLastCommittedEntryOfPageType(WebContents* web_contents, |
| PageType page_type) { |
| NavigationEntry* last_entry = |
| web_contents->GetController().GetLastCommittedEntry(); |
| return last_entry && last_entry->GetPageType() == page_type; |
| } |
| |
| void OverrideLastCommittedOrigin(RenderFrameHost* render_frame_host, |
| const url::Origin& origin) { |
| static_cast<RenderFrameHostImpl*>(render_frame_host) |
| ->SetLastCommittedOriginForTesting(origin); |
| } |
| |
| void CrashTab(WebContents* web_contents) { |
| RenderProcessHost* rph = web_contents->GetPrimaryMainFrame()->GetProcess(); |
| RenderProcessHostWatcher watcher( |
| rph, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| EXPECT_TRUE(rph->Shutdown(RESULT_CODE_KILLED)); |
| watcher.Wait(); |
| EXPECT_FALSE(watcher.did_exit_normally()); |
| EXPECT_TRUE(web_contents->IsCrashed()); |
| } |
| |
| void SimulateUnresponsivePrimaryMainFrameAndWaitForExit( |
| WebContents* web_contents) { |
| RenderProcessHost* rph = web_contents->GetPrimaryMainFrame()->GetProcess(); |
| RenderProcessHostWatcher watcher( |
| rph, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| |
| SimulateUnresponsiveRenderer( |
| web_contents, web_contents->GetPrimaryMainFrame()->GetRenderWidgetHost()); |
| |
| EXPECT_TRUE(rph->Shutdown(RESULT_CODE_HUNG)); |
| watcher.Wait(); |
| EXPECT_FALSE(watcher.did_exit_normally()); |
| EXPECT_TRUE(web_contents->IsCrashed()); |
| } |
| |
| void PwnCommitIPC(WebContents* web_contents, |
| const GURL& target_url, |
| const GURL& new_url, |
| const url::Origin& new_origin) { |
| // This will be cleaned up when |web_contents| is destroyed. |
| new CommitOriginInterceptor(web_contents, target_url, new_url, new_origin); |
| } |
| |
| bool CanCommitURLForTesting(int child_id, const GURL& url) { |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| return policy->CanCommitURL(child_id, url); |
| } |
| |
| void SimulateUnresponsiveRenderer(WebContents* web_contents, |
| RenderWidgetHost* widget) { |
| static_cast<WebContentsImpl*>(web_contents) |
| ->RendererUnresponsive(RenderWidgetHostImpl::From(widget), |
| base::DoNothing()); |
| } |
| |
| #if defined(USE_AURA) |
| bool IsResizeComplete(aura::test::WindowEventDispatcherTestApi* dispatcher_test, |
| RenderWidgetHostImpl* widget_host) { |
| dispatcher_test->WaitUntilPointerMovesDispatched(); |
| widget_host->SynchronizeVisualProperties(); |
| return !widget_host->visual_properties_ack_pending_for_testing(); |
| } |
| |
| void WaitForResizeComplete(WebContents* web_contents) { |
| aura::Window* content = web_contents->GetContentNativeView(); |
| if (!content) { |
| return; |
| } |
| |
| aura::WindowTreeHost* window_host = content->GetHost(); |
| aura::WindowEventDispatcher* dispatcher = window_host->dispatcher(); |
| aura::test::WindowEventDispatcherTestApi dispatcher_test(dispatcher); |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget()); |
| if (!IsResizeComplete(&dispatcher_test, widget_host)) { |
| ResizeObserver resize_observer( |
| widget_host, |
| base::BindRepeating(IsResizeComplete, &dispatcher_test, widget_host)); |
| resize_observer.Wait(); |
| } |
| } |
| #elif BUILDFLAG(IS_ANDROID) |
| bool IsResizeComplete(RenderWidgetHostImpl* widget_host) { |
| return !widget_host->visual_properties_ack_pending_for_testing(); |
| } |
| |
| void WaitForResizeComplete(WebContents* web_contents) { |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetRenderViewHost()->GetWidget()); |
| if (!IsResizeComplete(widget_host)) { |
| ResizeObserver resize_observer( |
| widget_host, base::BindRepeating(IsResizeComplete, widget_host)); |
| resize_observer.Wait(); |
| } |
| } |
| #endif |
| |
| void NotifyCopyableViewInWebContents(WebContents* web_contents, |
| base::OnceClosure done_callback) { |
| NotifyCopyableViewInFrame(web_contents->GetPrimaryMainFrame(), |
| std::move(done_callback)); |
| } |
| |
| void NotifyCopyableViewInFrame(RenderFrameHost* render_frame_host, |
| base::OnceClosure done_callback) { |
| RenderWidgetHostImpl* rwhi = static_cast<RenderWidgetHostImpl*>( |
| render_frame_host->GetView()->GetRenderWidgetHost()); |
| |
| // Note: this function intentionally avoids using RunLoops, which would make |
| // the code easier to read, so that it can be used on Android which doesn't |
| // support nested run loops. |
| |
| auto first_frame_done = base::BindOnce( |
| [](base::WeakPtr<RenderWidgetHostImpl> rwhi, |
| base::OnceClosure done_callback, bool success) { |
| // This is invoked when the first `CompositorFrame` is submitted from |
| // the renderer to the GPU. However, we want to wait until the Viz |
| // process has received the new `CompositorFrame` so that the previously |
| // submitted frame is available for copy. Waiting for a second frame to |
| // be submitted guarantees this, since the second frame cannot be sent |
| // until the first frame was ACKed by Viz. |
| |
| if (!rwhi || !success) { |
| std::move(done_callback).Run(); |
| return; |
| } |
| |
| // Force a redraw to ensure the callback below goes through the complete |
| // compositing pipeline. |
| rwhi->ForceRedrawForTesting(); |
| rwhi->InsertVisualStateCallback(base::BindOnce( |
| [](base::WeakPtr<RenderWidgetHostImpl> rwhi, |
| base::OnceClosure final_done_callback, bool success) { |
| if (rwhi) { |
| // `IsSurfaceAvailableForCopy` actually only checks if the |
| // browser currently embeds a surface or not (as opposed to |
| // sending a IPC to the GPU). However if the browser does not |
| // embed any surface, we won't be able to issue any copy |
| // requests. |
| ASSERT_TRUE(rwhi->GetView()->IsSurfaceAvailableForCopy()); |
| } |
| std::move(final_done_callback).Run(); |
| }, |
| rwhi->GetWeakPtr(), std::move(done_callback))); |
| }, |
| rwhi->GetWeakPtr(), std::move(done_callback)); |
| |
| rwhi->InsertVisualStateCallback(std::move(first_frame_done)); |
| } |
| |
| // TODO(crbug.com/40278950): Use |
| // `WebFrameWidgetImpl::NotifySwapAndPresentationTime` instead. |
| void WaitForCopyableViewInWebContents(WebContents* web_contents) { |
| WaitForCopyableViewInFrame(web_contents->GetPrimaryMainFrame()); |
| } |
| |
| void WaitForCopyableViewInFrame(RenderFrameHost* render_frame_host) { |
| base::test::TestFuture<void> future; |
| NotifyCopyableViewInFrame(render_frame_host, future.GetCallback()); |
| CHECK(future.Wait()); |
| } |
| |
| void SimulateEndOfPaintHoldingOnPrimaryMainFrame(WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| RenderWidgetHostImpl* main_frame_rwh = |
| web_contents_impl->GetPrimaryMainFrame()->GetRenderWidgetHost(); |
| main_frame_rwh->ForceFirstFrameAfterNavigationTimeout(); |
| } |
| |
| void SimulateMouseClick(WebContents* web_contents, |
| int modifiers, |
| blink::WebMouseEvent::Button button) { |
| int x = web_contents->GetContainerBounds().width() / 2; |
| int y = web_contents->GetContainerBounds().height() / 2; |
| SimulateMouseClickAt(web_contents, modifiers, button, gfx::Point(x, y)); |
| } |
| |
| void SimulateMouseClickAt(WebContents* web_contents, |
| int modifiers, |
| blink::WebMouseEvent::Button button, |
| const gfx::Point& point) { |
| auto* web_contents_impl = static_cast<WebContentsImpl*>(web_contents); |
| auto* rwhvb = static_cast<RenderWidgetHostViewBase*>( |
| web_contents->GetRenderWidgetHostView()); |
| blink::WebMouseEvent mouse_event(blink::WebInputEvent::Type::kMouseDown, |
| modifiers, ui::EventTimeForNow()); |
| mouse_event.button = button; |
| mouse_event.SetPositionInWidget(point.x(), point.y()); |
| // Mac needs positionInScreen for events to plugins. |
| gfx::Rect offset = web_contents->GetContainerBounds(); |
| mouse_event.SetPositionInScreen(point.x() + offset.x(), |
| point.y() + offset.y()); |
| mouse_event.click_count = 1; |
| web_contents_impl->GetInputEventRouter()->RouteMouseEvent(rwhvb, &mouse_event, |
| ui::LatencyInfo()); |
| mouse_event.SetType(blink::WebInputEvent::Type::kMouseUp); |
| web_contents_impl->GetInputEventRouter()->RouteMouseEvent(rwhvb, &mouse_event, |
| ui::LatencyInfo()); |
| } |
| |
| gfx::PointF GetCenterCoordinatesOfElementWithId( |
| const ToRenderFrameHost& adapter, |
| std::string_view id) { |
| float x = |
| EvalJs(adapter, JsReplace("const bounds = " |
| "document.getElementById($1)." |
| "getBoundingClientRect();" |
| "Math.floor(bounds.left + bounds.width / 2)", |
| id)) |
| .ExtractDouble(); |
| float y = |
| EvalJs(adapter, JsReplace("const bounds = " |
| "document.getElementById($1)." |
| "getBoundingClientRect();" |
| "Math.floor(bounds.top + bounds.height / 2)", |
| id)) |
| .ExtractDouble(); |
| return gfx::PointF(x, y); |
| } |
| |
| void SimulateMouseClickOrTapElementWithId(WebContents* web_contents, |
| std::string_view id) { |
| gfx::Point point = gfx::ToFlooredPoint( |
| GetCenterCoordinatesOfElementWithId(web_contents, id)); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| SimulateTapDownAt(web_contents, point); |
| SimulateTapAt(web_contents, point); |
| #else |
| SimulateMouseClickAt(web_contents, 0, blink::WebMouseEvent::Button::kLeft, |
| point); |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| void SimulateMouseEvent(WebContents* web_contents, |
| blink::WebInputEvent::Type type, |
| const gfx::Point& point) { |
| SimulateMouseEvent(web_contents, type, |
| blink::WebMouseEvent::Button::kNoButton, point); |
| } |
| |
| void SimulateMouseEvent(WebContents* web_contents, |
| blink::WebInputEvent::Type type, |
| blink::WebMouseEvent::Button button, |
| const gfx::Point& point) { |
| auto* web_contents_impl = static_cast<WebContentsImpl*>(web_contents); |
| auto* rwhvb = static_cast<RenderWidgetHostViewBase*>( |
| web_contents->GetRenderWidgetHostView()); |
| blink::WebMouseEvent mouse_event(type, 0, ui::EventTimeForNow()); |
| mouse_event.button = button; |
| mouse_event.SetPositionInWidget(point.x(), point.y()); |
| // Mac needs positionInScreen for events to plugins. |
| gfx::Rect offset = web_contents->GetContainerBounds(); |
| mouse_event.SetPositionInScreen(point.x() + offset.x(), |
| point.y() + offset.y()); |
| |
| web_contents_impl->GetInputEventRouter()->RouteMouseEvent(rwhvb, &mouse_event, |
| ui::LatencyInfo()); |
| } |
| |
| void SimulateMouseWheelEvent(WebContents* web_contents, |
| const gfx::Point& point, |
| const gfx::Vector2d& delta, |
| const blink::WebMouseWheelEvent::Phase phase) { |
| blink::WebMouseWheelEvent wheel_event(blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kNoModifiers, |
| ui::EventTimeForNow()); |
| |
| wheel_event.SetPositionInWidget(point.x(), point.y()); |
| wheel_event.delta_x = delta.x(); |
| wheel_event.delta_y = delta.y(); |
| wheel_event.phase = phase; |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget()); |
| widget_host->ForwardWheelEvent(wheel_event); |
| } |
| |
| #if !BUILDFLAG(IS_MAC) |
| void SimulateMouseWheelCtrlZoomEvent(RenderWidgetHost* render_widget_host, |
| const gfx::Point& point, |
| bool zoom_in, |
| blink::WebMouseWheelEvent::Phase phase) { |
| blink::WebMouseWheelEvent wheel_event(blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kControlKey, |
| ui::EventTimeForNow()); |
| |
| wheel_event.SetPositionInWidget(point.x(), point.y()); |
| wheel_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel; |
| wheel_event.delta_y = |
| (zoom_in ? 1.0 : -1.0) * ui::MouseWheelEvent::kWheelDelta; |
| wheel_event.wheel_ticks_y = (zoom_in ? 1.0 : -1.0); |
| wheel_event.phase = phase; |
| RenderWidgetHostImpl* widget_host = |
| RenderWidgetHostImpl::From(render_widget_host); |
| widget_host->ForwardWheelEvent(wheel_event); |
| } |
| |
| void SimulateTouchscreenPinch(WebContents* web_contents, |
| const gfx::PointF& anchor, |
| float scale_change, |
| base::OnceClosure on_complete) { |
| SyntheticPinchGestureParams params; |
| params.gesture_source_type = mojom::GestureSourceType::kTouchInput; |
| params.scale_factor = scale_change; |
| params.anchor = anchor; |
| |
| auto pinch_gesture = |
| std::make_unique<SyntheticTouchscreenPinchGesture>(params); |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetTopLevelRenderWidgetHostView()->GetRenderWidgetHost()); |
| widget_host->QueueSyntheticGesture( |
| std::move(pinch_gesture), |
| base::BindOnce( |
| [](base::OnceClosure on_complete, SyntheticGesture::Result result) { |
| std::move(on_complete).Run(); |
| }, |
| std::move(on_complete))); |
| } |
| |
| #endif // !BUILDFLAG(IS_MAC) |
| |
| void SimulateGesturePinchSequence(RenderWidgetHost* render_widget_host, |
| const gfx::Point& point, |
| float scale, |
| blink::WebGestureDevice source_device) { |
| RenderWidgetHostImpl* widget_host = |
| RenderWidgetHostImpl::From(render_widget_host); |
| |
| blink::WebGestureEvent pinch_begin( |
| blink::WebInputEvent::Type::kGesturePinchBegin, |
| blink::WebInputEvent::kNoModifiers, ui::EventTimeForNow(), source_device); |
| pinch_begin.SetPositionInWidget(gfx::PointF(point)); |
| pinch_begin.SetPositionInScreen(gfx::PointF(point)); |
| pinch_begin.SetNeedsWheelEvent(source_device == |
| blink::WebGestureDevice::kTouchpad); |
| widget_host->ForwardGestureEvent(pinch_begin); |
| |
| blink::WebGestureEvent pinch_update(pinch_begin); |
| pinch_update.SetType(blink::WebInputEvent::Type::kGesturePinchUpdate); |
| pinch_update.data.pinch_update.scale = scale; |
| pinch_update.SetNeedsWheelEvent(source_device == |
| blink::WebGestureDevice::kTouchpad); |
| widget_host->ForwardGestureEvent(pinch_update); |
| |
| blink::WebGestureEvent pinch_end(pinch_begin); |
| pinch_end.SetType(blink::WebInputEvent::Type::kGesturePinchEnd); |
| pinch_end.SetNeedsWheelEvent(source_device == |
| blink::WebGestureDevice::kTouchpad); |
| widget_host->ForwardGestureEvent(pinch_end); |
| } |
| |
| void SimulateGesturePinchSequence(WebContents* web_contents, |
| const gfx::Point& point, |
| float scale, |
| blink::WebGestureDevice source_device) { |
| RenderWidgetHost* widget_host = |
| web_contents->GetPrimaryMainFrame()->GetRenderWidgetHost(); |
| SimulateGesturePinchSequence(widget_host, point, scale, source_device); |
| } |
| |
| void SimulateGestureScrollSequence(RenderWidgetHost* render_widget_host, |
| const gfx::Point& point, |
| const gfx::Vector2dF& delta) { |
| blink::WebGestureEvent scroll_begin( |
| blink::WebGestureEvent::Type::kGestureScrollBegin, |
| blink::WebInputEvent::kNoModifiers, ui::EventTimeForNow(), |
| blink::WebGestureDevice::kTouchpad); |
| scroll_begin.SetPositionInWidget(gfx::PointF(point)); |
| scroll_begin.data.scroll_begin.delta_x_hint = delta.x(); |
| scroll_begin.data.scroll_begin.delta_y_hint = delta.y(); |
| render_widget_host->ForwardGestureEvent(scroll_begin); |
| |
| blink::WebGestureEvent scroll_update( |
| blink::WebGestureEvent::Type::kGestureScrollUpdate, |
| blink::WebInputEvent::kNoModifiers, ui::EventTimeForNow(), |
| blink::WebGestureDevice::kTouchpad); |
| scroll_update.SetPositionInWidget(gfx::PointF(point)); |
| scroll_update.data.scroll_update.delta_x = delta.x(); |
| scroll_update.data.scroll_update.delta_y = delta.y(); |
| render_widget_host->ForwardGestureEvent(scroll_update); |
| |
| blink::WebGestureEvent scroll_end( |
| blink::WebGestureEvent::Type::kGestureScrollEnd, |
| blink::WebInputEvent::kNoModifiers, ui::EventTimeForNow(), |
| blink::WebGestureDevice::kTouchpad); |
| scroll_end.SetPositionInWidget(gfx::PointF(point)); |
| render_widget_host->ForwardGestureEvent(scroll_end); |
| } |
| |
| void SimulateGestureScrollSequence(WebContents* web_contents, |
| const gfx::Point& point, |
| const gfx::Vector2dF& delta) { |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetPrimaryMainFrame()->GetRenderWidgetHost()); |
| |
| SimulateGestureScrollSequence(widget_host, point, delta); |
| } |
| |
| void SimulateGestureEvent(RenderWidgetHost* render_widget_host, |
| const blink::WebGestureEvent& gesture_event, |
| const ui::LatencyInfo& latency) { |
| RenderWidgetHostViewBase* view = |
| static_cast<RenderWidgetHostViewBase*>(render_widget_host->GetView()); |
| view->ProcessGestureEvent(gesture_event, latency); |
| } |
| |
| void SimulateGestureEvent(WebContents* web_contents, |
| const blink::WebGestureEvent& gesture_event, |
| const ui::LatencyInfo& latency) { |
| RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>( |
| web_contents->GetRenderWidgetHostView()); |
| view->ProcessGestureEvent(gesture_event, latency); |
| } |
| |
| void SimulateTouchGestureAt(WebContents* web_contents, |
| const gfx::Point& point, |
| blink::WebInputEvent::Type type) { |
| blink::WebGestureEvent gesture(type, 0, ui::EventTimeForNow(), |
| blink::WebGestureDevice::kTouchscreen); |
| gesture.SetPositionInWidget(gfx::PointF(point)); |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget()); |
| widget_host->ForwardGestureEvent(gesture); |
| } |
| |
| void SimulateTapDownAt(WebContents* web_contents, const gfx::Point& point) { |
| SimulateTouchGestureAt(web_contents, point, |
| blink::WebGestureEvent::Type::kGestureTapDown); |
| } |
| |
| void SimulateTapAt(WebContents* web_contents, const gfx::Point& point) { |
| SimulateTouchGestureAt(web_contents, point, |
| blink::WebGestureEvent::Type::kGestureTap); |
| } |
| |
| void SimulateTapWithModifiersAt(WebContents* web_contents, |
| unsigned modifiers, |
| const gfx::Point& point) { |
| blink::WebGestureEvent tap(blink::WebGestureEvent::Type::kGestureTap, |
| modifiers, ui::EventTimeForNow(), |
| blink::WebGestureDevice::kTouchpad); |
| tap.SetPositionInWidget(gfx::PointF(point)); |
| RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From( |
| web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget()); |
| widget_host->ForwardGestureEvent(tap); |
| } |
| |
| #if defined(USE_AURA) |
| void SimulateTouchEventAt(WebContents* web_contents, |
| ui::EventType event_type, |
| const gfx::Point& point) { |
| ui::TouchEvent touch(event_type, point, base::TimeTicks(), |
| ui::PointerDetails(ui::EventPointerType::kTouch, 0)); |
| static_cast<RenderWidgetHostViewAura*>( |
| web_contents->GetRenderWidgetHostView()) |
| ->OnTouchEvent(&touch); |
| } |
| |
| void SimulateLongTapAt(WebContents* web_contents, const gfx::Point& point) { |
| RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>( |
| web_contents->GetRenderWidgetHostView()); |
| |
| ui::TouchEvent touch_start( |
| ui::EventType::kTouchPressed, point, base::TimeTicks(), |
| ui::PointerDetails(ui::EventPointerType::kTouch, 0)); |
| rwhva->OnTouchEvent(&touch_start); |
| |
| ui::GestureEventDetails tap_down_details(ui::EventType::kGestureTapDown); |
| tap_down_details.set_device_type(ui::GestureDeviceType::DEVICE_TOUCHSCREEN); |
| ui::GestureEvent tap_down(point.x(), point.y(), 0, ui::EventTimeForNow(), |
| tap_down_details, touch_start.unique_event_id()); |
| rwhva->OnGestureEvent(&tap_down); |
| |
| ui::GestureEventDetails long_press_details(ui::EventType::kGestureLongPress); |
| long_press_details.set_device_type(ui::GestureDeviceType::DEVICE_TOUCHSCREEN); |
| ui::GestureEvent long_press(point.x(), point.y(), 0, ui::EventTimeForNow(), |
| long_press_details, |
| touch_start.unique_event_id()); |
| rwhva->OnGestureEvent(&long_press); |
| |
| ui::TouchEvent touch_end(ui::EventType::kTouchReleased, point, |
| base::TimeTicks(), |
| ui::PointerDetails(ui::EventPointerType::kTouch, 0)); |
| rwhva->OnTouchEvent(&touch_end); |
| |
| ui::GestureEventDetails long_tap_details(ui::EventType::kGestureLongTap); |
| long_tap_details.set_device_type(ui::GestureDeviceType::DEVICE_TOUCHSCREEN); |
| ui::GestureEvent long_tap(point.x(), point.y(), 0, ui::EventTimeForNow(), |
| long_tap_details, touch_end.unique_event_id()); |
| rwhva->OnGestureEvent(&long_tap); |
| } |
| |
| // Observer which waits for the selection bounds in a RenderWidgetHostViewAura |
| // to meet some desired conditions. |
| class SelectionBoundsWaiter : public TextInputManager::Observer { |
| public: |
| using Predicate = base::RepeatingCallback<bool()>; |
| |
| SelectionBoundsWaiter(RenderWidgetHostViewAura* rwhva, Predicate predicate) |
| : predicate_(std::move(predicate)) { |
| text_input_manager_observation_.Observe(rwhva->GetTextInputManager()); |
| } |
| SelectionBoundsWaiter(const SelectionBoundsWaiter&) = delete; |
| SelectionBoundsWaiter& operator=(const SelectionBoundsWaiter&) = delete; |
| virtual ~SelectionBoundsWaiter() = default; |
| |
| // TextInputManager::Observer: |
| void OnSelectionBoundsChanged( |
| TextInputManager* text_input_manager, |
| RenderWidgetHostViewBase* updated_view) override { |
| if (predicate_.Run()) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| void Wait() { run_loop_.Run(); } |
| |
| private: |
| base::RunLoop run_loop_; |
| Predicate predicate_; |
| base::ScopedObservation<TextInputManager, TextInputManager::Observer> |
| text_input_manager_observation_{this}; |
| }; |
| |
| NonZeroCaretSizeWaiter::NonZeroCaretSizeWaiter(WebContents* web_contents) { |
| RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>( |
| web_contents->GetRenderWidgetHostView()); |
| selection_bounds_waiter_ = std::make_unique<SelectionBoundsWaiter>( |
| rwhva, base::BindLambdaForTesting([rwhva]() { |
| return !rwhva->GetCaretBounds().size().IsZero(); |
| })); |
| } |
| |
| NonZeroCaretSizeWaiter::~NonZeroCaretSizeWaiter() = default; |
| |
| void NonZeroCaretSizeWaiter::Wait() { |
| selection_bounds_waiter_->Wait(); |
| } |
| |
| CaretBoundsUpdateWaiter::CaretBoundsUpdateWaiter(WebContents* web_contents) { |
| RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>( |
| web_contents->GetRenderWidgetHostView()); |
| const gfx::Rect current_caret_bounds = rwhva->GetCaretBounds(); |
| selection_bounds_waiter_ = std::make_unique<SelectionBoundsWaiter>( |
| rwhva, base::BindLambdaForTesting([rwhva, current_caret_bounds]() { |
| return rwhva->GetCaretBounds() != current_caret_bounds; |
| })); |
| } |
| |
| CaretBoundsUpdateWaiter::~CaretBoundsUpdateWaiter() = default; |
| |
| void CaretBoundsUpdateWaiter::Wait() { |
| selection_bounds_waiter_->Wait(); |
| } |
| |
| BoundingBoxUpdateWaiter::BoundingBoxUpdateWaiter(WebContents* web_contents) { |
| RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>( |
| web_contents->GetRenderWidgetHostView()); |
| const gfx::Rect current_bounding_box = rwhva->GetSelectionBoundingBox(); |
| selection_bounds_waiter_ = std::make_unique<SelectionBoundsWaiter>( |
| rwhva, base::BindLambdaForTesting([rwhva, current_bounding_box]() { |
| return rwhva->GetSelectionBoundingBox() != current_bounding_box; |
| })); |
| } |
| |
| BoundingBoxUpdateWaiter::~BoundingBoxUpdateWaiter() = default; |
| |
| void BoundingBoxUpdateWaiter::Wait() { |
| selection_bounds_waiter_->Wait(); |
| } |
| #endif |
| |
| double GetPendingZoomLevel(RenderWidgetHost* render_widget_host) { |
| auto* rwhi = static_cast<RenderWidgetHostImpl*>(render_widget_host); |
| return WebContentsImpl::FromRenderWidgetHostImpl(rwhi)->GetPendingZoomLevel( |
| rwhi); |
| } |
| |
| void SimulateKeyPress(WebContents* web_contents, |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code, |
| bool control, |
| bool shift, |
| bool alt, |
| bool command) { |
| SimulateKeyPressImpl(web_contents, key, code, key_code, control, shift, alt, |
| command, /*send_char=*/true); |
| } |
| |
| void SimulateKeyPressWithoutChar(WebContents* web_contents, |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code, |
| bool control, |
| bool shift, |
| bool alt, |
| bool command) { |
| SimulateKeyPressImpl(web_contents, key, code, key_code, control, shift, alt, |
| command, /*send_char=*/false); |
| } |
| |
| void SimulateProxyHostPostMessage(RenderFrameHost* source_render_frame_host, |
| RenderFrameHost* target_render_frame_host, |
| blink::TransferableMessage message) { |
| RenderFrameProxyHost* proxy_host = |
| static_cast<RenderFrameHostImpl*>(target_render_frame_host) |
| ->browsing_context_state() |
| ->GetRenderFrameProxyHost( |
| static_cast<SiteInstanceImpl*>( |
| source_render_frame_host->GetSiteInstance()) |
| ->group()); |
| CHECK(proxy_host); |
| |
| proxy_host->RouteMessageEvent( |
| source_render_frame_host->GetFrameToken(), |
| source_render_frame_host->GetLastCommittedOrigin(), |
| base::UTF8ToUTF16( |
| target_render_frame_host->GetLastCommittedOrigin().Serialize()), |
| std::move(message)); |
| } |
| |
| ScopedSimulateModifierKeyPress::ScopedSimulateModifierKeyPress( |
| WebContents* web_contents, |
| bool control, |
| bool shift, |
| bool alt, |
| bool command) |
| : web_contents_(web_contents), |
| modifiers_(0), |
| control_(control), |
| shift_(shift), |
| alt_(alt), |
| command_(command) { |
| modifiers_ = |
| SimulateModifierKeysDown(web_contents_, control_, shift_, alt_, command_); |
| } |
| |
| ScopedSimulateModifierKeyPress::~ScopedSimulateModifierKeyPress() { |
| modifiers_ = SimulateModifierKeysUp(web_contents_, control_, shift_, alt_, |
| command_, modifiers_); |
| DCHECK_EQ(0, modifiers_); |
| } |
| |
| void ScopedSimulateModifierKeyPress::MouseClickAt( |
| int additional_modifiers, |
| blink::WebMouseEvent::Button button, |
| const gfx::Point& point) { |
| SimulateMouseClickAt(web_contents_, modifiers_ | additional_modifiers, button, |
| point); |
| } |
| |
| void ScopedSimulateModifierKeyPress::KeyPress(ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code) { |
| SimulateKeyEvent(web_contents_, key, code, key_code, /*send_char=*/true, |
| modifiers_); |
| } |
| |
| void ScopedSimulateModifierKeyPress::KeyPressWithoutChar( |
| ui::DomKey key, |
| ui::DomCode code, |
| ui::KeyboardCode key_code) { |
| SimulateKeyEvent(web_contents_, key, code, key_code, /*send_char=*/false, |
| modifiers_); |
| } |
| |
| bool IsWebcamAvailableOnSystem(WebContents* web_contents) { |
| return EvalJs(web_contents, kHasVideoInputDeviceOnSystem).ExtractString() == |
| kHasVideoInputDevice; |
| } |
| |
| RenderFrameHost* ConvertToRenderFrameHost(WebContents* web_contents) { |
| return web_contents->GetPrimaryMainFrame(); |
| } |
| |
| RenderFrameHost* ConvertToRenderFrameHost(RenderFrameHost* render_frame_host) { |
| return render_frame_host; |
| } |
| |
| void ExecuteScriptAsync(const ToRenderFrameHost& adapter, |
| std::string_view script) { |
| // Prerendering pages will never have user gesture. |
| if (adapter.render_frame_host()->GetLifecycleState() == |
| RenderFrameHost::LifecycleState::kPrerendering) { |
| ExecuteScriptAsyncWithoutUserGesture(adapter, script); |
| } else { |
| adapter.render_frame_host()->ExecuteJavaScriptWithUserGestureForTests( |
| base::UTF8ToUTF16(script), base::NullCallback(), |
| ISOLATED_WORLD_ID_GLOBAL); |
| } |
| } |
| |
| void ExecuteScriptAsyncWithoutUserGesture(const ToRenderFrameHost& adapter, |
| std::string_view script) { |
| adapter.render_frame_host()->ExecuteJavaScriptForTests( |
| base::UTF8ToUTF16(script), base::NullCallback(), |
| ISOLATED_WORLD_ID_GLOBAL); |
| } |
| |
| // EvalJsResult methods. |
| EvalJsResult::EvalJsResult(base::Value value, std::string_view error) |
| : data_(error.empty() ? std::variant<std::string, base::Value>( |
| std::in_place_type_t<base::Value>(), |
| std::move(value)) |
| : std::variant<std::string, base::Value>( |
| std::in_place_type_t<std::string>(), |
| error)) {} |
| |
| EvalJsResult::EvalJsResult(const EvalJsResult& other) |
| : data_(std::visit( |
| absl::Overload{ |
| [](const std::string& error) |
| -> std::variant<std::string, base::Value> { return error; }, |
| [](const base::Value& value) |
| -> std::variant<std::string, base::Value> { |
| return value.Clone(); |
| }, |
| }, |
| other.data_)) {} |
| |
| EvalJsResult& EvalJsResult::operator=(const EvalJsResult& other) { |
| data_ = std::visit( |
| absl::Overload{ |
| [](const std::string& error) |
| -> std::variant<std::string, base::Value> { return error; }, |
| [](const base::Value& value) |
| -> std::variant<std::string, base::Value> { |
| return value.Clone(); |
| }, |
| }, |
| other.data_); |
| return *this; |
| } |
| |
| EvalJsResult::EvalJsResult(EvalJsResult&&) = default; |
| |
| EvalJsResult& EvalJsResult::operator=(EvalJsResult&&) = default; |
| |
| EvalJsResult::~EvalJsResult() = default; |
| |
| const std::string& EvalJsResult::ExtractString() const { |
| CHECK(is_ok()) |
| << "Can't ExtractString() because the script encountered a problem: " |
| << *error(); |
| CHECK(value()->is_string()) |
| << "Can't ExtractString() because script result: " << *value() |
| << "is not a string."; |
| return value()->GetString(); |
| } |
| |
| int EvalJsResult::ExtractInt() const { |
| CHECK(is_ok()) |
| << "Can't ExtractInt() because the script encountered a problem: " |
| << *error(); |
| CHECK(value()->is_int()) << "Can't ExtractInt() because script result: " |
| << *value() << "is not an int."; |
| return value()->GetInt(); |
| } |
| |
| bool EvalJsResult::ExtractBool() const { |
| CHECK(is_ok()) |
| << "Can't ExtractBool() because the script encountered a problem: " |
| << *error(); |
| CHECK(value()->is_bool()) |
| << "Can't ExtractBool() because script result: " << *value() |
| << "is not a bool."; |
| return value()->GetBool(); |
| } |
| |
| double EvalJsResult::ExtractDouble() const { |
| CHECK(is_ok()) |
| << "Can't ExtractDouble() because the script encountered a problem: " |
| << *error(); |
| CHECK(value()->is_double() || value()->is_int()) |
| << "Can't ExtractDouble() because script result: " << *value() |
| << "is not a double or int."; |
| return value()->GetDouble(); |
| } |
| |
| const base::Value::List& EvalJsResult::ExtractList() const { |
| CHECK(is_ok()) |
| << "Can't ExtractList() because the script encountered a problem: " |
| << *error(); |
| CHECK(value()->is_list()) |
| << "Can't ExtractList() because script result: " << *value() |
| << "is not a list."; |
| return value()->GetList(); |
| } |
| |
| const base::Value::Dict& EvalJsResult::ExtractDict() const { |
| CHECK(is_ok()) |
| << "Can't ExtractDict() because the script encountered a problem: " |
| << *error(); |
| CHECK(value()->is_dict()) |
| << "Can't ExtractDict() because script result: " << *value() |
| << "is not a dictionary."; |
| return value()->GetDict(); |
| } |
| |
| const std::string& EvalJsResult::ExtractError() const { |
| CHECK(!is_ok()) << "Can't ExtractError() because the script did not fail: " |
| << *value(); |
| return error().value(); |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const EvalJsResult& bar) { |
| if (!bar.is_ok()) { |
| os << bar.ExtractError(); |
| } else { |
| os << *bar.value(); |
| } |
| return os; |
| } |
| |
| namespace { |
| |
| // Parse a JS stack trace out of |js_error|, detect frames that match |
| // |source_name|, and interleave the appropriate lines of source code from |
| // |source| into the error report. This is meant to be useful for scripts that |
| // are passed to ExecJs/EvalJs functions, and hence dynamically generated. |
| // |
| // An adjustment of |column_adjustment_for_line_one| characters is subtracted |
| // when mapping positions from line 1 of |source|. This is to offset the effect |
| // of boilerplate added by the script runner. |
| // |
| // TODO(nick): Elide snippets to 80 chars, since it is common for sources to not |
| // include newlines. |
| std::string AnnotateAndAdjustJsStackTraces(std::string_view js_error, |
| std::string source_name, |
| std::string_view source, |
| int column_adjustment_for_line_one) { |
| // Escape wildcards in |source_name| for use in MatchPattern. |
| base::ReplaceChars(source_name, "\\", "\\\\", &source_name); |
| base::ReplaceChars(source_name, "*", "\\*", &source_name); |
| base::ReplaceChars(source_name, "?", "\\?", &source_name); |
| |
| // This vector maps line numbers to the corresponding text in |source|. |
| const std::vector<std::string_view> source_lines = base::SplitStringPiece( |
| source, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| |
| // |source_frame_pattern| should match any line that looks like a stack frame |
| // from a source file named |source_name|. |
| const std::string source_frame_pattern = |
| base::StringPrintf(" at *%s:*:*", source_name.c_str()); |
| |
| // This is the amount of indentation that is applied to the lines of inserted |
| // annotations. |
| const std::string indent(8, ' '); |
| const std::string_view elision_mark = ""; |
| |
| // Loop over each line of |js_error|, and append each to |annotated_error| -- |
| // possibly rewriting to include extra context. |
| std::ostringstream annotated_error; |
| for (std::string_view error_line : base::SplitStringPiece( |
| js_error, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { |
| // Does this look like a stack frame whose URL source matches |source_name|? |
| if (base::MatchPattern(error_line, source_frame_pattern)) { |
| // When a match occurs, annotate the stack trace with the corresponding |
| // line from |source|, along with a ^^^ underneath, indicating the column |
| // position. |
| std::vector<std::string_view> error_line_parts = base::SplitStringPiece( |
| error_line, ":", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| CHECK_GE(error_line_parts.size(), 2u); |
| |
| int column_number = 0; |
| base::StringToInt(error_line_parts.back(), &column_number); |
| error_line_parts.pop_back(); |
| int line_number = 0; |
| base::StringToInt(error_line_parts.back(), &line_number); |
| error_line_parts.pop_back(); |
| |
| // Protect against out-of-range matches. |
| if ((line_number > 0) && (column_number > 0) && |
| static_cast<size_t>(line_number) <= source_lines.size()) { |
| // Apply adjustment requested by caller to columns on the first line. |
| // This allows us to add preamble boilerplate to the script, but still |
| // locate errors correctly. |
| if (line_number == 1 && |
| column_number > column_adjustment_for_line_one) { |
| column_number -= column_adjustment_for_line_one; |
| } |
| |
| // Some source lines are huge. Elide |source_line| so that it doesn't |
| // occupy more than one actual line. |
| std::string source_line(source_lines[line_number - 1]); |
| |
| int max_column_number = 60 - indent.length(); |
| if (column_number > max_column_number) { |
| source_line = source_line.substr(column_number - max_column_number); |
| column_number = max_column_number; |
| source_line.replace(0, elision_mark.length(), elision_mark.data(), |
| elision_mark.length()); |
| } |
| |
| size_t max_length = 80 - indent.length(); |
| if (source_line.length() > max_length) { |
| source_line = base::StrCat( |
| {source_line.substr(0, max_length - elision_mark.length()), |
| elision_mark}); |
| } |
| |
| annotated_error << base::JoinString(error_line_parts, ":") << ":" |
| << line_number << ":" << column_number << "):\n" |
| << indent << source_line << '\n' |
| << indent << std::string(column_number - 1, ' ') |
| << "^^^^^\n"; |
| continue; |
| } |
| } |
| // This line was not rewritten -- just append it as-is. |
| annotated_error << error_line << "\n"; |
| } |
| return annotated_error.str(); |
| } |
| |
| // Waits for a response from ExecuteJavaScriptForTests, simulating an |
| // error if the target renderer is destroyed while executing the script. |
| class ExecuteJavaScriptForTestsWaiter : public WebContentsObserver { |
| public: |
| explicit ExecuteJavaScriptForTestsWaiter(const ToRenderFrameHost& adapter) |
| : WebContentsObserver( |
| WebContents::FromRenderFrameHost(adapter.render_frame_host())), |
| render_frame_host_(adapter.render_frame_host()) {} |
| |
| blink::mojom::LocalFrame::JavaScriptExecuteRequestForTestsCallback |
| GetCallback() { |
| return base::BindOnce(&ExecuteJavaScriptForTestsWaiter::SetValue, |
| weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| bool Wait() { |
| if (!has_value_) { |
| run_loop_.Run(); |
| } |
| return has_value_; |
| } |
| |
| blink::mojom::JavaScriptExecutionResultType GetResultType() { |
| DCHECK(has_value_); |
| return type_; |
| } |
| |
| const base::Value& GetResult() { |
| DCHECK(has_value_); |
| return value_; |
| } |
| |
| // WebContentsObserver |
| void PrimaryMainFrameRenderProcessGone( |
| base::TerminationStatus status) override { |
| if (status == base::TERMINATION_STATUS_NORMAL_TERMINATION || |
| status == base::TERMINATION_STATUS_STILL_RUNNING) { |
| return; |
| } |
| UpdateAfterScriptFailed("Renderer terminated."); |
| } |
| void RenderFrameDeleted(RenderFrameHost* render_frame_host) override { |
| if (render_frame_host_ != render_frame_host) { |
| return; |
| } |
| UpdateAfterScriptFailed("RenderFrame deleted."); |
| } |
| |
| private: |
| void UpdateAfterScriptFailed(const std::string& msg) { |
| render_frame_host_ = nullptr; |
| if (has_value_) { |
| return; |
| } |
| SetValue(blink::mojom::JavaScriptExecutionResultType::kException, |
| base::Value(msg)); |
| } |
| |
| void SetValue(blink::mojom::JavaScriptExecutionResultType type, |
| base::Value value) { |
| DCHECK(!has_value_); |
| has_value_ = true; |
| type_ = type; |
| value_ = value.Clone(); |
| run_loop_.Quit(); |
| } |
| |
| raw_ptr<RenderFrameHost> render_frame_host_; |
| base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed}; |
| bool has_value_ = false; |
| blink::mojom::JavaScriptExecutionResultType type_; |
| base::Value value_; |
| |
| base::WeakPtrFactory<ExecuteJavaScriptForTestsWaiter> weak_ptr_factory_{this}; |
| }; |
| |
| EvalJsResult EvalJsRunner( |
| const ToRenderFrameHost& execution_target, |
| std::string_view script, |
| std::string_view source_url, |
| int options, |
| int32_t world_id, |
| base::OnceClosure after_script_invoke = base::DoNothing()) { |
| RenderFrameHostImpl* rfh = |
| static_cast<RenderFrameHostImpl*>(execution_target.render_frame_host()); |
| if (!rfh->IsRenderFrameLive()) { |
| return EvalJsResult( |
| base::Value(), "Error: EvalJs won't work on an already-crashed frame."); |
| } |
| |
| bool user_gesture = rfh->GetLifecycleState() != |
| RenderFrameHost::LifecycleState::kPrerendering && |
| !(options & EXECUTE_SCRIPT_NO_USER_GESTURE) && |
| world_id == ISOLATED_WORLD_ID_GLOBAL; |
| bool resolve_promises = !(options & EXECUTE_SCRIPT_NO_RESOLVE_PROMISES); |
| bool honor_js_content_settings = |
| options & EXECUTE_SCRIPT_HONOR_JS_CONTENT_SETTINGS; |
| |
| ExecuteJavaScriptForTestsWaiter waiter(rfh); |
| rfh->ExecuteJavaScriptForTests(base::UTF8ToUTF16(script), user_gesture, |
| resolve_promises, honor_js_content_settings, |
| world_id, waiter.GetCallback()); |
| |
| std::move(after_script_invoke).Run(); |
| |
| bool has_value = waiter.Wait(); |
| if (!has_value) { |
| return EvalJsResult(base::Value(), |
| "Timeout waiting for Javascript to execute."); |
| } |
| |
| using blink::mojom::JavaScriptExecutionResultType; |
| JavaScriptExecutionResultType result_type = waiter.GetResultType(); |
| const base::Value& result_value = waiter.GetResult(); |
| |
| if (result_type == JavaScriptExecutionResultType::kException) { |
| // Parse the stack trace here, and interleave lines of source code from |
| // |script| to aid debugging. |
| CHECK(result_value.is_string() && !result_value.GetString().empty()); |
| std::string error_text = |
| "a JavaScript error: \"" + result_value.GetString() + "\""; |
| return EvalJsResult(base::Value(), |
| AnnotateAndAdjustJsStackTraces( |
| error_text, std::string(source_url), script, 0)); |
| } |
| |
| return EvalJsResult(result_value.Clone(), std::string()); |
| } |
| |
| class ScopedTestDevToolsProtocolClient : public TestDevToolsProtocolClient { |
| public: |
| explicit ScopedTestDevToolsProtocolClient(RenderFrameHost& rfh) { |
| AttachToFrameTreeHost(&rfh); |
| } |
| ~ScopedTestDevToolsProtocolClient() override { DetachProtocolClient(); } |
| }; |
| |
| } // namespace |
| |
| ::testing::AssertionResult ExecJs(const ToRenderFrameHost& execution_target, |
| std::string_view script, |
| int options, |
| int32_t world_id) { |
| // TODO(nick): Do we care enough about folks shooting themselves in the foot |
| // here with e.g. ASSERT_TRUE(ExecJs("window == window.top")) -- when they |
| // mean EvalJs -- to fail a CHECK() when eval_result.value.is_bool()? |
| EvalJsResult eval_result = |
| EvalJs(execution_target, script, options, world_id); |
| |
| // NOTE: |eval_result.value| is intentionally ignored by ExecJs(). |
| if (!eval_result.is_ok()) { |
| return ::testing::AssertionFailure() << eval_result; |
| } |
| return ::testing::AssertionSuccess(); |
| } |
| |
| EvalJsResult EvalJs(const ToRenderFrameHost& execution_target, |
| std::string_view script, |
| int options, |
| int32_t world_id, |
| base::OnceClosure after_script_invoke) { |
| TRACE_EVENT1("test", "EvalJs", "script", script); |
| |
| // The sourceURL= parameter provides a string that replaces <anonymous> in |
| // stack traces, if an Error is thrown. 'std::string' is meant to communicate |
| // that this is a dynamic argument originating from C++ code. |
| // |
| // Wrapping the script in braces makes it run in a block scope so that |
| // let/const don't leak outside the code being run, but vars will float to |
| // the outer scope. |
| const char* kSourceURL = "__const_std::string&_script__"; |
| std::string modified_script = |
| base::StrCat({"{", script, "\n}\n//# sourceURL=", kSourceURL}); |
| |
| return EvalJsRunner(execution_target, modified_script, kSourceURL, options, |
| world_id, std::move(after_script_invoke)); |
| } |
| |
| EvalJsResult EvalJsAfterLifecycleUpdate( |
| const ToRenderFrameHost& execution_target, |
| std::string_view raf_script, |
| std::string_view script, |
| int options, |
| int32_t world_id) { |
| TRACE_EVENT2("test", "EvalJsAfterLifecycleUpdate", "raf_script", raf_script, |
| "script", script); |
| |
| const char* kSourceURL = "__const_std::string&_script__"; |
| const char* kWrapperURL = "__const_std::string&_EvalJsAfterLifecycleUpdate__"; |
| std::string modified_raf_script; |
| if (raf_script.length()) { |
| modified_raf_script = |
| base::StrCat({raf_script, ";\n//# sourceURL=", kSourceURL}); |
| } |
| std::string modified_script = |
| base::StrCat({script, ";\n//# sourceURL=", kSourceURL}); |
| |
| // This runner_script delays running the argument scripts until just before |
| // (|raf_script|) and after (|script|) a rendering update. |
| std::string runner_script = JsReplace( |
| R"(new Promise((resolve, reject) => { |
| requestAnimationFrame(() => { |
| try { window.eval($1); } catch (e) { reject(e); } |
| setTimeout(() => { |
| try { resolve(window.eval($2)); } catch (e) { reject(e); } |
| }); |
| }); |
| }) |
| //# sourceURL=$3)", |
| modified_raf_script, modified_script, kWrapperURL); |
| |
| EvalJsResult result = EvalJsRunner(execution_target, runner_script, |
| kWrapperURL, options, world_id); |
| |
| if (!result.is_ok() && |
| base::StartsWith(result.ExtractError(), |
| "a JavaScript error: \"EvalError: Refused", |
| base::CompareCase::SENSITIVE)) { |
| return EvalJsResult(base::Value(), |
| base::StrCat({"EvalJsAfterLifecycleUpdate encountered " |
| "an EvalError, because eval() " |
| "is blocked by the document's CSP on " |
| "this page. To test content that " |
| "is protected by CSP, consider using " |
| "EvalJsAfterLifecycleUpdate in an " |
| "isolated world. Details: ", |
| result.ExtractError()})); |
| } |
| return result; |
| } |
| |
| RenderFrameHost* FrameMatchingPredicateOrNullptr( |
| Page& page, |
| base::RepeatingCallback<bool(RenderFrameHost*)> predicate) { |
| std::set<RenderFrameHost*> frame_set; |
| page.GetMainDocument().ForEachRenderFrameHost( |
| [&predicate, &frame_set](RenderFrameHost* rfh) { |
| if (predicate.Run(rfh)) { |
| frame_set.insert(rfh); |
| } |
| }); |
| EXPECT_LE(frame_set.size(), 1u); |
| return frame_set.size() == 1 ? *frame_set.begin() : nullptr; |
| } |
| |
| RenderFrameHost* FrameMatchingPredicate( |
| Page& page, |
| base::RepeatingCallback<bool(RenderFrameHost*)> predicate) { |
| RenderFrameHost* rfh = |
| FrameMatchingPredicateOrNullptr(page, std::move(predicate)); |
| EXPECT_TRUE(rfh); |
| return rfh; |
| } |
| |
| bool FrameMatchesName(std::string_view name, RenderFrameHost* frame) { |
| return frame->GetFrameName() == name; |
| } |
| |
| bool FrameIsChildOfMainFrame(RenderFrameHost* frame) { |
| return frame->GetParent() && !frame->GetParent()->GetParent(); |
| } |
| |
| bool FrameHasSourceUrl(const GURL& url, RenderFrameHost* frame) { |
| return frame->GetLastCommittedURL() == url; |
| } |
| |
| RenderFrameHost* ChildFrameAt(const ToRenderFrameHost& adapter, size_t index) { |
| RenderFrameHostImpl* rfh = |
| static_cast<RenderFrameHostImpl*>(adapter.render_frame_host()); |
| if (index >= rfh->frame_tree_node()->child_count()) { |
| return nullptr; |
| } |
| return rfh->frame_tree_node()->child_at(index)->current_frame_host(); |
| } |
| |
| bool HasOriginKeyedProcess(RenderFrameHost* frame) { |
| return static_cast<RenderFrameHostImpl*>(frame) |
| ->GetSiteInstance() |
| ->GetSiteInfo() |
| .requires_origin_keyed_process(); |
| } |
| |
| bool HasSandboxedSiteInstance(RenderFrameHost* frame) { |
| return static_cast<RenderFrameHostImpl*>(frame) |
| ->GetSiteInstance() |
| ->GetSiteInfo() |
| .is_sandboxed(); |
| } |
| |
| std::vector<RenderFrameHost*> CollectAllRenderFrameHosts( |
| RenderFrameHost* starting_rfh) { |
| std::vector<RenderFrameHost*> visited_frames; |
| starting_rfh->ForEachRenderFrameHost( |
| [&](RenderFrameHost* rfh) { visited_frames.push_back(rfh); }); |
| return visited_frames; |
| } |
| |
| std::vector<RenderFrameHost*> CollectAllRenderFrameHosts(Page& page) { |
| return CollectAllRenderFrameHosts(&page.GetMainDocument()); |
| } |
| |
| std::vector<RenderFrameHost*> CollectAllRenderFrameHosts( |
| WebContents* web_contents) { |
| std::vector<RenderFrameHost*> visited_frames; |
| web_contents->ForEachRenderFrameHost( |
| [&](RenderFrameHost* rfh) { visited_frames.push_back(rfh); }); |
| return visited_frames; |
| } |
| |
| std::vector<WebContents*> GetAllWebContents() { |
| std::vector<WebContentsImpl*> all_wci = WebContentsImpl::GetAllWebContents(); |
| std::vector<WebContents*> all_wc; |
| std::ranges::transform(all_wci, std::back_inserter(all_wc), |
| [](WebContentsImpl* wc) { return wc; }); |
| |
| return all_wc; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| bool ExecuteWebUIResourceTest(WebContents* web_contents) { |
| // Inject WebUI test runner script. |
| std::string script; |
| scoped_refptr<base::RefCountedMemory> bytes = |
| ui::ResourceBundle::GetSharedInstance().LoadDataResourceBytes( |
| IDR_ASH_WEBUI_COMMON_WEBUI_RESOURCE_TEST_JS); |
| |
| if (net::GZipHeader::HasGZipHeader(base::span(*bytes))) { |
| AppendGzippedResource(*bytes, &script); |
| } else { |
| auto chars = base::as_chars(base::span(*bytes)); |
| script.append(chars.data(), chars.size()); |
| } |
| |
| script.append("\n"); |
| ExecuteScriptAsync(web_contents, script); |
| |
| DOMMessageQueue message_queue(web_contents); |
| |
| bool should_wait_flag = base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kWaitForDebuggerWebUI); |
| |
| if (should_wait_flag) { |
| ExecuteScriptAsync( |
| web_contents, |
| "window.waitUser = true; " |
| "window.go = function() { window.waitUser = false }; " |
| "console.log('Waiting for debugger...'); " |
| "console.log('Run: go() in the JS console when you are ready.');"); |
| } |
| |
| ExecuteScriptAsync(web_contents, "runTests()"); |
| |
| std::string message; |
| do { |
| if (!message_queue.WaitForMessage(&message)) { |
| return false; |
| } |
| } while (message.compare("\"PENDING\"") == 0); |
| |
| return message.compare("\"SUCCESS\"") == 0; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| std::string GetCookies(BrowserContext* browser_context, |
| const GURL& url, |
| net::CookieOptions::SameSiteCookieContext context, |
| net::CookiePartitionKeyCollection key_collection) { |
| mojo::Remote<network::mojom::CookieManager> cookie_manager; |
| browser_context->GetDefaultStoragePartition() |
| ->GetNetworkContext() |
| ->GetCookieManager(cookie_manager.BindNewPipeAndPassReceiver()); |
| net::CookieOptions options; |
| options.set_same_site_cookie_context(context); |
| base::test::TestFuture<const net::CookieAccessResultList&, |
| const net::CookieAccessResultList&> |
| future; |
| cookie_manager->GetCookieList(url, options, key_collection, |
| future.GetCallback()); |
| return net::CanonicalCookie::BuildCookieLine(std::get<0>(future.Get())); |
| } |
| |
| std::vector<net::CanonicalCookie> GetCanonicalCookies( |
| BrowserContext* browser_context, |
| const GURL& url, |
| net::CookiePartitionKeyCollection key_collection) { |
| mojo::Remote<network::mojom::CookieManager> cookie_manager; |
| browser_context->GetDefaultStoragePartition() |
| ->GetNetworkContext() |
| ->GetCookieManager(cookie_manager.BindNewPipeAndPassReceiver()); |
| // Allow access to SameSite cookies in tests. |
| net::CookieOptions options; |
| options.set_same_site_cookie_context( |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive()); |
| base::test::TestFuture<const net::CookieAccessResultList&, |
| const net::CookieAccessResultList&> |
| future; |
| cookie_manager->GetCookieList(url, options, key_collection, |
| future.GetCallback()); |
| return net::cookie_util::StripAccessResults(std::get<0>(future.Get())); |
| } |
| |
| bool SetCookie( |
| BrowserContext* browser_context, |
| const GURL& url, |
| const std::string& value, |
| net::CookieOptions::SameSiteCookieContext context, |
| base::optional_ref<const net::CookiePartitionKey> cookie_partition_key) { |
| if (cookie_partition_key) { |
| DCHECK(base::Contains(base::ToLowerASCII(value), ";partitioned")); |
| } |
| mojo::Remote<network::mojom::CookieManager> cookie_manager; |
| browser_context->GetDefaultStoragePartition() |
| ->GetNetworkContext() |
| ->GetCookieManager(cookie_manager.BindNewPipeAndPassReceiver()); |
| std::unique_ptr<net::CanonicalCookie> cc( |
| net::CanonicalCookie::CreateForTesting( |
| url, value, base::Time::Now(), std::nullopt /* server_time */, |
| cookie_partition_key.CopyAsOptional())); |
| DCHECK(cc.get()); |
| |
| net::CookieOptions options; |
| options.set_include_httponly(); |
| options.set_same_site_cookie_context(context); |
| base::test::TestFuture<net::CookieAccessResult> future; |
| cookie_manager->SetCanonicalCookie(*cc.get(), url, options, |
| future.GetCallback()); |
| return future.Get().status.IsInclude(); |
| } |
| |
| uint32_t DeleteCookies(BrowserContext* browser_context, |
| network::mojom::CookieDeletionFilter filter) { |
| mojo::Remote<network::mojom::CookieManager> cookie_manager; |
| browser_context->GetDefaultStoragePartition() |
| ->GetNetworkContext() |
| ->GetCookieManager(cookie_manager.BindNewPipeAndPassReceiver()); |
| |
| base::test::TestFuture<uint32_t> future; |
| cookie_manager->DeleteCookies( |
| network::mojom::CookieDeletionFilter::New(filter), future.GetCallback()); |
| return future.Get(); |
| } |
| |
| void FetchHistogramsFromChildProcesses() { |
| // Wait for all initialized processes to be ready before fetching histograms |
| // for the first time. |
| for (RenderProcessHost::iterator it(RenderProcessHost::AllHostsIterator()); |
| !it.IsAtEnd(); it.Advance()) { |
| RenderProcessHost* process = it.GetCurrentValue(); |
| if (process->IsInitializedAndNotDead() && !process->IsReady()) { |
| RenderProcessHostWatcher ready_watcher( |
| process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY); |
| ready_watcher.Wait(); |
| } |
| } |
| |
| base::RunLoop run_loop; |
| |
| FetchHistogramsAsynchronously( |
| base::SingleThreadTaskRunner::GetCurrentDefault(), run_loop.QuitClosure(), |
| // If this call times out, it means that a child process is not |
| // responding, which is something we should not ignore. The timeout is |
| // set to be longer than the normal browser test timeout so that it will |
| // be prempted by the normal timeout. |
| TestTimeouts::action_max_timeout()); |
| run_loop.Run(); |
| } |
| |
| void SetupCrossSiteRedirector(net::EmbeddedTestServer* embedded_test_server) { |
| embedded_test_server->RegisterRequestHandler(base::BindRepeating( |
| &CrossSiteRedirectResponseHandler, embedded_test_server)); |
| } |
| |
| void SetFileSystemAccessPermissionContext( |
| BrowserContext* browser_context, |
| FileSystemAccessPermissionContext* permission_context) { |
| static_cast<FileSystemAccessManagerImpl*>( |
| browser_context->GetDefaultStoragePartition() |
| ->GetFileSystemAccessEntryFactory()) |
| ->SetPermissionContextForTesting(permission_context); |
| } |
| |
| bool WaitForRenderFrameReady(RenderFrameHost* rfh) { |
| if (!rfh) { |
| return false; |
| } |
| std::string result = |
| EvalJs(rfh, |
| "(async function() {" |
| " if (document.readyState != 'complete') {" |
| " await new Promise((resolve) =>" |
| " document.addEventListener('readystatechange', event => {" |
| " if (document.readyState == 'complete') {" |
| " resolve();" |
| " }" |
| " }));" |
| " }" |
| "})().then(() => 'pageLoadComplete');", |
| EXECUTE_SCRIPT_NO_USER_GESTURE) |
| .ExtractString(); |
| EXPECT_EQ("pageLoadComplete", result); |
| return "pageLoadComplete" == result; |
| } |
| |
| void WaitForAccessibilityFocusChange() { |
| base::RunLoop run_loop; |
| ui::BrowserAccessibilityManager::SetFocusChangeCallbackForTesting( |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| ui::AXNodeData GetFocusedAccessibilityNodeInfo(WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| ui::BrowserAccessibilityManager* manager = |
| web_contents_impl->GetRootBrowserAccessibilityManager(); |
| if (!manager) { |
| return ui::AXNodeData(); |
| } |
| ui::BrowserAccessibility* focused_node = manager->GetFocus(); |
| return focused_node->GetData(); |
| } |
| |
| bool AccessibilityTreeContainsNodeWithName(ui::BrowserAccessibility* node, |
| std::string_view name) { |
| // If an image annotation is set, it plays the same role as a name, so it |
| // makes sense to check both in the same test helper. |
| if (node->GetStringAttribute(ax::mojom::StringAttribute::kName) == name || |
| node->GetStringAttribute(ax::mojom::StringAttribute::kImageAnnotation) == |
| name) { |
| return true; |
| } |
| for (unsigned i = 0; i < node->PlatformChildCount(); i++) { |
| if (AccessibilityTreeContainsNodeWithName(node->PlatformGetChild(i), |
| name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void WaitForAccessibilityTreeToChange(WebContents* web_contents) { |
| AccessibilityNotificationWaiter accessibility_waiter(web_contents); |
| ASSERT_TRUE(accessibility_waiter.WaitForNotification()); |
| } |
| |
| void WaitForAccessibilityTreeToContainNodeWithName(WebContents* web_contents, |
| std::string_view name) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| web_contents_impl->GetPrimaryMainFrame()); |
| ui::BrowserAccessibilityManager* main_frame_manager = |
| main_frame->browser_accessibility_manager(); |
| while (!main_frame_manager || |
| !AccessibilityTreeContainsNodeWithName( |
| main_frame_manager->GetBrowserAccessibilityRoot(), name)) { |
| WaitForAccessibilityTreeToChange(web_contents); |
| main_frame_manager = main_frame->browser_accessibility_manager(); |
| } |
| } |
| |
| ui::AXTreeUpdate GetAccessibilityTreeSnapshot(WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| ui::BrowserAccessibilityManager* manager = |
| web_contents_impl->GetRootBrowserAccessibilityManager(); |
| if (!manager) { |
| return ui::AXTreeUpdate(); |
| } |
| return manager->SnapshotAXTreeForTesting(); |
| } |
| |
| ui::AXTreeUpdate GetAccessibilityTreeSnapshotFromId( |
| const ui::AXTreeID& tree_id) { |
| ui::BrowserAccessibilityManager* manager = |
| ui::BrowserAccessibilityManager::FromID(tree_id); |
| return manager ? manager->SnapshotAXTreeForTesting() : ui::AXTreeUpdate(); |
| } |
| |
| ui::AXPlatformNodeDelegate* GetRootAccessibilityNode( |
| WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| ui::BrowserAccessibilityManager* manager = |
| web_contents_impl->GetRootBrowserAccessibilityManager(); |
| return manager ? manager->GetBrowserAccessibilityRoot() : nullptr; |
| } |
| |
| FindAccessibilityNodeCriteria::FindAccessibilityNodeCriteria() = default; |
| |
| FindAccessibilityNodeCriteria::~FindAccessibilityNodeCriteria() = default; |
| |
| ui::AXPlatformNodeDelegate* FindAccessibilityNode( |
| WebContents* web_contents, |
| const FindAccessibilityNodeCriteria& criteria) { |
| ui::AXPlatformNodeDelegate* root = GetRootAccessibilityNode(web_contents); |
| CHECK(root); |
| return FindAccessibilityNodeInSubtree(root, criteria); |
| } |
| |
| ui::AXPlatformNodeDelegate* FindAccessibilityNodeInSubtree( |
| ui::AXPlatformNodeDelegate* node, |
| const FindAccessibilityNodeCriteria& criteria) { |
| auto* node_internal = |
| ui::BrowserAccessibility::FromAXPlatformNodeDelegate(node); |
| DCHECK(node_internal); |
| if ((!criteria.name || |
| node_internal->GetStringAttribute(ax::mojom::StringAttribute::kName) == |
| criteria.name.value()) && |
| (!criteria.role || node_internal->GetRole() == criteria.role.value())) { |
| return node; |
| } |
| |
| for (unsigned int i = 0; i < node_internal->PlatformChildCount(); ++i) { |
| ui::BrowserAccessibility* child = node_internal->PlatformGetChild(i); |
| ui::AXPlatformNodeDelegate* result = |
| FindAccessibilityNodeInSubtree(child, criteria); |
| if (result) { |
| return result; |
| } |
| } |
| return nullptr; |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| template <typename T> |
| Microsoft::WRL::ComPtr<T> QueryInterfaceFromNode( |
| ui::AXPlatformNodeDelegate* node) { |
| Microsoft::WRL::ComPtr<T> result; |
| EXPECT_HRESULT_SUCCEEDED( |
| node->GetNativeViewAccessible()->QueryInterface(__uuidof(T), &result)); |
| return result; |
| } |
| |
| void UiaGetPropertyValueVtArrayVtUnknownValidate( |
| PROPERTYID property_id, |
| ui::AXPlatformNodeDelegate* target_node, |
| const std::vector<std::string>& expected_names) { |
| ASSERT_TRUE(target_node); |
| |
| base::win::ScopedVariant result_variant; |
| Microsoft::WRL::ComPtr<IRawElementProviderSimple> node_provider = |
| QueryInterfaceFromNode<IRawElementProviderSimple>(target_node); |
| |
| node_provider->GetPropertyValue(property_id, result_variant.Receive()); |
| ASSERT_EQ(VT_ARRAY | VT_UNKNOWN, result_variant.type()); |
| ASSERT_EQ(1u, SafeArrayGetDim(V_ARRAY(result_variant.ptr()))); |
| |
| LONG lower_bound, upper_bound, size; |
| ASSERT_HRESULT_SUCCEEDED( |
| SafeArrayGetLBound(V_ARRAY(result_variant.ptr()), 1, &lower_bound)); |
| ASSERT_HRESULT_SUCCEEDED( |
| SafeArrayGetUBound(V_ARRAY(result_variant.ptr()), 1, &upper_bound)); |
| size = upper_bound - lower_bound + 1; |
| ASSERT_EQ(static_cast<LONG>(expected_names.size()), size); |
| |
| std::vector<std::string> names; |
| for (LONG i = 0; i < size; ++i) { |
| Microsoft::WRL::ComPtr<IUnknown> unknown_element; |
| ASSERT_HRESULT_SUCCEEDED( |
| SafeArrayGetElement(V_ARRAY(result_variant.ptr()), &i, |
| static_cast<void**>(&unknown_element))); |
| ASSERT_NE(nullptr, unknown_element); |
| |
| Microsoft::WRL::ComPtr<IRawElementProviderSimple> |
| raw_element_provider_simple; |
| ASSERT_HRESULT_SUCCEEDED(unknown_element.As(&raw_element_provider_simple)); |
| ASSERT_NE(nullptr, raw_element_provider_simple); |
| |
| base::win::ScopedVariant name; |
| ASSERT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPropertyValue( |
| UIA_NamePropertyId, name.Receive())); |
| ASSERT_EQ(VT_BSTR, name.type()); |
| names.push_back(base::WideToUTF8( |
| std::wstring(V_BSTR(name.ptr()), SysStringLen(V_BSTR(name.ptr()))))); |
| } |
| |
| ASSERT_THAT(names, ::testing::UnorderedElementsAreArray(expected_names)); |
| } |
| #endif |
| |
| RenderWidgetHost* GetKeyboardLockWidget(WebContents* web_contents) { |
| return static_cast<WebContentsImpl*>(web_contents)->GetKeyboardLockWidget(); |
| } |
| |
| RenderWidgetHost* GetMouseLockWidget(WebContents* web_contents) { |
| return static_cast<WebContentsImpl*>(web_contents) |
| ->mouse_lock_widget_for_testing(); |
| } |
| |
| void RequestKeyboardLock( |
| WebContents* web_contents, |
| std::optional<base::flat_set<ui::DomCode>> codes, |
| base::OnceCallback<void(blink::mojom::KeyboardLockRequestResult)> |
| callback) { |
| DCHECK(!codes.has_value() || !codes.value().empty()); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| RenderWidgetHostImpl* render_widget_host_impl = |
| web_contents_impl->GetPrimaryMainFrame()->GetRenderWidgetHost(); |
| render_widget_host_impl->Focus(); |
| render_widget_host_impl->RequestKeyboardLock(std::move(codes), |
| std::move(callback)); |
| } |
| |
| void CancelKeyboardLock(WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| RenderWidgetHostImpl* render_widget_host_impl = |
| web_contents_impl->GetPrimaryMainFrame()->GetRenderWidgetHost(); |
| render_widget_host_impl->CancelKeyboardLock(); |
| } |
| |
| ScreenOrientationDelegate* GetScreenOrientationDelegate() { |
| return ScreenOrientationProvider::GetDelegateForTesting(); |
| } |
| |
| std::vector<RenderWidgetHostView*> GetInputEventRouterRenderWidgetHostViews( |
| WebContents* web_contents) { |
| return static_cast<WebContentsImpl*>(web_contents) |
| ->GetRenderWidgetHostViewsForTests(); |
| } |
| |
| RenderWidgetHost* GetFocusedRenderWidgetHost(WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| return web_contents_impl->GetFocusedRenderWidgetHost( |
| web_contents_impl->GetPrimaryMainFrame()->GetRenderWidgetHost()); |
| } |
| |
| bool IsRenderWidgetHostFocused(const RenderWidgetHost* host) { |
| return static_cast<const RenderWidgetHostImpl*>(host)->is_focused(); |
| } |
| |
| WebContents* GetFocusedWebContents(WebContents* web_contents) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents); |
| return web_contents_impl->GetFocusedWebContents(); |
| } |
| |
| namespace { |
| |
| RenderFrameMetadataProviderImpl* RenderFrameMetadataProviderFromRenderFrameHost( |
| RenderFrameHost* render_frame_host) { |
| DCHECK(render_frame_host); |
| DCHECK(render_frame_host->GetRenderWidgetHost()); |
| // This helper should return a valid provider since it's used for |
| // RenderFrameSubmissionObserver ctor. |
| DCHECK(RenderWidgetHostImpl::From(render_frame_host->GetRenderWidgetHost()) |
| ->render_frame_metadata_provider()); |
| return RenderWidgetHostImpl::From(render_frame_host->GetRenderWidgetHost()) |
| ->render_frame_metadata_provider(); |
| } |
| |
| } // namespace |
| |
| TitleWatcher::TitleWatcher(WebContents* web_contents, |
| std::u16string_view expected_title, |
| bool include_nestable_tasks) |
| : WebContentsObserver(web_contents), |
| run_loop_(include_nestable_tasks |
| ? base::RunLoop::Type::kNestableTasksAllowed |
| : base::RunLoop::Type::kDefault) { |
| expected_titles_.emplace_back(expected_title); |
| } |
| |
| void TitleWatcher::AlsoWaitForTitle(std::u16string_view expected_title) { |
| expected_titles_.emplace_back(expected_title); |
| } |
| |
| TitleWatcher::~TitleWatcher() = default; |
| |
| const std::u16string& TitleWatcher::WaitAndGetTitle() { |
| TestTitle(); |
| run_loop_.Run(); |
| return observed_title_; |
| } |
| |
| void TitleWatcher::DidStopLoading() { |
| // When navigating through the history, the restored NavigationEntry's title |
| // will be used. If the entry ends up having the same title after we return |
| // to it, as will usually be the case, then WebContentsObserver::TitleSet |
| // will then be suppressed, since the NavigationEntry's title hasn't changed. |
| TestTitle(); |
| } |
| |
| void TitleWatcher::TitleWasSet(NavigationEntry* entry) { |
| TestTitle(); |
| } |
| |
| void TitleWatcher::TestTitle() { |
| const std::u16string& current_title = web_contents()->GetTitle(); |
| if (base::Contains(expected_titles_, current_title)) { |
| observed_title_ = current_title; |
| run_loop_.Quit(); |
| } |
| } |
| |
| RenderProcessHostWatcher::RenderProcessHostWatcher( |
| RenderProcessHost* render_process_host, |
| WatchType type) |
| : type_(type), |
| did_exit_normally_(true), |
| allow_renderer_crashes_( |
| std::make_unique<ScopedAllowRendererCrashes>(render_process_host)), |
| quit_closure_(run_loop_.QuitClosure()) { |
| observation_.Observe(render_process_host); |
| } |
| |
| RenderProcessHostWatcher::RenderProcessHostWatcher(WebContents* web_contents, |
| WatchType type) |
| : RenderProcessHostWatcher( |
| web_contents->GetPrimaryMainFrame()->GetProcess(), |
| type) {} |
| RenderProcessHostWatcher::~RenderProcessHostWatcher() = default; |
| |
| void RenderProcessHostWatcher::Wait() { |
| run_loop_.Run(); |
| |
| DCHECK(allow_renderer_crashes_) |
| << "RenderProcessHostWatcher::Wait() may only be called once"; |
| allow_renderer_crashes_.reset(); |
| // Call this here just in case something else quits the RunLoop. |
| observation_.Reset(); |
| } |
| |
| void RenderProcessHostWatcher::QuitRunLoop() { |
| std::move(quit_closure_).Run(); |
| observation_.Reset(); |
| } |
| |
| void RenderProcessHostWatcher::RenderProcessReady(RenderProcessHost* host) { |
| if (type_ == WATCH_FOR_PROCESS_READY) { |
| QuitRunLoop(); |
| } |
| } |
| |
| void RenderProcessHostWatcher::RenderProcessExited( |
| RenderProcessHost* host, |
| const ChildProcessTerminationInfo& info) { |
| did_exit_normally_ = |
| info.status == base::TERMINATION_STATUS_NORMAL_TERMINATION; |
| if (type_ == WATCH_FOR_PROCESS_EXIT) { |
| QuitRunLoop(); |
| } |
| } |
| |
| void RenderProcessHostWatcher::RenderProcessHostDestroyed( |
| RenderProcessHost* host) { |
| if (type_ == WATCH_FOR_HOST_DESTRUCTION) { |
| QuitRunLoop(); |
| } |
| } |
| |
| RenderProcessHostKillWaiter::RenderProcessHostKillWaiter( |
| RenderProcessHost* render_process_host, |
| std::string_view uma_name) |
| : exit_watcher_(render_process_host, |
| RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT), |
| uma_name_(uma_name) {} |
| |
| std::optional<int> RenderProcessHostKillWaiter::Wait() { |
| std::optional<bad_message::BadMessageReason> result; |
| |
| // Wait for the renderer kill. |
| exit_watcher_.Wait(); |
| #if !BUILDFLAG(IS_ANDROID) |
| // Getting termination status on android is not reliable. To avoid flakiness, |
| // we can skip this check and just check bad message. On other platforms we |
| // want to verify that the renderer got killed, rather than exiting normally. |
| if (exit_watcher_.did_exit_normally()) { |
| LOG(ERROR) << "Renderer unexpectedly exited normally."; |
| return result; |
| } |
| #endif |
| |
| // Find the logged UMA data (if present). |
| std::vector<base::Bucket> uma_samples = |
| histogram_tester_.GetAllSamples(uma_name_); |
| // No UMA will be present if the kill was not triggered by the //content layer |
| // (e.g. if it was triggered by bad_message::ReceivedBadMessage from //chrome |
| // layer or from somewhere in the //components layer). |
| if (uma_samples.empty()) { |
| LOG(ERROR) << "Unexpectedly found no '" << uma_name_ << "' samples."; |
| return result; |
| } |
| const base::Bucket& bucket = uma_samples.back(); |
| // Assuming that user of RenderProcessHostKillWatcher makes sure that only one |
| // kill can happen while using the class. |
| DCHECK_EQ(1u, uma_samples.size()) |
| << "Multiple renderer kills are unsupported"; |
| |
| return bucket.min; |
| } |
| |
| RenderProcessHostBadMojoMessageWaiter::RenderProcessHostBadMojoMessageWaiter( |
| RenderProcessHost* render_process_host) |
| : monitored_render_process_id_(render_process_host->GetID()), |
| kill_waiter_(render_process_host, |
| "Stability.BadMessageTerminated.Content") { |
| // base::Unretained is safe below, because the destructor unregisters the |
| // callback. |
| RenderProcessHostImpl::SetBadMojoMessageCallbackForTesting( |
| base::BindRepeating( |
| &RenderProcessHostBadMojoMessageWaiter::OnBadMojoMessage, |
| base::Unretained(this))); |
| } |
| |
| RenderProcessHostBadMojoMessageWaiter:: |
| ~RenderProcessHostBadMojoMessageWaiter() { |
| RenderProcessHostImpl::SetBadMojoMessageCallbackForTesting( |
| RenderProcessHostImpl::BadMojoMessageCallbackForTesting()); |
| } |
| |
| std::optional<std::string> RenderProcessHostBadMojoMessageWaiter::Wait() { |
| std::optional<int> bad_message_reason = kill_waiter_.Wait(); |
| if (!bad_message_reason.has_value()) { |
| return std::nullopt; |
| } |
| if (bad_message_reason.value() != bad_message::RPH_MOJO_PROCESS_ERROR) { |
| LOG(ERROR) << "Unexpected |bad_message_reason|: " |
| << bad_message_reason.value(); |
| return std::nullopt; |
| } |
| |
| return observed_mojo_error_; |
| } |
| |
| void RenderProcessHostBadMojoMessageWaiter::OnBadMojoMessage( |
| ChildProcessId render_process_id, |
| const std::string& error) { |
| if (render_process_id == monitored_render_process_id_) { |
| observed_mojo_error_ = error; |
| } |
| } |
| |
| class DOMMessageQueue::MessageObserver : public WebContentsObserver { |
| public: |
| MessageObserver(DOMMessageQueue* queue, WebContents* contents) |
| : WebContentsObserver(contents), |
| queue_(queue), |
| render_frame_host_(nullptr), |
| watching_frame_(false) {} |
| |
| MessageObserver(DOMMessageQueue* queue, RenderFrameHost* render_frame_host) |
| : WebContentsObserver( |
| WebContents::FromRenderFrameHost(render_frame_host)), |
| queue_(queue), |
| render_frame_host_(render_frame_host), |
| watching_frame_(true) {} |
| |
| ~MessageObserver() override = default; |
| |
| private: |
| void DomOperationResponse(RenderFrameHost* rfh, |
| const std::string& result) override { |
| if (!watching_frame_ || render_frame_host_ == rfh) { |
| queue_->OnDomMessageReceived(result); |
| } |
| } |
| |
| void PrimaryMainFrameRenderProcessGone( |
| base::TerminationStatus status) override { |
| queue_->PrimaryMainFrameRenderProcessGone(status); |
| } |
| |
| void RenderFrameDeleted(RenderFrameHost* render_frame_host) override { |
| if (render_frame_host_ != render_frame_host) { |
| return; |
| } |
| render_frame_host_ = nullptr; |
| queue_->RenderFrameDeleted(); |
| } |
| |
| void WebContentsDestroyed() override { |
| queue_->OnBackingWebContentsDestroyed(this); |
| } |
| |
| raw_ptr<DOMMessageQueue> queue_; |
| raw_ptr<RenderFrameHost> render_frame_host_; |
| bool watching_frame_; |
| }; |
| |
| DOMMessageQueue::DOMMessageQueue() { |
| // TODO(crbug.com/40746969): Remove the need to listen for this |
| // notification. |
| for (auto* contents : WebContentsImpl::GetAllWebContents()) { |
| observers_.emplace(std::make_unique<MessageObserver>(this, contents)); |
| } |
| web_contents_creation_subscription_ = |
| RegisterWebContentsCreationCallback(base::BindRepeating( |
| &DOMMessageQueue::OnWebContentsCreated, base::Unretained(this))); |
| } |
| |
| DOMMessageQueue::DOMMessageQueue(WebContents* web_contents) { |
| observers_.emplace(std::make_unique<MessageObserver>(this, web_contents)); |
| } |
| |
| DOMMessageQueue::DOMMessageQueue(RenderFrameHost* render_frame_host) { |
| observers_.emplace( |
| std::make_unique<MessageObserver>(this, render_frame_host)); |
| } |
| |
| DOMMessageQueue::~DOMMessageQueue() = default; |
| |
| void DOMMessageQueue::PrimaryMainFrameRenderProcessGone( |
| base::TerminationStatus status) { |
| switch (status) { |
| case base::TERMINATION_STATUS_NORMAL_TERMINATION: |
| case base::TERMINATION_STATUS_STILL_RUNNING: |
| break; |
| default: |
| renderer_crashed_ = true; |
| if (callback_) { |
| std::move(callback_).Run(); |
| } |
| break; |
| } |
| } |
| |
| void DOMMessageQueue::RenderFrameDeleted() { |
| if (callback_) { |
| std::move(callback_).Run(); |
| } |
| } |
| |
| void DOMMessageQueue::ClearQueue() { |
| message_queue_ = base::queue<std::string>(); |
| } |
| |
| void DOMMessageQueue::OnDomMessageReceived(const std::string& message) { |
| message_queue_.push(message); |
| if (callback_) { |
| std::move(callback_).Run(); |
| } |
| } |
| |
| void DOMMessageQueue::OnWebContentsCreated(WebContents* contents) { |
| observers_.emplace(std::make_unique<MessageObserver>(this, contents)); |
| } |
| |
| void DOMMessageQueue::OnBackingWebContentsDestroyed(MessageObserver* observer) { |
| for (auto& entry : observers_) { |
| if (entry.get() == observer) { |
| observers_.erase(entry); |
| break; |
| } |
| } |
| } |
| |
| void DOMMessageQueue::SetOnMessageAvailableCallback( |
| base::OnceClosure callback) { |
| CHECK(!callback_); |
| if (!message_queue_.empty() || renderer_crashed_) { |
| std::move(callback).Run(); |
| } else { |
| callback_ = std::move(callback); |
| } |
| } |
| |
| bool DOMMessageQueue::WaitForMessage(std::string* message) { |
| DCHECK(message); |
| if (!renderer_crashed_ && message_queue_.empty()) { |
| // This will be quit when a new message comes in. |
| base::RunLoop run_loop{base::RunLoop::Type::kNestableTasksAllowed}; |
| callback_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| return PopMessage(message); |
| } |
| |
| bool DOMMessageQueue::PopMessage(std::string* message) { |
| DCHECK(message); |
| if (renderer_crashed_ || message_queue_.empty()) { |
| return false; |
| } |
| *message = message_queue_.front(); |
| message_queue_.pop(); |
| return true; |
| } |
| |
| bool DOMMessageQueue::HasMessages() { |
| return !message_queue_.empty(); |
| } |
| |
| WebContentsAddedObserver::WebContentsAddedObserver() |
| : creation_subscription_(RegisterWebContentsCreationCallback( |
| base::BindRepeating(&WebContentsAddedObserver::WebContentsCreated, |
| base::Unretained(this)))) {} |
| |
| WebContentsAddedObserver::~WebContentsAddedObserver() = default; |
| |
| void WebContentsAddedObserver::WebContentsCreated(WebContents* web_contents) { |
| DCHECK(!web_contents_); |
| web_contents_ = web_contents; |
| |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| WebContents* WebContentsAddedObserver::GetWebContents() { |
| if (web_contents_) { |
| return web_contents_; |
| } |
| |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| return web_contents_; |
| } |
| |
| bool RequestFrame(WebContents* web_contents) { |
| DCHECK(web_contents); |
| return RenderWidgetHostImpl::From(web_contents->GetPrimaryMainFrame() |
| ->GetRenderViewHost() |
| ->GetWidget()) |
| ->RequestRepaintOnNewSurface(); |
| } |
| |
| RenderFrameSubmissionObserver::RenderFrameSubmissionObserver( |
| RenderFrameMetadataProviderImpl* render_frame_metadata_provider) |
| : render_frame_metadata_provider_(render_frame_metadata_provider) { |
| render_frame_metadata_provider_->AddObserver(this); |
| render_frame_metadata_provider_->ReportAllFrameSubmissionsForTesting(true); |
| } |
| |
| RenderFrameSubmissionObserver::RenderFrameSubmissionObserver( |
| FrameTreeNode* node) |
| : RenderFrameSubmissionObserver( |
| RenderFrameMetadataProviderFromRenderFrameHost( |
| node->current_frame_host())) {} |
| |
| RenderFrameSubmissionObserver::RenderFrameSubmissionObserver( |
| WebContents* web_contents) |
| : RenderFrameSubmissionObserver( |
| RenderFrameMetadataProviderFromRenderFrameHost( |
| web_contents->GetPrimaryMainFrame())) {} |
| |
| RenderFrameSubmissionObserver::RenderFrameSubmissionObserver( |
| RenderFrameHost* rfh) |
| : RenderFrameSubmissionObserver( |
| RenderFrameMetadataProviderFromRenderFrameHost(rfh)) {} |
| |
| RenderFrameSubmissionObserver::~RenderFrameSubmissionObserver() { |
| render_frame_metadata_provider_->RemoveObserver(this); |
| render_frame_metadata_provider_->ReportAllFrameSubmissionsForTesting(false); |
| } |
| |
| void RenderFrameSubmissionObserver::WaitForAnyFrameSubmission() { |
| break_on_any_frame_ = true; |
| Wait(); |
| break_on_any_frame_ = false; |
| } |
| |
| void RenderFrameSubmissionObserver::WaitForMetadataChange() { |
| Wait(); |
| } |
| |
| void RenderFrameSubmissionObserver::WaitForPageScaleFactor( |
| float expected_page_scale_factor, |
| const float tolerance) { |
| while (std::abs(render_frame_metadata_provider_->LastRenderFrameMetadata() |
| .page_scale_factor - |
| expected_page_scale_factor) > tolerance) { |
| WaitForMetadataChange(); |
| } |
| } |
| |
| void RenderFrameSubmissionObserver::WaitForExternalPageScaleFactor( |
| float expected_external_page_scale_factor, |
| const float tolerance) { |
| while (std::abs(render_frame_metadata_provider_->LastRenderFrameMetadata() |
| .external_page_scale_factor - |
| expected_external_page_scale_factor) > tolerance) { |
| WaitForMetadataChange(); |
| } |
| } |
| |
| void RenderFrameSubmissionObserver::WaitForScrollOffset( |
| const gfx::PointF& expected_offset) { |
| while (render_frame_metadata_provider_->LastRenderFrameMetadata() |
| .root_scroll_offset != expected_offset) { |
| const auto& offset = |
| render_frame_metadata_provider_->LastRenderFrameMetadata() |
| .root_scroll_offset; |
| constexpr float kEpsilon = 0.01f; |
| if (offset.has_value()) { |
| const auto diff = expected_offset - *offset; |
| if (std::abs(diff.x()) <= kEpsilon && std::abs(diff.y()) <= kEpsilon) { |
| break; |
| } |
| } |
| WaitForMetadataChange(); |
| } |
| } |
| |
| void RenderFrameSubmissionObserver::WaitForScrollOffsetAtTop( |
| bool expected_scroll_offset_at_top) { |
| while (render_frame_metadata_provider_->LastRenderFrameMetadata() |
| .is_scroll_offset_at_top != expected_scroll_offset_at_top) { |
| WaitForMetadataChange(); |
| } |
| } |
| |
| const cc::RenderFrameMetadata& |
| RenderFrameSubmissionObserver::LastRenderFrameMetadata() const { |
| return render_frame_metadata_provider_->LastRenderFrameMetadata(); |
| } |
| |
| void RenderFrameSubmissionObserver::NotifyOnNextMetadataChange( |
| base::OnceClosure closure) { |
| DCHECK(closure); |
| DCHECK(metadata_change_closure_.is_null()); |
| metadata_change_closure_ = std::move(closure); |
| } |
| |
| void RenderFrameSubmissionObserver::Quit() { |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| void RenderFrameSubmissionObserver::Wait() { |
| base::RunLoop run_loop{base::RunLoop::Type::kNestableTasksAllowed}; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| void RenderFrameSubmissionObserver:: |
| OnRenderFrameMetadataChangedBeforeActivation( |
| const cc::RenderFrameMetadata& metadata) {} |
| |
| void RenderFrameSubmissionObserver::OnRenderFrameMetadataChangedAfterActivation( |
| base::TimeTicks activation_time) { |
| Quit(); |
| if (metadata_change_closure_) { |
| std::move(metadata_change_closure_).Run(); |
| } |
| } |
| |
| void RenderFrameSubmissionObserver::OnRenderFrameSubmission() { |
| render_frame_count_++; |
| if (break_on_any_frame_) { |
| Quit(); |
| } |
| } |
| |
| void RenderFrameSubmissionObserver::OnLocalSurfaceIdChanged( |
| const cc::RenderFrameMetadata& metadata) {} |
| |
| MainThreadFrameObserver::MainThreadFrameObserver( |
| RenderWidgetHost* render_widget_host) |
| : render_widget_host_(render_widget_host), |
| routing_id_(render_widget_host_->GetProcess()->GetNextRoutingID()) {} |
| |
| MainThreadFrameObserver::~MainThreadFrameObserver() = default; |
| |
| void MainThreadFrameObserver::Wait() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| static_cast<RenderWidgetHostImpl*>(render_widget_host_) |
| ->InsertVisualStateCallback(base::BindOnce(&MainThreadFrameObserver::Quit, |
| base::Unretained(this))); |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| void MainThreadFrameObserver::Quit(bool) { |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| InputMsgWatcher::InputMsgWatcher(RenderWidgetHost* render_widget_host, |
| blink::WebInputEvent::Type type) |
| : render_widget_host_(render_widget_host), |
| wait_for_type_(type), |
| ack_result_(blink::mojom::InputEventResultState::kUnknown), |
| ack_source_(blink::mojom::InputEventResultSource::kUnknown) { |
| render_widget_host->AddInputEventObserver(this); |
| } |
| |
| InputMsgWatcher::~InputMsgWatcher() { |
| render_widget_host_->RemoveInputEventObserver(this); |
| } |
| |
| void InputMsgWatcher::OnInputEventAck( |
| const RenderWidgetHost& widget, |
| blink::mojom::InputEventResultSource ack_source, |
| blink::mojom::InputEventResultState ack_state, |
| const blink::WebInputEvent& event) { |
| if (event.GetType() == wait_for_type_) { |
| ack_result_ = ack_state; |
| ack_source_ = ack_source; |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| } |
| |
| void InputMsgWatcher::OnInputEvent(const RenderWidgetHost& widget, |
| const blink::WebInputEvent& event) { |
| last_sent_event_type_ = event.GetType(); |
| } |
| |
| bool InputMsgWatcher::HasReceivedAck() const { |
| return ack_result_ != blink::mojom::InputEventResultState::kUnknown; |
| } |
| |
| blink::mojom::InputEventResultState InputMsgWatcher::WaitForAck() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| return ack_result_; |
| } |
| |
| blink::mojom::InputEventResultState |
| InputMsgWatcher::GetAckStateWaitIfNecessary() { |
| if (HasReceivedAck()) { |
| return ack_result_; |
| } |
| return WaitForAck(); |
| } |
| |
| InputEventAckWaiter::InputEventAckWaiter(RenderWidgetHost* render_widget_host, |
| InputEventAckPredicate predicate) |
| : render_widget_host_( |
| static_cast<RenderWidgetHostImpl*>(render_widget_host)->GetWeakPtr()), |
| predicate_(predicate) { |
| render_widget_host_->AddInputEventObserver(this); |
| } |
| |
| namespace { |
| InputEventAckWaiter::InputEventAckPredicate EventAckHasType( |
| blink::WebInputEvent::Type type) { |
| return base::BindRepeating( |
| [](blink::WebInputEvent::Type expected_type, |
| blink::mojom::InputEventResultSource source, |
| blink::mojom::InputEventResultState state, |
| const blink::WebInputEvent& event) { |
| return event.GetType() == expected_type; |
| }, |
| type); |
| } |
| } // namespace |
| |
| InputEventAckWaiter::InputEventAckWaiter(RenderWidgetHost* render_widget_host, |
| blink::WebInputEvent::Type type) |
| : InputEventAckWaiter(render_widget_host, EventAckHasType(type)) {} |
| |
| InputEventAckWaiter::~InputEventAckWaiter() { |
| if (render_widget_host_) { |
| render_widget_host_->RemoveInputEventObserver(this); |
| } |
| } |
| |
| void InputEventAckWaiter::Wait() { |
| if (!event_received_) { |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| } |
| |
| void InputEventAckWaiter::Reset() { |
| event_received_ = false; |
| quit_closure_ = base::OnceClosure(); |
| } |
| |
| void InputEventAckWaiter::OnInputEventAck( |
| const RenderWidgetHost& widget, |
| blink::mojom::InputEventResultSource source, |
| blink::mojom::InputEventResultState state, |
| const blink::WebInputEvent& event) { |
| if (predicate_.Run(source, state, event)) { |
| event_received_ = true; |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| } |
| |
| // TODO(dcheng): Make the test clipboard on different threads share the |
| // same backing store. crbug.com/629765 |
| // TODO(slangley): crbug.com/775830 - Cleanup BrowserTestClipboardScope now that |
| // there is no need to thread hop for Windows. |
| BrowserTestClipboardScope::BrowserTestClipboardScope() { |
| ui::TestClipboard::CreateForCurrentThread(); |
| } |
| |
| BrowserTestClipboardScope::~BrowserTestClipboardScope() { |
| ui::Clipboard::DestroyClipboardForCurrentThread(); |
| } |
| |
| void BrowserTestClipboardScope::SetRtf(const std::string& rtf) { |
| ui::ScopedClipboardWriter clipboard_writer(ui::ClipboardBuffer::kCopyPaste); |
| clipboard_writer.WriteRTF(rtf); |
| } |
| |
| void BrowserTestClipboardScope::SetText(const std::string& text) { |
| ui::ScopedClipboardWriter clipboard_writer(ui::ClipboardBuffer::kCopyPaste); |
| clipboard_writer.WriteText(base::ASCIIToUTF16(text)); |
| } |
| |
| void BrowserTestClipboardScope::GetText(std::string* result) { |
| ui::Clipboard::GetForCurrentThread()->ReadAsciiText( |
| ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, result); |
| } |
| |
| class FrameFocusedObserver::FrameTreeNodeObserverImpl |
| : public FrameTreeNode::Observer { |
| public: |
| explicit FrameTreeNodeObserverImpl(FrameTreeNode* owner) : owner_(owner) { |
| owner->AddObserver(this); |
| } |
| ~FrameTreeNodeObserverImpl() override { |
| if (owner_) { |
| owner_->RemoveObserver(this); |
| } |
| } |
| |
| void Run() { run_loop_.Run(); } |
| |
| void OnFrameTreeNodeFocused(FrameTreeNode* node) override { |
| if (node == owner_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| void OnFrameTreeNodeDestroyed(FrameTreeNode* node) override { |
| if (node == owner_) { |
| owner_ = nullptr; |
| } |
| } |
| |
| private: |
| raw_ptr<FrameTreeNode> owner_; |
| base::RunLoop run_loop_; |
| }; |
| |
| FrameFocusedObserver::FrameFocusedObserver(RenderFrameHost* owner_host) |
| : impl_(std::make_unique<FrameTreeNodeObserverImpl>( |
| static_cast<RenderFrameHostImpl*>(owner_host)->frame_tree_node())) {} |
| |
| FrameFocusedObserver::~FrameFocusedObserver() = default; |
| |
| void FrameFocusedObserver::Wait() { |
| impl_->Run(); |
| } |
| |
| class FrameDeletedObserver::FrameTreeNodeObserverImpl |
| : public FrameTreeNode::Observer { |
| public: |
| explicit FrameTreeNodeObserverImpl(FrameTreeNode* owner) |
| : frame_tree_node_id_(owner->frame_tree_node_id()), owner_(owner) { |
| owner->AddObserver(this); |
| } |
| ~FrameTreeNodeObserverImpl() override = default; |
| |
| void Run() { |
| if (!IsDestroyed()) { |
| run_loop_.Run(); |
| } |
| } |
| |
| bool IsDestroyed() const { return owner_ == nullptr; } |
| |
| FrameTreeNodeId frame_tree_node_id() const { return frame_tree_node_id_; } |
| |
| private: |
| // FrameTreeNode::Observer: |
| void OnFrameTreeNodeDestroyed(FrameTreeNode* node) override { |
| if (node == owner_) { |
| owner_ = nullptr; |
| run_loop_.Quit(); |
| } |
| } |
| |
| const FrameTreeNodeId frame_tree_node_id_; |
| raw_ptr<FrameTreeNode> owner_; |
| base::RunLoop run_loop_; |
| }; |
| |
| FrameDeletedObserver::FrameDeletedObserver(RenderFrameHost* owner_host) |
| : impl_(std::make_unique<FrameTreeNodeObserverImpl>( |
| static_cast<RenderFrameHostImpl*>(owner_host)->frame_tree_node())) {} |
| |
| FrameDeletedObserver::~FrameDeletedObserver() = default; |
| |
| void FrameDeletedObserver::Wait() { |
| impl_->Run(); |
| } |
| |
| bool FrameDeletedObserver::IsDeleted() const { |
| return impl_->IsDestroyed(); |
| } |
| |
| FrameTreeNodeId FrameDeletedObserver::GetFrameTreeNodeId() const { |
| return impl_->frame_tree_node_id(); |
| } |
| |
| TestNavigationManager::TestNavigationManager(WebContents* web_contents, |
| const GURL& url) |
| : WebContentsObserver(web_contents), url_(url) {} |
| |
| TestNavigationManager::~TestNavigationManager() { |
| ResumeIfPaused(); |
| } |
| |
| bool TestNavigationManager::WaitForFirstYieldAfterDidStartNavigation() { |
| TRACE_EVENT( |
| "test", |
| "TestNavigationManager::WaitForFirstYieldAfterDidStartNavigation"); |
| if (current_state_ >= NavigationState::WILL_START) { |
| return true; |
| } |
| |
| DCHECK_EQ(desired_state_, NavigationState::WILL_START); |
| // Ignore the result because DidStartNavigation will update |desired_state_| |
| // we check below. |
| (void)WaitForDesiredState(); |
| // This returns false if the runloop was terminated by a timeout rather than |
| // reaching the |WILL_START|. |
| return current_state_ >= NavigationState::WILL_START; |
| } |
| |
| bool TestNavigationManager::WaitForRequestStart() { |
| TRACE_EVENT("test", "TestNavigationManager::WaitForRequestStart"); |
| desired_state_ = NavigationState::REQUEST_STARTED; |
| return WaitForDesiredState(); |
| } |
| |
| bool TestNavigationManager::WaitForLoaderStart() { |
| TRACE_EVENT("test", "TestNavigationManager::WaitForLoaderStart"); |
| desired_state_ = NavigationState::LOADER_STARTED; |
| return WaitForDesiredState(); |
| } |
| |
| bool TestNavigationManager::WaitForRequestRedirected() { |
| desired_state_ = NavigationState::REDIRECTED; |
| return WaitForDesiredState(); |
| } |
| |
| void TestNavigationManager::ResumeNavigation() { |
| TRACE_EVENT("test", "TestNavigationManager::ResumeNavigation"); |
| CHECK(current_state_ == NavigationState::REQUEST_STARTED || |
| current_state_ == NavigationState::REDIRECTED || |
| current_state_ == NavigationState::RESPONSE); |
| CHECK_EQ(current_state_, desired_state_); |
| CHECK(navigation_paused_); |
| ResumeIfPaused(); |
| } |
| |
| NavigationHandle* TestNavigationManager::GetNavigationHandle() { |
| return request_; |
| } |
| |
| ukm::SourceId TestActivationManager::next_page_ukm_source_id() const { |
| EXPECT_NE(ukm::kInvalidSourceId, next_page_ukm_source_id_); |
| return next_page_ukm_source_id_; |
| } |
| |
| bool TestNavigationManager::WaitForResponse() { |
| TRACE_EVENT("test", "TestNavigationManager::WaitForResponse"); |
| desired_state_ = NavigationState::RESPONSE; |
| return WaitForDesiredState(); |
| } |
| |
| bool TestNavigationManager::WaitForNavigationFinished() { |
| TRACE_EVENT("test", "TestNavigationManager::WaitForNavigationFinished"); |
| desired_state_ = NavigationState::FINISHED; |
| return WaitForDesiredState(); |
| } |
| |
| void TestNavigationManager::WaitForSpeculativeRenderFrameHostCreation() { |
| TRACE_EVENT( |
| "test", |
| "TestNavigationManager::WaitForSpeculativeRenderFrameHostCreation"); |
| if (current_state_ < NavigationState::REQUEST_STARTED) { |
| CHECK(WaitForRequestStart()); |
| } |
| if (!speculative_rfh_created_) { |
| base::RunLoop run_loop(message_loop_type_); |
| wait_rfh_closure_ = run_loop.QuitClosure(); |
| ResumeNavigation(); |
| run_loop.Run(); |
| } |
| } |
| |
| void TestNavigationManager::DidStartNavigation(NavigationHandle* handle) { |
| if (!ShouldMonitorNavigation(handle)) { |
| return; |
| } |
| |
| DCHECK(!handle->IsPageActivation()) |
| << "For PageActivating navigations, use TestActivationManager."; |
| |
| request_ = NavigationRequest::From(handle); |
| NavigationThrottleRegistry& registry = |
| *request_->GetNavigationThrottleRegistryForTesting(); |
| registry.AddThrottle(std::make_unique<TestNavigationManagerThrottle>( |
| registry, |
| base::BindOnce(&TestNavigationManager::OnWillStartRequest, |
| weak_factory_.GetWeakPtr()), |
| base::BindRepeating(&TestNavigationManager::OnWillRedirectRequest, |
| weak_factory_.GetWeakPtr()), |
| base::BindOnce(&TestNavigationManager::OnWillProcessResponse, |
| weak_factory_.GetWeakPtr()))); |
| |
| current_state_ = NavigationState::WILL_START; |
| |
| OnNavigationStateChanged(); |
| |
| // This is the default desired state. A browser-initiated navigation can |
| // reach WillStartRequest state synchronously, so the TestNavigationManager |
| // is set to always pause navigations at WillStartRequest. This ensures the |
| // navigation will defer and the user can always call |
| // WaitForRequestStart. |
| if (!request_->IsPageActivation() && |
| desired_state_ == NavigationState::WILL_START) { |
| desired_state_ = NavigationState::REQUEST_STARTED; |
| } |
| } |
| |
| void TestNavigationManager::DidUpdateNavigationHandleTiming( |
| NavigationHandle* handle) { |
| if (handle != request_ || |
| handle->GetNavigationHandleTiming().loader_start_time.is_null() || |
| current_state_ >= NavigationState::LOADER_STARTED) { |
| return; |
| } |
| |
| CHECK(!handle->IsPageActivation()) |
| << "For PageActivating navigations, use TestActivationManager."; |
| |
| current_state_ = NavigationState::LOADER_STARTED; |
| |
| OnNavigationStateChanged(); |
| } |
| |
| void TestNavigationManager::DidRedirectNavigation(NavigationHandle* handle) { |
| if (handle != request_) { |
| return; |
| } |
| |
| CHECK(!handle->IsPageActivation()) |
| << "For PageActivating navigations, use TestActivationManager."; |
| |
| current_state_ = NavigationState::REDIRECTED; |
| |
| OnNavigationStateChanged(); |
| } |
| |
| void TestNavigationManager::DidFinishNavigation(NavigationHandle* handle) { |
| if (handle != request_) { |
| return; |
| } |
| was_committed_ = handle->HasCommitted(); |
| was_successful_ = was_committed_ && !handle->IsErrorPage(); |
| current_state_ = NavigationState::FINISHED; |
| navigation_paused_ = false; |
| request_ = nullptr; |
| |
| // Invalidate the WeakPtrs so that the throttle callbacks will not be |
| // called after this point. We need to do this because |
| // TestNavigationManagerThrottle posts tasks for these callbacks and we |
| // may get a call to this function before those tasks run. |
| weak_factory_.InvalidateWeakPtrs(); |
| OnNavigationStateChanged(); |
| } |
| |
| void TestNavigationManager::OnWillStartRequest() { |
| current_state_ = NavigationState::REQUEST_STARTED; |
| navigation_paused_ = true; |
| OnNavigationStateChanged(); |
| } |
| |
| void TestNavigationManager::OnWillRedirectRequest() { |
| current_state_ = NavigationState::REDIRECTED; |
| navigation_paused_ = true; |
| OnNavigationStateChanged(); |
| } |
| |
| void TestNavigationManager::OnWillProcessResponse() { |
| current_state_ = NavigationState::RESPONSE; |
| navigation_paused_ = true; |
| OnNavigationStateChanged(); |
| } |
| |
| void TestNavigationManager::RenderFrameCreated( |
| RenderFrameHost* render_frame_host) { |
| RenderFrameHostImpl* host_impl = |
| static_cast<RenderFrameHostImpl*>(render_frame_host); |
| NavigationRequest* request = |
| host_impl->frame_tree_node()->navigation_request(); |
| if (host_impl->lifecycle_state() == |
| RenderFrameHostImpl::LifecycleStateImpl::kSpeculative && |
| IsRequestCompatibleWithSpeculativeRFH(request) && |
| request->GetURL() == url_ && |
| (request == request_ || request_ == nullptr)) { |
| DCHECK(host_impl->frame_tree_node()->HasNavigation()); |
| speculative_rfh_created_ = true; |
| created_speculative_rfh_ = |
| std::make_unique<RenderFrameHostWrapper>(render_frame_host); |
| if (wait_rfh_closure_) { |
| std::move(wait_rfh_closure_).Run(); |
| } |
| } |
| } |
| |
| RenderFrameHost* TestNavigationManager::GetCreatedSpeculativeRFH() { |
| if (!created_speculative_rfh_) { |
| return nullptr; |
| } |
| return created_speculative_rfh_->get(); |
| } |
| |
| // TODO(csharrison): Remove CallResumeForTesting method calls in favor of doing |
| // it through the throttle. |
| bool TestNavigationManager::WaitForDesiredState() { |
| // If the desired state has already been reached, just return. |
| if (current_state_ == desired_state_) { |
| return true; |
| } |
| |
| // Resume the navigation if it was paused. |
| ResumeIfPaused(); |
| |
| // Wait for the desired state if needed. |
| if (current_state_ < desired_state_) { |
| DCHECK(!state_quit_closure_); |
| base::RunLoop run_loop(message_loop_type_); |
| state_quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| // Return false if the navigation did not reach the state specified by the |
| // user. |
| return current_state_ == desired_state_; |
| } |
| |
| void TestNavigationManager::OnNavigationStateChanged() { |
| TRACE_EVENT("test", "TestNavigationManager::OnNavigationStateChanged", "this", |
| this); |
| |
| // If the state the user was waiting for has been reached, exit the message |
| // loop. |
| if (current_state_ >= desired_state_) { |
| if (state_quit_closure_) { |
| std::move(state_quit_closure_).Run(); |
| } |
| return; |
| } |
| |
| // Otherwise, the navigation should be resumed if it was previously paused. |
| ResumeIfPaused(); |
| } |
| |
| void TestNavigationManager::ResumeIfPaused() { |
| TRACE_EVENT("test", "TestNavigationManager::ResumeIfPaused", "this", this); |
| |
| if (!navigation_paused_) { |
| return; |
| } |
| |
| navigation_paused_ = false; |
| |
| auto* registry = request_->GetNavigationThrottleRegistryForTesting(); |
| ASSERT_EQ(1u, registry->GetDeferringThrottles().size()); |
| registry->ResumeProcessingNavigationEvent( |
| *registry->GetDeferringThrottles().cbegin()); |
| } |
| |
| bool TestNavigationManager::ShouldMonitorNavigation(NavigationHandle* handle) { |
| if (request_ || handle->GetURL() != url_) { |
| return false; |
| } |
| if (current_state_ != NavigationState::INITIAL) { |
| return false; |
| } |
| return true; |
| } |
| |
| void TestNavigationManager::AllowNestableTasks() { |
| message_loop_type_ = base::RunLoop::Type::kNestableTasksAllowed; |
| } |
| |
| void TestNavigationManager::WriteIntoTrace( |
| perfetto::TracedValue context) const { |
| perfetto::TracedDictionary dict = std::move(context).WriteDictionary(); |
| dict.Add("url", url_); |
| dict.Add("navigation_request", request_); |
| dict.Add("navigation_paused", navigation_paused_); |
| dict.Add("current_state", current_state_); |
| dict.Add("desired_state", desired_state_); |
| } |
| |
| namespace { |
| |
| // A helper CommitDeferringCondition instantiated and inserted into all |
| // navigations from TestActivationManager. It delegates WillCommitNavigation |
| // method of the CommitDeferringCondition back to the TestActivationManager so |
| // that the manager can see and decide how to proceed with the condition for |
| // every occurring navigation. |
| class TestActivationManagerCondition : public CommitDeferringCondition { |
| using WillCommitCallback = |
| base::OnceCallback<Result(CommitDeferringCondition&, base::OnceClosure)>; |
| |
| public: |
| TestActivationManagerCondition(NavigationHandle& handle, |
| WillCommitCallback on_will_commit_navigation) |
| : CommitDeferringCondition(handle), |
| on_will_commit_navigation_(std::move(on_will_commit_navigation)) {} |
| ~TestActivationManagerCondition() override = default; |
| |
| TestActivationManagerCondition(const TestActivationManagerCondition&) = |
| delete; |
| TestActivationManagerCondition& operator=( |
| const TestActivationManagerCondition&) = delete; |
| |
| Result WillCommitNavigation(base::OnceClosure resume) override { |
| return std::move(on_will_commit_navigation_).Run(*this, std::move(resume)); |
| } |
| const char* TraceEventName() const override { |
| return "TestActivationManagerCondition"; |
| } |
| |
| private: |
| WillCommitCallback on_will_commit_navigation_; |
| }; |
| |
| // We need this wrapper since the TestActivationManager can be destroyed while |
| // navigations are ongoing so we need to pass the callback with a WeakPtr. |
| // However, we can't bind a WeakPtr to a method that returns non-void so we use |
| // this wrapper to provide the default return value. |
| CommitDeferringCondition::Result ConditionCallbackWeakWrapper( |
| base::WeakPtr<TestActivationManager> manager, |
| base::RepeatingCallback< |
| CommitDeferringCondition::Result(TestActivationManager*, |
| CommitDeferringCondition&, |
| base::OnceClosure)> manager_func, |
| CommitDeferringCondition& condition, |
| base::OnceClosure resume_callback) { |
| // If the manager was destroyed, we don't need to pause navigation any longer |
| // so just proceed. |
| if (!manager) { |
| return CommitDeferringCondition::Result::kProceed; |
| } |
| |
| return manager_func.Run(manager.get(), condition, std::move(resume_callback)); |
| } |
| |
| } // namespace |
| |
| class TestActivationManager::ConditionInserter { |
| using WillCommitCallback = |
| base::RepeatingCallback<CommitDeferringCondition::Result( |
| CommitDeferringCondition&, |
| base::OnceClosure)>; |
| |
| public: |
| explicit ConditionInserter(WillCommitCallback callback, |
| CommitDeferringConditionRunner::InsertOrder order) |
| : condition_callback_(std::move(callback)), |
| generator_id_( |
| CommitDeferringConditionRunner::InstallConditionGeneratorForTesting( |
| base::BindRepeating(&ConditionInserter::Install, |
| base::Unretained(this)), |
| order)) {} |
| ~ConditionInserter() { |
| CommitDeferringConditionRunner::UninstallConditionGeneratorForTesting( |
| generator_id_); |
| } |
| |
| private: |
| std::unique_ptr<CommitDeferringCondition> Install( |
| NavigationHandle& handle, |
| CommitDeferringCondition::NavigationType navigation_type) { |
| // TestActivationManager should only pause during checks for an activating |
| // navigation. It's possible for a navigation to start off as activating |
| // but become non-activating after the initial checks. In that case, |
| // CommitDeferringConditions will be run a second time as non-activating so |
| // we'll avoid registering the second time with this early out. |
| // TODO(bokan): We can't check navigation_type here because BFCache is |
| // considered kOther. Ideally all page activations would be a single type. |
| // crbug.com/1226442. |
| auto* request = NavigationRequest::From(&handle); |
| if (!request->IsServedFromBackForwardCache() && |
| !request->is_running_potential_prerender_activation_checks()) { |
| return nullptr; |
| } |
| |
| return std::make_unique<TestActivationManagerCondition>( |
| handle, condition_callback_); |
| } |
| |
| WillCommitCallback condition_callback_; |
| const int generator_id_; |
| }; |
| |
| TestActivationManager::TestActivationManager(WebContents* web_contents, |
| const GURL& url) |
| : WebContentsObserver(web_contents), url_(url) { |
| first_condition_inserter_ = std::make_unique<ConditionInserter>( |
| base::BindRepeating( |
| &ConditionCallbackWeakWrapper, weak_factory_.GetWeakPtr(), |
| base::BindRepeating(&TestActivationManager::FirstConditionCallback)), |
| CommitDeferringConditionRunner::InsertOrder::kBefore); |
| last_condition_inserter_ = std::make_unique<ConditionInserter>( |
| base::BindRepeating( |
| &ConditionCallbackWeakWrapper, weak_factory_.GetWeakPtr(), |
| base::BindRepeating(&TestActivationManager::LastConditionCallback)), |
| CommitDeferringConditionRunner::InsertOrder::kAfter); |
| } |
| |
| TestActivationManager::~TestActivationManager() { |
| DCHECK(!quit_closure_); |
| if (is_paused()) { |
| std::move(resume_callback_).Run(); |
| } |
| } |
| |
| bool TestActivationManager::WaitForBeforeChecks() { |
| TRACE_EVENT("test", "TestActivationManager::WaitForBeforeChecks"); |
| desired_state_ = ActivationState::kBeforeChecks; |
| return WaitForDesiredState(); |
| } |
| |
| bool TestActivationManager::WaitForAfterChecks() { |
| TRACE_EVENT("test", "TestActivationManager::WaitForAfterChecks"); |
| desired_state_ = ActivationState::kAfterChecks; |
| return WaitForDesiredState(); |
| } |
| |
| void TestActivationManager::WaitForNavigationFinished() { |
| TRACE_EVENT("test", "TestActivationManager::WaitForNavigationFinished"); |
| desired_state_ = ActivationState::kFinished; |
| bool finished = WaitForDesiredState(); |
| DCHECK(finished); |
| } |
| |
| void TestActivationManager::ResumeActivation() { |
| TRACE_EVENT("test", "TestActivationManager::ResumeActivation"); |
| DCHECK(is_paused()); |
| |
| // Set desired_state_ to kFinished so the navigation can proceed to finish |
| // unless it yields somewhere and/or the caller calls another WaitFor method. |
| desired_state_ = ActivationState::kFinished; |
| std::move(resume_callback_).Run(); |
| } |
| |
| NavigationHandle* TestActivationManager::GetNavigationHandle() { |
| return request_; |
| } |
| |
| void TestActivationManager::SetCallbackCalledAfterActivationIsReady( |
| base::OnceClosure callback) { |
| DCHECK(!callback_in_last_condition); |
| callback_in_last_condition = std::move(callback); |
| } |
| |
| CommitDeferringCondition::Result TestActivationManager::FirstConditionCallback( |
| CommitDeferringCondition& condition, |
| base::OnceClosure resume_callback) { |
| if (condition.GetNavigationHandle().GetURL() != url_) { |
| return CommitDeferringCondition::Result::kProceed; |
| } |
| |
| DCHECK(!is_tracking_activation_) |
| << "Second request for watched URL: " << url_; |
| is_tracking_activation_ = true; |
| |
| DCHECK(!request_); |
| request_ = NavigationRequest::From(&condition.GetNavigationHandle()); |
| DCHECK(request_->is_running_potential_prerender_activation_checks() || |
| request_->IsServedFromBackForwardCache()) |
| << "TestActivationManager should only be used for for page " |
| "activations. For regular navigations, use TestNavigationManager."; |
| |
| DCHECK_EQ(current_state_, ActivationState::kInitial); |
| current_state_ = ActivationState::kBeforeChecks; |
| |
| if (current_state_ < desired_state_) { |
| return CommitDeferringCondition::Result::kProceed; |
| } |
| |
| resume_callback_ = std::move(resume_callback); |
| StopWaitingIfNeeded(); |
| |
| // Always defer here so test code gets a chance to set WaitFor... before the |
| // navigation finishes. |
| return CommitDeferringCondition::Result::kDefer; |
| } |
| |
| CommitDeferringCondition::Result TestActivationManager::LastConditionCallback( |
| CommitDeferringCondition& condition, |
| base::OnceClosure resume_callback) { |
| if (callback_in_last_condition) { |
| std::move(callback_in_last_condition).Run(); |
| } |
| |
| if (request_ != &condition.GetNavigationHandle()) { |
| return CommitDeferringCondition::Result::kProceed; |
| } |
| |
| DCHECK(is_tracking_activation_); |
| |
| current_state_ = ActivationState::kAfterChecks; |
| if (current_state_ < desired_state_) { |
| return CommitDeferringCondition::Result::kProceed; |
| } |
| |
| resume_callback_ = std::move(resume_callback); |
| StopWaitingIfNeeded(); |
| |
| return CommitDeferringCondition::Result::kDefer; |
| } |
| |
| void TestActivationManager::DidFinishNavigation(NavigationHandle* handle) { |
| if (handle == request_) { |
| DCHECK(is_tracking_activation_); |
| was_committed_ = handle->HasCommitted(); |
| was_successful_ = was_committed_ && !handle->IsErrorPage(); |
| was_activated_ = was_successful_ && handle->IsPageActivation(); |
| next_page_ukm_source_id_ = handle->GetNextPageUkmSourceId(); |
| request_ = nullptr; |
| current_state_ = ActivationState::kFinished; |
| StopWaitingIfNeeded(); |
| } |
| } |
| |
| bool TestActivationManager::WaitForDesiredState() { |
| DCHECK_LE(current_state_, desired_state_); |
| |
| // If the desired state has already been reached, just return. |
| if (current_state_ == desired_state_) { |
| return true; |
| } |
| |
| // Resume the navigation if it was paused. |
| if (is_paused()) { |
| ResumeActivation(); |
| } |
| |
| // Wait for the desired state if needed. |
| if (current_state_ < desired_state_) { |
| DCHECK(!quit_closure_); |
| base::RunLoop run_loop(base::RunLoop::Type::kDefault); |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| // Return false if the navigation did not reach the state specified by the |
| // user. |
| return current_state_ == desired_state_; |
| } |
| |
| void TestActivationManager::StopWaitingIfNeeded() { |
| if (current_state_ == desired_state_ && quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| NavigationHandleCommitObserver::NavigationHandleCommitObserver( |
| WebContents* web_contents, |
| const GURL& url) |
| : WebContentsObserver(web_contents), url_(url) {} |
| |
| void NavigationHandleCommitObserver::DidFinishNavigation( |
| NavigationHandle* handle) { |
| if (handle->GetURL() != url_) { |
| return; |
| } |
| has_committed_ = true; |
| was_same_document_ = handle->IsSameDocument(); |
| was_renderer_initiated_ = handle->IsRendererInitiated(); |
| navigation_type_ = |
| NavigationRequest::From(handle)->common_params().navigation_type; |
| } |
| |
| WebContentsConsoleObserver::WebContentsConsoleObserver( |
| WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| WebContentsConsoleObserver::~WebContentsConsoleObserver() = default; |
| |
| bool WebContentsConsoleObserver::Wait() { |
| return waiter_helper_.Wait(); |
| } |
| |
| void WebContentsConsoleObserver::SetFilter(Filter filter) { |
| filter_ = std::move(filter); |
| } |
| |
| void WebContentsConsoleObserver::SetPattern(std::string pattern) { |
| DCHECK(!pattern.empty()) << "An empty pattern will never match."; |
| pattern_ = std::move(pattern); |
| } |
| |
| std::string WebContentsConsoleObserver::GetMessageAt(size_t index) const { |
| if (index >= messages_.size()) { |
| ADD_FAILURE() << "Tried to retrieve a non-existent message at index: " |
| << index; |
| return std::string(); |
| } |
| return base::UTF16ToUTF8(messages_[index].message); |
| } |
| |
| void WebContentsConsoleObserver::OnDidAddMessageToConsole( |
| RenderFrameHost* source_frame, |
| blink::mojom::ConsoleMessageLevel log_level, |
| const std::u16string& message_contents, |
| int32_t line_no, |
| const std::u16string& source_id, |
| const std::optional<std::u16string>& untrusted_stack_trace) { |
| Message message( |
| {source_frame, log_level, message_contents, line_no, source_id}); |
| if (filter_ && !filter_.Run(message)) { |
| return; |
| } |
| |
| if (!pattern_.empty() && |
| !base::MatchPattern(base::UTF16ToUTF8(message_contents), pattern_)) { |
| return; |
| } |
| |
| messages_.push_back(std::move(message)); |
| waiter_helper_.OnEvent(); |
| } |
| |
| namespace { |
| static constexpr int kEnableLogMessageId = 0; |
| static constexpr char kEnableLogMessage[] = R"({"id":0,"method":"Log.enable"})"; |
| static constexpr int kDisableLogMessageId = 1; |
| static constexpr char kDisableLogMessage[] = |
| R"({"id":1,"method":"Log.disable"})"; |
| |
| static constexpr int kEnableMediaMessageId = 2; |
| static constexpr char kEnableMediaMessage[] = |
| R"({"id":2,"method":"Media.enable"})"; |
| static constexpr int kDisableMediaMessageId = 3; |
| static constexpr char kDisableMediaMessage[] = |
| R"({"id":3,"method":"Media.disable"})"; |
| } // namespace |
| |
| DevToolsInspectorLogWatcher::DevToolsInspectorLogWatcher( |
| WebContents* web_contents, |
| Domain domain) { |
| host_ = DevToolsAgentHost::GetOrCreateFor(web_contents); |
| host_->AttachClient(this); |
| |
| switch (domain) { |
| case Domain::Log: |
| host_->DispatchProtocolMessage( |
| this, base::byte_span_from_cstring(kEnableLogMessage)); |
| break; |
| case Domain::Media: |
| host_->DispatchProtocolMessage( |
| this, base::byte_span_from_cstring(kEnableMediaMessage)); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| run_loop_enable_log_.Run(); |
| domain_ = domain; |
| } |
| |
| DevToolsInspectorLogWatcher::~DevToolsInspectorLogWatcher() { |
| host_->DetachClient(this); |
| } |
| |
| void DevToolsInspectorLogWatcher::DispatchProtocolMessage( |
| DevToolsAgentHost* host, |
| base::span<const uint8_t> message) { |
| std::string_view message_str(reinterpret_cast<const char*>(message.data()), |
| message.size()); |
| auto parsed_message = |
| std::move(base::JSONReader::Read(message_str)->GetDict()); |
| std::optional<int> command_id = parsed_message.FindInt("id"); |
| if (command_id.has_value()) { |
| switch (command_id.value()) { |
| case kEnableLogMessageId: |
| case kEnableMediaMessageId: |
| run_loop_enable_log_.Quit(); |
| break; |
| case kDisableLogMessageId: |
| case kDisableMediaMessageId: |
| run_loop_disable_log_.Quit(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| return; |
| } |
| |
| std::string* notification = parsed_message.FindString("method"); |
| if (!notification) { |
| return; |
| } |
| |
| if (*notification == "Log.entryAdded") { |
| std::string* text = |
| parsed_message.FindStringByDottedPath("params.entry.text"); |
| DCHECK(text); |
| last_message_ = *text; |
| std::string* url = |
| parsed_message.FindStringByDottedPath("params.entry.url"); |
| if (url) { |
| last_url_ = GURL(*url); |
| } |
| } |
| |
| if (notification->find("Media.") != std::string::npos) { |
| last_media_notification_ = *notification; |
| |
| if (*notification == "Media.playerEventsAdded") { |
| bool last_auto_pip_event_info_set = false; |
| const base::Value::List* events = |
| parsed_message.FindListByDottedPath("params.events"); |
| if (events) { |
| for (const base::Value& event : *events) { |
| const auto* dict = event.GetIfDict(); |
| if (!dict) { |
| continue; |
| } |
| const std::string* text = dict->FindString("value"); |
| if ((text != nullptr) && |
| ((*text).find("auto_picture_in_picture_info") != |
| std::string::npos)) { |
| last_auto_picture_in_picture_event_info_ = *text; |
| last_auto_pip_event_info_set = true; |
| } |
| } |
| } |
| if (last_auto_pip_event_info_set) { |
| NotifyLastAutoPipEventInfoSet(); |
| } |
| } |
| } |
| } |
| |
| void DevToolsInspectorLogWatcher::AgentHostClosed(DevToolsAgentHost* host) {} |
| |
| void DevToolsInspectorLogWatcher::FlushAndStopWatching() { |
| switch (domain_) { |
| case Domain::Log: |
| host_->DispatchProtocolMessage( |
| this, base::byte_span_from_cstring(kDisableLogMessage)); |
| break; |
| case Domain::Media: |
| host_->DispatchProtocolMessage( |
| this, base::byte_span_from_cstring(kDisableMediaMessage)); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| run_loop_disable_log_.Run(); |
| } |
| |
| void DevToolsInspectorLogWatcher::NotifyLastAutoPipEventInfoSet() { |
| for (DevToolsInspectorLogWatcherObserver& obs : observers_) { |
| obs.OnLastAutoPipEventInfoSet(); |
| } |
| } |
| |
| namespace { |
| mojo::Remote<blink::mojom::FileSystemManager> GetFileSystemManager( |
| RenderProcessHost* rph, |
| const blink::StorageKey& storage_key) { |
| FileSystemManagerImpl* file_system = static_cast<RenderProcessHostImpl*>(rph) |
| ->GetFileSystemManagerForTesting(); |
| mojo::Remote<blink::mojom::FileSystemManager> file_system_manager_remote; |
| GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FileSystemManagerImpl::BindReceiver, |
| base::Unretained(file_system), storage_key, |
| file_system_manager_remote.BindNewPipeAndPassReceiver())); |
| return file_system_manager_remote; |
| } |
| } // namespace |
| |
| // static |
| void PwnMessageHelper::FileSystemCreate(RenderProcessHost* process, |
| int request_id, |
| GURL path, |
| bool exclusive, |
| bool is_directory, |
| bool recursive, |
| const blink::StorageKey& storage_key) { |
| TestFileapiOperationWaiter waiter; |
| mojo::Remote<blink::mojom::FileSystemManager> file_system_manager = |
| GetFileSystemManager(process, storage_key); |
| file_system_manager->Create( |
| path, exclusive, is_directory, recursive, |
| base::BindOnce(&TestFileapiOperationWaiter::DidCreate, |
| base::Unretained(&waiter))); |
| waiter.WaitForOperationToFinish(); |
| } |
| |
| // static |
| void PwnMessageHelper::FileSystemWrite(RenderProcessHost* process, |
| int request_id, |
| GURL file_path, |
| std::string blob_uuid, |
| int64_t position, |
| const blink::StorageKey& storage_key) { |
| TestFileapiOperationWaiter waiter; |
| mojo::Remote<blink::mojom::FileSystemManager> file_system_manager = |
| GetFileSystemManager(process, storage_key); |
| mojo::PendingRemote<blink::mojom::FileSystemOperationListener> listener; |
| mojo::Receiver<blink::mojom::FileSystemOperationListener> receiver( |
| &waiter, listener.InitWithNewPipeAndPassReceiver()); |
| mojo::Remote<blink::mojom::FileSystemCancellableOperation> op; |
| |
| file_system_manager->Write( |
| file_path, process->GetBrowserContext()->GetBlobRemote(blob_uuid), |
| position, op.BindNewPipeAndPassReceiver(), std::move(listener)); |
| waiter.WaitForOperationToFinish(); |
| } |
| |
| void PwnMessageHelper::OpenURL(RenderFrameHost* render_frame_host, |
| const GURL& url) { |
| auto params = blink::mojom::OpenURLParams::New(); |
| params->url = url; |
| params->disposition = WindowOpenDisposition::CURRENT_TAB; |
| params->should_replace_current_entry = false; |
| params->user_gesture = true; |
| static_cast<mojom::FrameHost*>( |
| static_cast<RenderFrameHostImpl*>(render_frame_host)) |
| ->OpenURL(std::move(params)); |
| } |
| |
| #if defined(USE_AURA) |
| namespace { |
| |
| // This class interacts with the internals of the DelegatedFrameHost without |
| // exposing them in the header. |
| class EvictionStateWaiter : public DelegatedFrameHost::Observer { |
| public: |
| explicit EvictionStateWaiter(DelegatedFrameHost* delegated_frame_host) |
| : delegated_frame_host_(delegated_frame_host) { |
| delegated_frame_host_->AddObserverForTesting(this); |
| } |
| |
| EvictionStateWaiter(const EvictionStateWaiter&) = delete; |
| EvictionStateWaiter& operator=(const EvictionStateWaiter&) = delete; |
| |
| ~EvictionStateWaiter() override { |
| delegated_frame_host_->RemoveObserverForTesting(this); |
| } |
| |
| void WaitForEvictionState(DelegatedFrameHost::FrameEvictionState state) { |
| if (delegated_frame_host_->frame_eviction_state() == state) { |
| return; |
| } |
| |
| waited_eviction_state_ = state; |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| // DelegatedFrameHost::Observer: |
| void OnFrameEvictionStateChanged( |
| DelegatedFrameHost::FrameEvictionState new_state) override { |
| if (!quit_closure_.is_null() && (new_state == waited_eviction_state_)) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| private: |
| raw_ptr<DelegatedFrameHost> delegated_frame_host_; |
| DelegatedFrameHost::FrameEvictionState waited_eviction_state_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| } // namespace |
| |
| void VerifyStaleContentOnFrameEviction( |
| RenderWidgetHostView* render_widget_host_view) { |
| auto* render_widget_host_view_aura = |
| static_cast<RenderWidgetHostViewAura*>(render_widget_host_view); |
| DelegatedFrameHost* delegated_frame_host = |
| render_widget_host_view_aura->GetDelegatedFrameHost(); |
| |
| // Initially there should be no stale content set. |
| EXPECT_FALSE( |
| delegated_frame_host->stale_content_layer()->has_external_content()); |
| EXPECT_EQ(delegated_frame_host->frame_eviction_state(), |
| DelegatedFrameHost::FrameEvictionState::kNotStarted); |
| |
| // Hide the view and evict the frame, and expect that stale content will be |
| // set. |
| EvictionStateWaiter waiter{delegated_frame_host}; |
| render_widget_host_view_aura->WasOccluded(); |
| static_cast<viz::FrameEvictorClient*>(delegated_frame_host) |
| ->EvictDelegatedFrame(delegated_frame_host->GetFrameEvictorForTesting() |
| ->CollectSurfaceIdsForEviction()); |
| EXPECT_EQ(delegated_frame_host->frame_eviction_state(), |
| DelegatedFrameHost::FrameEvictionState::kPendingEvictionRequests); |
| // Wait until the stale frame content is copied and set onto the layer, i.e. |
| // the eviction state changes from kPendingEvictionRequests back to |
| // kNotStarted. |
| waiter.WaitForEvictionState( |
| DelegatedFrameHost::FrameEvictionState::kNotStarted); |
| EXPECT_TRUE( |
| delegated_frame_host->stale_content_layer()->has_external_content()); |
| } |
| |
| #endif // defined(USE_AURA) |
| |
| // static |
| void BlobURLStoreInterceptor::Intercept(GURL target_url, |
| storage::BlobUrlRegistry* registry, |
| mojo::ReceiverId receiver_id) { |
| auto interceptor = base::WrapUnique(new BlobURLStoreInterceptor(target_url)); |
| auto* raw_interceptor = interceptor.get(); |
| auto impl = registry->receivers_for_testing().SwapImplForTesting( |
| receiver_id, std::move(interceptor)); |
| raw_interceptor->url_store_ = std::move(impl); |
| } |
| |
| blink::mojom::BlobURLStore* BlobURLStoreInterceptor::GetForwardingInterface() { |
| return url_store_.get(); |
| } |
| |
| void BlobURLStoreInterceptor::Register( |
| mojo::PendingRemote<blink::mojom::Blob> blob, |
| const GURL& url, |
| RegisterCallback callback) { |
| GetForwardingInterface()->Register(std::move(blob), target_url_, |
| std::move(callback)); |
| } |
| |
| BlobURLStoreInterceptor::BlobURLStoreInterceptor(GURL target_url) |
| : target_url_(target_url) {} |
| BlobURLStoreInterceptor::~BlobURLStoreInterceptor() = default; |
| |
| namespace { |
| |
| int LoadBasicRequest( |
| network::mojom::URLLoaderFactory* url_loader_factory, |
| const GURL& url, |
| int load_flags, |
| const std::optional<url::Origin>& request_initiator = std::nullopt) { |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = url; |
| request->load_flags = load_flags; |
| request->request_initiator = request_initiator; |
| // Allow access to SameSite cookies in tests. |
| request->site_for_cookies = net::SiteForCookies::FromUrl(url); |
| |
| SimpleURLLoaderTestHelper simple_loader_helper; |
| std::unique_ptr<network::SimpleURLLoader> simple_loader = |
| network::SimpleURLLoader::Create(std::move(request), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| |
| simple_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| url_loader_factory, simple_loader_helper.GetCallbackDeprecated()); |
| simple_loader_helper.WaitForCallback(); |
| |
| return simple_loader->NetError(); |
| } |
| |
| } // namespace |
| |
| int LoadBasicRequest(network::mojom::NetworkContext* network_context, |
| const GURL& url, |
| int load_flags) { |
| mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory; |
| network::mojom::URLLoaderFactoryParamsPtr url_loader_factory_params = |
| network::mojom::URLLoaderFactoryParams::New(); |
| url_loader_factory_params->process_id = network::mojom::kBrowserProcessId; |
| url_loader_factory_params->is_orb_enabled = false; |
| url::Origin origin = url::Origin::Create(url); |
| url_loader_factory_params->isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(origin); |
| network_context->CreateURLLoaderFactory( |
| url_loader_factory.BindNewPipeAndPassReceiver(), |
| std::move(url_loader_factory_params)); |
| // |url_loader_factory| will receive disconnect notification asynchronously if |
| // |network_context| is already disconnected. However it's still false |
| // at this point. |
| EXPECT_TRUE(url_loader_factory.is_connected()); |
| |
| return LoadBasicRequest(url_loader_factory.get(), url, load_flags); |
| } |
| |
| int LoadBasicRequest(RenderFrameHost* frame, const GURL& url) { |
| mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory; |
| frame->CreateNetworkServiceDefaultFactory( |
| url_loader_factory.BindNewPipeAndPassReceiver()); |
| return LoadBasicRequest( |
| url_loader_factory.get(), url, 0 /* load_flags */, |
| frame->GetLastCommittedOrigin() /* request_initiator */); |
| } |
| |
| void EnsureCookiesFlushed(BrowserContext* browser_context) { |
| browser_context->ForEachLoadedStoragePartition( |
| [](StoragePartition* partition) { |
| base::RunLoop run_loop; |
| partition->GetCookieManagerForBrowserProcess()->FlushCookieStore( |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| }); |
| } |
| |
| bool TestGuestAutoresize(WebContents* embedder_web_contents, |
| RenderFrameHost* guest_main_frame) { |
| RenderFrameProxyHost* subframe_proxy_host = |
| FrameTreeNode::From(guest_main_frame) |
| ->render_manager() |
| ->GetProxyToOuterDelegate(); |
| RenderWidgetHostImpl* guest_rwh_impl = static_cast<RenderWidgetHostImpl*>( |
| guest_main_frame->GetRenderWidgetHost()); |
| |
| auto interceptor = std::make_unique<SynchronizeVisualPropertiesInterceptor>( |
| subframe_proxy_host); |
| |
| viz::LocalSurfaceId current_id = |
| guest_rwh_impl->GetView()->GetLocalSurfaceId(); |
| // The guest may not yet be fully attached / initted. If not, |current_id| |
| // will be invalid, and we should wait for an ID before proceeding. |
| if (!current_id.is_valid()) { |
| current_id = interceptor->WaitForSurfaceId(); |
| } |
| |
| // Enable auto-resize. |
| gfx::Size min_size(10, 10); |
| gfx::Size max_size(100, 100); |
| guest_rwh_impl->SetAutoResize(true, min_size, max_size); |
| guest_rwh_impl->GetView()->EnableAutoResize(min_size, max_size); |
| |
| // Enabling auto resize generates a surface ID, wait for it. |
| current_id = interceptor->WaitForSurfaceId(); |
| |
| // Fake an auto-resize update. |
| viz::LocalSurfaceId local_surface_id(current_id.parent_sequence_number(), |
| current_id.child_sequence_number() + 1, |
| current_id.embed_token()); |
| cc::RenderFrameMetadata metadata; |
| metadata.viewport_size_in_pixels = gfx::Size(75, 75); |
| metadata.local_surface_id = local_surface_id; |
| guest_rwh_impl->OnLocalSurfaceIdChanged(metadata); |
| |
| // This won't generate a response, as we short-circuit auto-resizes, so cause |
| // an additional update by disabling auto-resize. |
| guest_rwh_impl->GetView()->DisableAutoResize(gfx::Size(75, 75)); |
| |
| // Get the first delivered surface id and ensure it has the surface id which |
| // we expect. |
| return interceptor->WaitForSurfaceId() == |
| viz::LocalSurfaceId(current_id.parent_sequence_number() + 1, |
| current_id.child_sequence_number() + 1, |
| current_id.embed_token()); |
| } |
| |
| RenderWidgetHostMouseEventMonitor::RenderWidgetHostMouseEventMonitor( |
| RenderWidgetHost* host) |
| : host_(host), event_received_(false) { |
| mouse_callback_ = base::BindRepeating( |
| &RenderWidgetHostMouseEventMonitor::MouseEventCallback, |
| base::Unretained(this)); |
| host_->AddMouseEventCallback(mouse_callback_); |
| } |
| |
| RenderWidgetHostMouseEventMonitor::~RenderWidgetHostMouseEventMonitor() { |
| host_->RemoveMouseEventCallback(mouse_callback_); |
| } |
| |
| DidStartNavigationObserver::DidStartNavigationObserver(WebContents* contents) |
| : WebContentsObserver(contents) {} |
| DidStartNavigationObserver::~DidStartNavigationObserver() = default; |
| |
| void DidStartNavigationObserver::DidStartNavigation(NavigationHandle* handle) { |
| if (observed_) { |
| return; |
| } |
| observed_ = true; |
| navigation_handle_ = handle; |
| run_loop_.Quit(); |
| } |
| |
| void DidStartNavigationObserver::DidFinishNavigation(NavigationHandle* handle) { |
| if (navigation_handle_ == handle) { |
| navigation_handle_ = nullptr; |
| } |
| } |
| |
| ProxyDSFObserver::ProxyDSFObserver() { |
| // Set callback and observer to track the creation of RenderFrameProxyHosts. |
| ProxyHostObserver* observer = GetProxyHostObserver(); |
| observer->set_created_callback(base::BindRepeating( |
| &ProxyDSFObserver::OnCreation, base::Unretained(this))); |
| RenderFrameProxyHost::SetObserverForTesting(observer); |
| } |
| |
| ProxyDSFObserver::~ProxyDSFObserver() { |
| // Stop observing RenderFrameProxyHosts. |
| GetProxyHostObserver()->Reset(); |
| RenderFrameProxyHost::SetObserverForTesting(nullptr); |
| } |
| |
| void ProxyDSFObserver::WaitForOneProxyHostCreation() { |
| if (!proxy_host_created_dsf_.empty()) { |
| return; |
| } |
| runner_ = std::make_unique<base::RunLoop>(); |
| runner_->Run(); |
| } |
| |
| void ProxyDSFObserver::OnCreation(RenderFrameProxyHost* rfph) { |
| // Not all RenderFrameProxyHosts will be created with a |
| // CrossProcessFrameConnector. We're only interested in the ones that do. |
| if (auto* cpfc = rfph->cross_process_frame_connector()) { |
| proxy_host_created_dsf_.push_back( |
| cpfc->screen_infos().current().device_scale_factor); |
| } |
| if (runner_) { |
| runner_->Quit(); |
| } |
| } |
| |
| bool CompareWebContentsOutputToReference( |
| WebContents* web_contents, |
| const base::FilePath& expected_path, |
| const gfx::Size& snapshot_size, |
| const cc::PixelComparator& comparator) { |
| // Produce a frame of output first to ensure the system is in a consistent, |
| // known state. |
| { |
| base::RunLoop run_loop; |
| web_contents->GetPrimaryMainFrame()->InsertVisualStateCallback( |
| base::BindLambdaForTesting([&](bool visual_state_updated) { |
| ASSERT_TRUE(visual_state_updated); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| auto* rwh = RenderWidgetHostImpl::From( |
| web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget()); |
| |
| if (!rwh->GetView() || !rwh->GetView()->IsSurfaceAvailableForCopy()) { |
| ADD_FAILURE() << "RWHV surface not available for copy."; |
| return false; |
| } |
| |
| bool snapshot_matches = false; |
| { |
| base::RunLoop run_loop; |
| rwh->GetView()->CopyFromSurface( |
| gfx::Rect(), gfx::Size(), |
| base::BindLambdaForTesting([&](const SkBitmap& bitmap) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| ASSERT_FALSE(bitmap.drawsNothing()); |
| |
| SkBitmap clipped_bitmap; |
| bitmap.extractSubset( |
| &clipped_bitmap, |
| SkIRect::MakeWH(snapshot_size.width(), snapshot_size.height())); |
| |
| snapshot_matches = |
| cc::MatchesPNGFile(clipped_bitmap, expected_path, comparator); |
| |
| // When rebaselining the pixel test, the test may fail. However, the |
| // reference file will still be overwritten. |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kRebaselinePixelTests)) { |
| ASSERT_TRUE(cc::WritePNGFile(clipped_bitmap, expected_path, |
| /*discard_transparency=*/false)); |
| } |
| |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| return snapshot_matches; |
| } |
| |
| RenderFrameHostChangedCallbackRunner::RenderFrameHostChangedCallbackRunner( |
| WebContents* content, |
| RenderFrameHostChangedCallback callback) |
| : WebContentsObserver(content), callback_(std::move(callback)) {} |
| |
| RenderFrameHostChangedCallbackRunner::~RenderFrameHostChangedCallbackRunner() = |
| default; |
| |
| void RenderFrameHostChangedCallbackRunner::RenderFrameHostChanged( |
| RenderFrameHost* old_host, |
| RenderFrameHost* new_host) { |
| if (callback_) { |
| std::move(callback_).Run(old_host, new_host); |
| } |
| } |
| |
| DidFinishNavigationObserver::DidFinishNavigationObserver( |
| WebContents* web_contents, |
| base::RepeatingCallback<void(NavigationHandle*)> callback) |
| : WebContentsObserver(web_contents), callback_(callback) {} |
| |
| DidFinishNavigationObserver::DidFinishNavigationObserver( |
| RenderFrameHost* render_frame_host, |
| base::RepeatingCallback<void(NavigationHandle*)> callback) |
| : DidFinishNavigationObserver( |
| WebContents::FromRenderFrameHost(render_frame_host), |
| callback) {} |
| |
| DidFinishNavigationObserver::~DidFinishNavigationObserver() = default; |
| |
| void DidFinishNavigationObserver::DidFinishNavigation( |
| NavigationHandle* navigation_handle) { |
| callback_.Run(navigation_handle); |
| } |
| |
| // Since the loading state might be switched more than once due to navigation |
| // restart, the following helper methods for history traversal need to call |
| // `WaitForNavigationFinished()` to ensure the navigation is finally committed |
| // and `WaitForLoadStop()` to ensure the `WebContents` finishes the last |
| // loading. |
| bool HistoryGoToIndex(WebContents* wc, int index) { |
| TestNavigationObserver observer(wc); |
| wc->GetController().GoToIndex(index); |
| WaitForNavigationFinished(wc, observer); |
| return WaitForLoadStop(wc); |
| } |
| |
| bool HistoryGoToOffset(WebContents* wc, int offset) { |
| TestNavigationObserver observer(wc); |
| wc->GetController().GoToOffset(offset); |
| WaitForNavigationFinished(wc, observer); |
| return WaitForLoadStop(wc); |
| } |
| |
| bool HistoryGoBack(WebContents* wc) { |
| TestNavigationObserver observer(wc); |
| wc->GetController().GoBack(); |
| WaitForNavigationFinished(wc, observer); |
| return WaitForLoadStop(wc); |
| } |
| |
| bool HistoryGoForward(WebContents* wc) { |
| TestNavigationObserver observer(wc); |
| wc->GetController().GoForward(); |
| WaitForNavigationFinished(wc, observer); |
| return WaitForLoadStop(wc); |
| } |
| |
| CreateAndLoadWebContentsObserver::CreateAndLoadWebContentsObserver( |
| int num_expected_contents) |
| : creation_subscription_( |
| RegisterWebContentsCreationCallback(base::BindRepeating( |
| &CreateAndLoadWebContentsObserver::OnWebContentsCreated, |
| base::Unretained(this)))), |
| num_expected_contents_(num_expected_contents) { |
| EXPECT_GE(num_expected_contents, 1); |
| } |
| |
| CreateAndLoadWebContentsObserver::~CreateAndLoadWebContentsObserver() = default; |
| |
| void CreateAndLoadWebContentsObserver::OnWebContentsCreated( |
| WebContents* web_contents) { |
| ++num_new_contents_seen_; |
| if (num_new_contents_seen_ < num_expected_contents_) { |
| return; |
| } |
| |
| // If there is already a WebContents, then this will fail the test later. |
| if (num_new_contents_seen_ > num_expected_contents_) { |
| ADD_FAILURE() << "Unexpected WebContents creation"; |
| // If we're called before Wait(), then `contents_creation_quit_closure_` |
| // has not been set. If we're called after, then we'll clear this when |
| // we see the creation of the expected contents and it won't be set again. |
| EXPECT_FALSE(contents_creation_quit_closure_); |
| return; |
| } |
| |
| web_contents_ = web_contents; |
| load_stop_observer_.emplace(web_contents_); |
| |
| if (contents_creation_quit_closure_) { |
| std::move(contents_creation_quit_closure_).Run(); |
| } |
| } |
| |
| WebContents* CreateAndLoadWebContentsObserver::Wait() { |
| // Wait for a new WebContents if we haven't gotten one yet. |
| if (!load_stop_observer_) { |
| base::RunLoop run_loop; |
| contents_creation_quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| load_stop_observer_->Wait(); |
| |
| // Do this after waiting for load to complete, since only the specified number |
| // of WebContents should be created before Wait() returns. If an additional |
| // one is created while the expected contents is loading, then we still fail |
| // the test. |
| EXPECT_EQ(num_expected_contents_, num_new_contents_seen_); |
| creation_subscription_ = base::CallbackListSubscription(); |
| |
| return web_contents_; |
| } |
| |
| CookieChangeObserver::CookieChangeObserver(WebContents* web_contents, |
| int num_expected_calls) |
| : WebContentsObserver(web_contents), |
| run_loop_(base::RunLoop::Type::kNestableTasksAllowed), |
| num_expected_calls_(num_expected_calls) {} |
| |
| CookieChangeObserver::~CookieChangeObserver() = default; |
| |
| void CookieChangeObserver::Wait() { |
| run_loop_.Run(); |
| } |
| |
| void CookieChangeObserver::OnCookiesAccessed( |
| RenderFrameHost* render_frame_host, |
| const CookieAccessDetails& details) { |
| OnCookieAccessed(details); |
| } |
| |
| void CookieChangeObserver::OnCookiesAccessed( |
| NavigationHandle* navigation, |
| const CookieAccessDetails& details) { |
| OnCookieAccessed(details); |
| } |
| |
| void CookieChangeObserver::OnCookieAccessed( |
| const CookieAccessDetails& details) { |
| if (details.type == CookieAccessDetails::Type::kRead) { |
| num_read_seen_++; |
| } else if (details.type == CookieAccessDetails::Type::kChange) { |
| num_write_seen_++; |
| } |
| |
| if (++num_seen_ == num_expected_calls_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| SpeculativeRenderFrameHostObserver::SpeculativeRenderFrameHostObserver( |
| WebContents* web_contents, |
| const GURL& url) |
| : WebContentsObserver(web_contents), url_(url) {} |
| |
| SpeculativeRenderFrameHostObserver::~SpeculativeRenderFrameHostObserver() = |
| default; |
| |
| void SpeculativeRenderFrameHostObserver::Wait() { |
| run_loop_.Run(); |
| } |
| |
| void SpeculativeRenderFrameHostObserver::RenderFrameCreated( |
| RenderFrameHost* render_frame_host) { |
| RenderFrameHostImpl* host_impl = |
| static_cast<RenderFrameHostImpl*>(render_frame_host); |
| NavigationRequest* request = |
| host_impl->frame_tree_node()->navigation_request(); |
| if (host_impl->lifecycle_state() == |
| RenderFrameHostImpl::LifecycleStateImpl::kSpeculative && |
| IsRequestCompatibleWithSpeculativeRFH(request) && |
| request->GetURL() == url_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| SpareRenderProcessHostStartedObserver::SpareRenderProcessHostStartedObserver() { |
| scoped_observation_.Observe(&SpareRenderProcessHostManager::Get()); |
| } |
| |
| SpareRenderProcessHostStartedObserver:: |
| ~SpareRenderProcessHostStartedObserver() = default; |
| |
| void SpareRenderProcessHostStartedObserver::OnSpareRenderProcessHostReady( |
| RenderProcessHost* host) { |
| spare_render_process_host_ = host; |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| RenderProcessHost* |
| SpareRenderProcessHostStartedObserver::WaitForSpareRenderProcessStarted() { |
| base::RunLoop loop; |
| quit_closure_ = loop.QuitClosure(); |
| if (!spare_render_process_host_) { |
| loop.Run(); |
| } |
| |
| RenderProcessHost* host = std::exchange(spare_render_process_host_, nullptr); |
| scoped_observation_.Reset(); |
| return host; |
| } |
| |
| base::CallbackListSubscription RegisterWebContentsCreationCallback( |
| base::RepeatingCallback<void(WebContents*)> callback) { |
| return WebContentsImpl::FriendWrapper::AddCreatedCallbackForTesting(callback); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| void SetConditionalFocusWindowForTesting(base::TimeDelta window) { |
| MediaStreamManager::GetInstance()->SetConditionalFocusWindowForTesting( |
| window); |
| } |
| |
| void SetCapturedSurfaceControllerFactoryForTesting( |
| base::RepeatingCallback<std::unique_ptr<MockCapturedSurfaceController>( |
| GlobalRenderFrameHostId, |
| WebContentsMediaCaptureId)> factory) { |
| using FactoryType = |
| ::base::RepeatingCallback<std::unique_ptr<CapturedSurfaceController>( |
| GlobalRenderFrameHostId, WebContentsMediaCaptureId, |
| base::RepeatingCallback<void(int)>)>; |
| using MockFactoryType = |
| ::base::RepeatingCallback<std::unique_ptr<MockCapturedSurfaceController>( |
| GlobalRenderFrameHostId, WebContentsMediaCaptureId)>; |
| |
| FactoryType wrapped_factory = base::BindRepeating( |
| [](MockFactoryType mock_factory, GlobalRenderFrameHostId rfh_id, |
| WebContentsMediaCaptureId captured_wc_id, |
| base::RepeatingCallback<void(int)> on_zoom_level_change_callback) { |
| std::unique_ptr<MockCapturedSurfaceController> mock_controller = |
| mock_factory.Run(rfh_id, captured_wc_id); |
| std::unique_ptr<CapturedSurfaceController> wrapped_object = |
| base::WrapUnique<CapturedSurfaceController>( |
| mock_controller.release()); |
| return wrapped_object; |
| }, |
| factory); |
| |
| MediaStreamManager::GetInstance() |
| ->SetCapturedSurfaceControllerFactoryForTesting(wrapped_factory); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| |
| std::optional<int> GetDOMNodeId(RenderFrameHost& rfh, |
| std::string_view query_selector) { |
| ScopedTestDevToolsProtocolClient devtools_client(rfh); |
| |
| // Get the document node. |
| const base::Value::Dict* result = |
| devtools_client.SendCommandSync("DOM.getDocument"); |
| CHECK(result); |
| |
| std::optional<int> document_id = result->FindIntByDottedPath("root.nodeId"); |
| CHECK(document_id.has_value()); |
| |
| // Find a node matching the selector in the document. |
| auto params = base::Value::Dict() |
| .Set("nodeId", document_id.value()) |
| .Set("selector", query_selector); |
| result = |
| devtools_client.SendCommandSync("DOM.querySelector", std::move(params)); |
| CHECK(result); |
| |
| // QuerySelector returns a node_id: 0 when the selector isn't matched. |
| std::optional<int> node_id = result->FindInt("nodeId"); |
| if (!node_id || node_id.value() == 0) { |
| return std::nullopt; |
| } |
| |
| // Extract the backendNodeId from the matched node. backendNodeId corresponds |
| // to the Blink DOMNodeId |
| params = base::Value::Dict().Set("nodeId", node_id.value()); |
| result = |
| devtools_client.SendCommandSync("DOM.describeNode", std::move(params)); |
| CHECK(result); |
| |
| std::optional<int> dom_node_id = |
| result->FindIntByDottedPath("node.backendNodeId"); |
| CHECK(dom_node_id.has_value()); |
| |
| return dom_node_id; |
| } |
| |
| std::optional<int> GetDOMNodeIdFromSubframe( |
| content::RenderFrameHost& rfh, |
| std::string_view subframe_query_selector, |
| std::string_view query_selector) { |
| content::ScopedTestDevToolsProtocolClient devtools_client(rfh); |
| |
| // Get the main document node. |
| const base::Value::Dict* result = |
| devtools_client.SendCommandSync("DOM.getDocument"); |
| CHECK(result); |
| std::optional<int> document_id = result->FindIntByDottedPath("root.nodeId"); |
| CHECK(document_id.has_value()); |
| |
| // Find the <iframe> element node in the main document. |
| auto params = base::Value::Dict() |
| .Set("nodeId", document_id.value()) |
| .Set("selector", subframe_query_selector); |
| result = |
| devtools_client.SendCommandSync("DOM.querySelector", std::move(params)); |
| CHECK(result); |
| std::optional<int> iframe_node_id = result->FindInt("nodeId"); |
| if (!iframe_node_id || iframe_node_id.value() == 0) { |
| return std::nullopt; |
| } |
| |
| // Get contentDocument of iframe. |
| params = base::Value::Dict().Set("nodeId", iframe_node_id.value()); |
| result = |
| devtools_client.SendCommandSync("DOM.describeNode", std::move(params)); |
| CHECK(result); |
| std::optional<int> content_doc_backend_node_id = |
| result->FindIntByDottedPath("node.contentDocument.backendNodeId"); |
| if (!content_doc_backend_node_id) { |
| return std::nullopt; |
| } |
| |
| // Resolve that backendNodeId to get a Runtime objectId for the document. |
| params = base::Value::Dict().Set("backendNodeId", |
| content_doc_backend_node_id.value()); |
| result = |
| devtools_client.SendCommandSync("DOM.resolveNode", std::move(params)); |
| CHECK(result); |
| const std::string* content_doc_object_id = |
| result->FindStringByDottedPath("object.objectId"); |
| if (!content_doc_object_id) { |
| return std::nullopt; |
| } |
| |
| // Request the DOM nodeId for the iframe's document from its objectId. |
| params = base::Value::Dict().Set("objectId", *content_doc_object_id); |
| result = |
| devtools_client.SendCommandSync("DOM.requestNode", std::move(params)); |
| CHECK(result); |
| std::optional<int> content_doc_node_id = result->FindInt("nodeId"); |
| if (!content_doc_node_id || content_doc_node_id.value() == 0) { |
| return std::nullopt; |
| } |
| |
| // Query for the target element within the iframe's document. |
| params = base::Value::Dict() |
| .Set("nodeId", content_doc_node_id.value()) |
| .Set("selector", query_selector); |
| result = |
| devtools_client.SendCommandSync("DOM.querySelector", std::move(params)); |
| CHECK(result); |
| std::optional<int> final_node_id = result->FindInt("nodeId"); |
| if (!final_node_id || final_node_id.value() == 0) { |
| return std::nullopt; |
| } |
| |
| params = base::Value::Dict().Set("nodeId", final_node_id.value()); |
| result = |
| devtools_client.SendCommandSync("DOM.describeNode", std::move(params)); |
| CHECK(result); |
| std::optional<int> dom_node_id = |
| result->FindIntByDottedPath("node.backendNodeId"); |
| CHECK(dom_node_id.has_value()); |
| |
| return dom_node_id; |
| } |
| |
| namespace { |
| |
| class DOMContentLoadedObserver : public WebContentsObserver { |
| public: |
| explicit DOMContentLoadedObserver(RenderFrameHost* render_frame_host) |
| : WebContentsObserver( |
| WebContents::FromRenderFrameHost(render_frame_host)), |
| render_frame_host_(render_frame_host->GetWeakDocumentPtr()) {} |
| |
| void DOMContentLoaded(RenderFrameHost* render_frame_host) override { |
| if (render_frame_host_.AsRenderFrameHostIfValid() == render_frame_host) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| // Block the running thread until the DOMContentLoaded event fires. Returns |
| // true if the function returns as a result of the DOMContentLoaded event |
| // being fired, false otherwise (e.g. timeout). |
| [[nodiscard]] bool Wait() { |
| CHECK(render_frame_host_.AsRenderFrameHostIfValid()); |
| if (render_frame_host_.AsRenderFrameHostIfValid()->IsDOMContentLoaded()) { |
| run_loop_.Quit(); |
| } |
| run_loop_.Run(); |
| CHECK(render_frame_host_.AsRenderFrameHostIfValid()); |
| return render_frame_host_.AsRenderFrameHostIfValid()->IsDOMContentLoaded(); |
| } |
| |
| private: |
| WeakDocumentPtr render_frame_host_; |
| base::RunLoop run_loop_; |
| }; |
| |
| } // namespace |
| |
| // Suspends execution in the current thread until the DOMContentLoaded event |
| // fires in the given RenderFrameHost. Note, this will only observe the Document |
| // associated with the given RenderFrameHost at the time of the call. |
| bool WaitForDOMContentLoaded(RenderFrameHost* rfh) { |
| DOMContentLoadedObserver observer(rfh); |
| return observer.Wait(); |
| } |
| |
| CreateNewPopupWidgetInterceptor::CreateNewPopupWidgetInterceptor( |
| RenderFrameHost* rfh, |
| base::OnceCallback<void(RenderWidgetHost*)> did_create_callback) |
| : swapped_impl_(static_cast<RenderFrameHostImpl*>(rfh) |
| ->local_frame_host_receiver_for_testing(), |
| this), |
| did_create_callback_(std::move(did_create_callback)) {} |
| |
| CreateNewPopupWidgetInterceptor::~CreateNewPopupWidgetInterceptor() = default; |
| |
| void CreateNewPopupWidgetInterceptor::CreateNewPopupWidget( |
| mojo::PendingAssociatedReceiver<blink::mojom::PopupWidgetHost> |
| blink_popup_widget_host, |
| mojo::PendingAssociatedReceiver<blink::mojom::WidgetHost> blink_widget_host, |
| mojo::PendingAssociatedRemote<blink::mojom::Widget> blink_widget) { |
| class PopupWidgetCreationObserver : public RenderWidgetHostFactory { |
| public: |
| PopupWidgetCreationObserver() { RegisterFactory(this); } |
| |
| ~PopupWidgetCreationObserver() override { UnregisterFactory(); } |
| |
| // RenderWidgetHostFactory overrides: |
| RenderWidgetHostImpl* CreateSelfOwnedRenderWidgetHost( |
| FrameTree* frame_tree, |
| RenderWidgetHostDelegate* delegate, |
| base::SafeRef<SiteInstanceGroup> site_instance_group, |
| int32_t routing_id, |
| bool hidden) override { |
| CHECK(!last_created_widget_); |
| last_created_widget_ = |
| RenderWidgetHostFactory::CreateSelfOwnedRenderWidgetHost( |
| frame_tree, delegate, std::move(site_instance_group), routing_id, |
| hidden); |
| return last_created_widget_; |
| } |
| |
| RenderWidgetHostImpl* TakeLastCreatedWidget() { |
| return std::exchange(last_created_widget_, nullptr); |
| } |
| |
| private: |
| raw_ptr<RenderWidgetHostImpl> last_created_widget_; |
| }; |
| |
| PopupWidgetCreationObserver creation_observer; |
| |
| GetForwardingInterface()->CreateNewPopupWidget( |
| std::move(blink_popup_widget_host), std::move(blink_widget_host), |
| std::move(blink_widget)); |
| |
| if (!did_create_callback_) { |
| return; |
| } |
| |
| if (auto* widget = creation_observer.TakeLastCreatedWidget(); widget) { |
| std::move(did_create_callback_).Run(widget); |
| } |
| } |
| |
| blink::mojom::LocalFrameHost* |
| CreateNewPopupWidgetInterceptor::GetForwardingInterface() { |
| return swapped_impl_.old_impl(); |
| } |
| |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID) |
| ShowPopupWidgetWaiter::ShowPopupMenuInterceptor::ShowPopupMenuInterceptor( |
| RenderFrameHost* rfh, |
| base::OnceCallback<void(const gfx::Rect&)> did_show_popup_menu_callback) |
| : swapped_impl_(static_cast<RenderFrameHostImpl*>(rfh) |
| ->local_frame_host_receiver_for_testing(), |
| this), |
| did_show_popup_menu_callback_(std::move(did_show_popup_menu_callback)) {} |
| |
| ShowPopupWidgetWaiter::ShowPopupMenuInterceptor::~ShowPopupMenuInterceptor() = |
| default; |
| |
| void ShowPopupWidgetWaiter::ShowPopupMenuInterceptor::ShowPopupMenu( |
| mojo::PendingRemote<blink::mojom::PopupMenuClient> popup_client, |
| const gfx::Rect& bounds, |
| double font_size, |
| int32_t selected_item, |
| std::vector<blink::mojom::MenuItemPtr> menu_items, |
| bool right_aligned, |
| bool allow_multiple_selection) { |
| if (did_show_popup_menu_callback_) { |
| std::move(did_show_popup_menu_callback_).Run(bounds); |
| mojo::Remote<blink::mojom::PopupMenuClient>(std::move(popup_client)) |
| ->DidCancel(); |
| return; |
| } |
| |
| GetForwardingInterface()->ShowPopupMenu( |
| std::move(popup_client), bounds, font_size, selected_item, |
| std::move(menu_items), right_aligned, allow_multiple_selection); |
| } |
| |
| blink::mojom::LocalFrameHost* |
| ShowPopupWidgetWaiter::ShowPopupMenuInterceptor::GetForwardingInterface() { |
| return swapped_impl_.old_impl(); |
| } |
| #endif |
| |
| ShowPopupWidgetWaiter::ShowPopupWidgetWaiter(WebContents* web_contents, |
| RenderFrameHost* frame_host) |
| : create_new_popup_widget_interceptor_( |
| static_cast<RenderFrameHostImpl*>(frame_host), |
| base::BindOnce(&ShowPopupWidgetWaiter::DidCreatePopupWidget, |
| base::Unretained(this))), |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID) |
| show_popup_menu_interceptor_( |
| frame_host, |
| base::BindOnce(&ShowPopupWidgetWaiter::DidShowPopupMenu, |
| base::Unretained(this))), |
| #endif |
| |
| frame_host_(frame_host) { |
| } |
| |
| ShowPopupWidgetWaiter::~ShowPopupWidgetWaiter() { |
| if (auto* rwhi = RenderWidgetHostImpl::FromID(process_id_, routing_id_)) { |
| std::ignore = |
| rwhi->popup_widget_host_receiver_for_testing().SwapImplForTesting(rwhi); |
| } |
| } |
| |
| void ShowPopupWidgetWaiter::Wait() { |
| run_loop_.Run(); |
| } |
| |
| blink::mojom::PopupWidgetHost* ShowPopupWidgetWaiter::GetForwardingInterface() { |
| DCHECK_NE(IPC::mojom::kRoutingIdNone, routing_id_); |
| return RenderWidgetHostImpl::FromID(process_id_, routing_id_); |
| } |
| |
| void ShowPopupWidgetWaiter::ShowPopup(const gfx::Rect& initial_rect, |
| const gfx::Rect& initial_anchor_rect, |
| ShowPopupCallback callback) { |
| GetForwardingInterface()->ShowPopup(initial_rect, initial_anchor_rect, |
| std::move(callback)); |
| initial_rect_ = initial_rect; |
| run_loop_.Quit(); |
| } |
| |
| void ShowPopupWidgetWaiter::DidCreatePopupWidget( |
| RenderWidgetHost* render_widget_host) { |
| process_id_ = render_widget_host->GetProcess()->GetDeprecatedID(); |
| routing_id_ = render_widget_host->GetRoutingID(); |
| // Swapped back in destructor from process_id_ and routing_id_ lookup. |
| std::ignore = static_cast<RenderWidgetHostImpl*>(render_widget_host) |
| ->popup_widget_host_receiver_for_testing() |
| .SwapImplForTesting(this); |
| } |
| |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID) |
| void ShowPopupWidgetWaiter::DidShowPopupMenu(const gfx::Rect& bounds) { |
| initial_rect_ = bounds; |
| run_loop_.Quit(); |
| } |
| #endif |
| |
| } // namespace content |