Commit c42ecf7b authored by Tricia Crichton's avatar Tricia Crichton Committed by Commit Bot

[ChromeDriver] Wait for Current Frame to load

When Waiting for Pending Navigation, code will respond to loading events
for the current frame as well as the top frame. ChromeDriver will
therefore wait for changes in the current frame to complete before
running the next command.

Bug: chromedriver:3164
Change-Id: Id9000afcb7e7a7689466c395071dcccc09c8b23d
Fixed: chromedriver:3164
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1859637Reviewed-by: default avatarJohn Chen <johnchen@chromium.org>
Commit-Queue: Tricia Crichton <triciac@chromium.org>
Cr-Commit-Position: refs/heads/master@{#716204}
parent 54af0e9b
...@@ -153,10 +153,10 @@ Status ChromeDesktopImpl::WaitForPageToLoad( ...@@ -153,10 +153,10 @@ Status ChromeDesktopImpl::WaitForPageToLoad(
// https://code.google.com/p/chromedriver/issues/detail?id=1205 // https://code.google.com/p/chromedriver/issues/detail?id=1205
device_metrics = nullptr; device_metrics = nullptr;
} }
std::unique_ptr<WebView> web_view_tmp( std::unique_ptr<WebView> web_view_tmp(new WebViewImpl(
new WebViewImpl(id, w3c_compliant, devtools_http_client_->browser_info(), id, w3c_compliant, nullptr, devtools_http_client_->browser_info(),
devtools_http_client_->CreateClient(id), device_metrics, devtools_http_client_->CreateClient(id), device_metrics,
page_load_strategy())); page_load_strategy()));
Status status = web_view_tmp->ConnectIfNecessary(); Status status = web_view_tmp->ConnectIfNecessary();
if (status.IsError()) if (status.IsError())
return status; return status;
......
...@@ -100,9 +100,9 @@ void ChromeImpl::UpdateWebViews(const WebViewsInfo& views_info, ...@@ -100,9 +100,9 @@ void ChromeImpl::UpdateWebViews(const WebViewsInfo& views_info,
// OnConnected will fire when DevToolsClient connects later. // OnConnected will fire when DevToolsClient connects later.
CHECK(!page_load_strategy_.empty()); CHECK(!page_load_strategy_.empty());
web_views_.push_back(std::make_unique<WebViewImpl>( web_views_.push_back(std::make_unique<WebViewImpl>(
view.id, w3c_compliant, devtools_http_client_->browser_info(), view.id, w3c_compliant, nullptr,
std::move(client), devtools_http_client_->device_metrics(), devtools_http_client_->browser_info(), std::move(client),
page_load_strategy_)); devtools_http_client_->device_metrics(), page_load_strategy_));
} }
} }
} }
......
...@@ -278,9 +278,18 @@ Status DevToolsClientImpl::HandleEventsUntil( ...@@ -278,9 +278,18 @@ Status DevToolsClientImpl::HandleEventsUntil(
return Status(kOk); return Status(kOk);
} }
Status status = ProcessNextMessage(-1, timeout); // Create a small timeout so conditional_func can be retried
if (status.IsError()) // when only funcinterval has expired, continue while loop
// but return timeout status if primary timeout has expired
Timeout funcinterval =
Timeout(base::TimeDelta::FromMilliseconds(100), &timeout);
Status status = ProcessNextMessage(-1, funcinterval);
if (status.code() == kTimeout) {
if (timeout.IsExpired())
return status;
} else if (status.IsError()) {
return status; return status;
}
} }
} }
......
...@@ -201,8 +201,8 @@ Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) { ...@@ -201,8 +201,8 @@ Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) {
std::unique_ptr<DevToolsClient> client(new DevToolsClientImpl( std::unique_ptr<DevToolsClient> client(new DevToolsClientImpl(
socket_factory_, endpoint_.GetDebuggerUrl(*it), *it)); socket_factory_, endpoint_.GetDebuggerUrl(*it), *it));
std::unique_ptr<WebViewImpl> web_view( std::unique_ptr<WebViewImpl> web_view(
new WebViewImpl(*it, false, &browser_info_, std::move(client), NULL, new WebViewImpl(*it, false, nullptr, &browser_info_, std::move(client),
page_load_strategy_)); nullptr, page_load_strategy_));
status = web_view->ConnectIfNecessary(); status = web_view->ConnectIfNecessary();
// Ignore disconnected error, because the debugger might have closed when // Ignore disconnected error, because the debugger might have closed when
......
...@@ -27,6 +27,8 @@ Status MakeNavigationCheckFailedStatus(Status command_status) { ...@@ -27,6 +27,8 @@ Status MakeNavigationCheckFailedStatus(Status command_status) {
return Status(kUnexpectedAlertOpen); return Status(kUnexpectedAlertOpen);
else if (command_status.code() == kTimeout) else if (command_status.code() == kTimeout)
return Status(kTimeout); return Status(kTimeout);
else if (command_status.code() == kNoSuchExecutionContext)
return Status(kNoSuchExecutionContext);
else else
return Status(kUnknownError, "cannot determine loading status", return Status(kUnknownError, "cannot determine loading status",
command_status); command_status);
...@@ -36,12 +38,15 @@ Status MakeNavigationCheckFailedStatus(Status command_status) { ...@@ -36,12 +38,15 @@ Status MakeNavigationCheckFailedStatus(Status command_status) {
NavigationTracker::NavigationTracker( NavigationTracker::NavigationTracker(
DevToolsClient* client, DevToolsClient* client,
WebView* web_view,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager, const JavaScriptDialogManager* dialog_manager,
const bool is_eager) const bool is_eager)
: client_(client), : client_(client),
loading_state_(kUnknown), loading_state_(kUnknown),
web_view_(web_view),
top_frame_id_(client->GetId()), top_frame_id_(client->GetId()),
current_frame_id_(top_frame_id_),
dialog_manager_(dialog_manager), dialog_manager_(dialog_manager),
is_eager_(is_eager), is_eager_(is_eager),
timed_out_(false) { timed_out_(false) {
...@@ -51,12 +56,15 @@ NavigationTracker::NavigationTracker( ...@@ -51,12 +56,15 @@ NavigationTracker::NavigationTracker(
NavigationTracker::NavigationTracker( NavigationTracker::NavigationTracker(
DevToolsClient* client, DevToolsClient* client,
LoadingState known_state, LoadingState known_state,
WebView* web_view,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager, const JavaScriptDialogManager* dialog_manager,
const bool is_eager) const bool is_eager)
: client_(client), : client_(client),
loading_state_(known_state), loading_state_(known_state),
web_view_(web_view),
top_frame_id_(client->GetId()), top_frame_id_(client->GetId()),
current_frame_id_(top_frame_id_),
dialog_manager_(dialog_manager), dialog_manager_(dialog_manager),
is_eager_(is_eager), is_eager_(is_eager),
timed_out_(false) { timed_out_(false) {
...@@ -65,6 +73,14 @@ NavigationTracker::NavigationTracker( ...@@ -65,6 +73,14 @@ NavigationTracker::NavigationTracker(
NavigationTracker::~NavigationTracker() {} NavigationTracker::~NavigationTracker() {}
void NavigationTracker::ClearState(const std::string& new_frame_id) {
loading_state_ = kUnknown;
if (new_frame_id.empty())
current_frame_id_ = top_frame_id_;
else
current_frame_id_ = new_frame_id;
}
Status NavigationTracker::IsPendingNavigation(const std::string& frame_id, Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) { bool* is_pending) {
...@@ -76,7 +92,6 @@ Status NavigationTracker::IsPendingNavigation(const std::string& frame_id, ...@@ -76,7 +92,6 @@ Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
*is_pending = false; *is_pending = false;
return Status(kOk); return Status(kOk);
} }
// Some DevTools commands (e.g. Input.dispatchMouseEvent) are handled in the // Some DevTools commands (e.g. Input.dispatchMouseEvent) are handled in the
// browser process, and may cause the renderer process to start a new // browser process, and may cause the renderer process to start a new
// navigation. We need to call Runtime.evaluate to force a roundtrip to the // navigation. We need to call Runtime.evaluate to force a roundtrip to the
...@@ -123,7 +138,6 @@ Status NavigationTracker::IsPendingNavigation(const std::string& frame_id, ...@@ -123,7 +138,6 @@ Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
loading_state_ = kLoading; loading_state_ = kLoading;
return Status(kOk); return Status(kOk);
} }
// If we're loading the ChromeDriver automation extension background page, // If we're loading the ChromeDriver automation extension background page,
// look for a known function to determine the loading status. // look for a known function to determine the loading status.
if (base_url == kAutomationExtensionBackgroundPage) { if (base_url == kAutomationExtensionBackgroundPage) {
...@@ -134,7 +148,9 @@ Status NavigationTracker::IsPendingNavigation(const std::string& frame_id, ...@@ -134,7 +148,9 @@ Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
loading_state_ = function_exists ? kNotLoading : kLoading; loading_state_ = function_exists ? kNotLoading : kLoading;
} }
status = DetermineUnknownLoadingState(); status = DetermineUnknownLoadingState();
if (status.IsError()) if (status.code() == kNoSuchExecutionContext)
loading_state_ = kLoading;
else if (status.IsError())
return MakeNavigationCheckFailedStatus(status); return MakeNavigationCheckFailedStatus(status);
} }
*is_pending = loading_state_ == kLoading; *is_pending = loading_state_ == kLoading;
...@@ -175,25 +191,29 @@ Status NavigationTracker::OnEvent(DevToolsClient* client, ...@@ -175,25 +191,29 @@ Status NavigationTracker::OnEvent(DevToolsClient* client,
const base::DictionaryValue& params) { const base::DictionaryValue& params) {
if (method == "Page.loadEventFired" || if (method == "Page.loadEventFired" ||
(is_eager_ && method == "Page.domContentEventFired")) { (is_eager_ && method == "Page.domContentEventFired")) {
loading_state_ = kNotLoading; if (top_frame_id_ == current_frame_id_) {
loading_state_ = kNotLoading;
} else {
return DetermineUnknownLoadingState();
}
} else if (method == "Page.frameStartedLoading") { } else if (method == "Page.frameStartedLoading") {
// If frame that started loading is the top frame // If frame that started loading is the current frame
// set loading_state_ to loading. If it is a subframe // set loading_state_ to loading. If it is another subframe
// the loading state should not change // the loading state should not change
std::string frame_id; std::string frame_id;
if (!params.GetString("frameId", &frame_id)) if (!params.GetString("frameId", &frame_id))
return Status(kUnknownError, "missing or invalid 'frameId'"); return Status(kUnknownError, "missing or invalid 'frameId'");
if (frame_id == top_frame_id_) { if (frame_id == current_frame_id_) {
loading_state_ = kLoading; loading_state_ = kLoading;
} }
} else if (method == "Page.frameStoppedLoading") { } else if (method == "Page.frameStoppedLoading") {
// Sometimes Page.frameStoppedLoading fires without // Sometimes Page.frameStoppedLoading fires without
// an associated Page.loadEventFired. If this happens // an associated Page.loadEventFired. If this happens
// for the top frame, assume loading has finished. // for the current frame, assume loading has finished.
std::string frame_id; std::string frame_id;
if (!params.GetString("frameId", &frame_id)) if (!params.GetString("frameId", &frame_id))
return Status(kUnknownError, "missing or invalid 'frameId'"); return Status(kUnknownError, "missing or invalid 'frameId'");
if (frame_id == top_frame_id_) { if (frame_id == current_frame_id_) {
loading_state_ = kNotLoading; loading_state_ = kNotLoading;
} }
} else if (method == "Inspector.targetCrashed") { } else if (method == "Inspector.targetCrashed") {
...@@ -205,15 +225,18 @@ Status NavigationTracker::OnEvent(DevToolsClient* client, ...@@ -205,15 +225,18 @@ Status NavigationTracker::OnEvent(DevToolsClient* client,
} }
Status NavigationTracker::DetermineUnknownLoadingState() { Status NavigationTracker::DetermineUnknownLoadingState() {
base::DictionaryValue params; std::unique_ptr<base::Value> result;
params.SetString("expression", "document.readyState"); Status status = web_view_->EvaluateScript(current_frame_id_,
std::unique_ptr<base::DictionaryValue> result; "document.readyState", &result);
Status status = if (status.code() == kNoSuchExecutionContext) {
client_->SendCommandAndGetResult("Runtime.evaluate", params, &result); loading_state_ = kLoading;
std::string ready_state; // result is not set in this case, so return here
if (status.IsError() || !result->GetString("result.value", &ready_state)) { return Status(kOk);
} else if (status.IsError()) {
return MakeNavigationCheckFailedStatus(status); return MakeNavigationCheckFailedStatus(status);
} }
std::string ready_state = result->GetString();
if (ready_state == "complete") { if (ready_state == "complete") {
loading_state_ = kNotLoading; loading_state_ = kNotLoading;
} else { } else {
......
...@@ -30,12 +30,14 @@ class NavigationTracker : public DevToolsEventListener, ...@@ -30,12 +30,14 @@ class NavigationTracker : public DevToolsEventListener,
public PageLoadStrategy { public PageLoadStrategy {
public: public:
NavigationTracker(DevToolsClient* client, NavigationTracker(DevToolsClient* client,
WebView* web_view,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager, const JavaScriptDialogManager* dialog_manager,
const bool is_eager = false); const bool is_eager = false);
NavigationTracker(DevToolsClient* client, NavigationTracker(DevToolsClient* client,
LoadingState known_state, LoadingState known_state,
WebView* web_view,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager, const JavaScriptDialogManager* dialog_manager,
const bool is_eager = false); const bool is_eager = false);
...@@ -49,6 +51,7 @@ class NavigationTracker : public DevToolsEventListener, ...@@ -49,6 +51,7 @@ class NavigationTracker : public DevToolsEventListener,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) override; bool* is_pending) override;
void set_timed_out(bool timed_out) override; void set_timed_out(bool timed_out) override;
void ClearState(const std::string& new_frame_id) override;
bool IsNonBlocking() const override; bool IsNonBlocking() const override;
Status CheckFunctionExists(const Timeout* timeout, bool* exists); Status CheckFunctionExists(const Timeout* timeout, bool* exists);
...@@ -67,7 +70,9 @@ class NavigationTracker : public DevToolsEventListener, ...@@ -67,7 +70,9 @@ class NavigationTracker : public DevToolsEventListener,
Status DetermineUnknownLoadingState(); Status DetermineUnknownLoadingState();
DevToolsClient* client_; DevToolsClient* client_;
LoadingState loading_state_; LoadingState loading_state_;
WebView* web_view_;
std::string top_frame_id_; std::string top_frame_id_;
std::string current_frame_id_;
const JavaScriptDialogManager* dialog_manager_; const JavaScriptDialogManager* dialog_manager_;
const bool is_eager_; const bool is_eager_;
bool timed_out_; bool timed_out_;
......
...@@ -6,12 +6,15 @@ ...@@ -6,12 +6,15 @@
#include "base/compiler_specific.h" #include "base/compiler_specific.h"
#include "base/json/json_reader.h" #include "base/json/json_reader.h"
#include "base/memory/ptr_util.h"
#include "base/values.h" #include "base/values.h"
#include "chrome/test/chromedriver/chrome/browser_info.h" #include "chrome/test/chromedriver/chrome/browser_info.h"
#include "chrome/test/chromedriver/chrome/javascript_dialog_manager.h" #include "chrome/test/chromedriver/chrome/javascript_dialog_manager.h"
#include "chrome/test/chromedriver/chrome/navigation_tracker.h" #include "chrome/test/chromedriver/chrome/navigation_tracker.h"
#include "chrome/test/chromedriver/chrome/status.h" #include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/stub_devtools_client.h" #include "chrome/test/chromedriver/chrome/stub_devtools_client.h"
#include "chrome/test/chromedriver/chrome/stub_web_view.h"
#include "chrome/test/chromedriver/chrome/web_view_impl.h"
#include "chrome/test/chromedriver/net/timeout.h" #include "chrome/test/chromedriver/net/timeout.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
...@@ -90,24 +93,57 @@ class DeterminingLoadStateDevToolsClient : public StubDevToolsClient { ...@@ -90,24 +93,57 @@ class DeterminingLoadStateDevToolsClient : public StubDevToolsClient {
base::DictionaryValue* send_event_first_params_; base::DictionaryValue* send_event_first_params_;
}; };
class EvaluateScriptWebView : public StubWebView {
public:
explicit EvaluateScriptWebView(StatusCode code)
: StubWebView("1"), result_(std::string()), code_(code) {}
Status EvaluateScript(const std::string& frame,
const std::string& function,
std::unique_ptr<base::Value>* result) override {
base::Value value(result_);
result->reset(value.DeepCopy());
return Status(code_);
}
void nextEvaluateScript(std::string result, StatusCode code) {
result_ = result;
code_ = code;
}
private:
std::string result_;
StatusCode code_;
};
} // namespace } // namespace
TEST(NavigationTracker, FrameLoadStartStop) { TEST(NavigationTracker, FrameLoadStartStop) {
base::DictionaryValue dict; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(false, true, std::string(), &dict);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<DeterminingLoadStateDevToolsClient>(
false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
base::DictionaryValue params; base::DictionaryValue params;
params.SetString("frameId", client.GetId()); params.SetString("frameId", client_ptr->GetId());
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStartedLoading", params).code()); kOk,
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), true)); tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), true));
ASSERT_EQ(kOk, ASSERT_EQ(kOk,
tracker.OnEvent(&client, "Page.loadEventFired", params).code()); tracker.OnEvent(client_ptr, "Page.loadEventFired", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), false)); ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), false));
} }
// When a frame fails to load due to (for example) a DNS resolution error, we // When a frame fails to load due to (for example) a DNS resolution error, we
...@@ -115,82 +151,205 @@ TEST(NavigationTracker, FrameLoadStartStop) { ...@@ -115,82 +151,205 @@ TEST(NavigationTracker, FrameLoadStartStop) {
// Page.loadEventFired event. // Page.loadEventFired event.
TEST(NavigationTracker, FrameLoadStartStartStop) { TEST(NavigationTracker, FrameLoadStartStartStop) {
base::DictionaryValue dict; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(false, true, std::string(), &dict);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<DeterminingLoadStateDevToolsClient>(
false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
base::DictionaryValue params; base::DictionaryValue params;
params.SetString("frameId", client.GetId()); params.SetString("frameId", client_ptr->GetId());
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStartedLoading", params).code()); kOk,
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), true)); tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), true));
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStartedLoading", params).code()); kOk,
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), true)); tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), true));
ASSERT_EQ(kOk, ASSERT_EQ(kOk,
tracker.OnEvent(&client, "Page.loadEventFired", params).code()); tracker.OnEvent(client_ptr, "Page.loadEventFired", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), false)); ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), false));
} }
TEST(NavigationTracker, MultipleFramesLoad) { TEST(NavigationTracker, MultipleFramesLoad) {
base::DictionaryValue dict; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(false, true, std::string(), &dict);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<DeterminingLoadStateDevToolsClient>(
base::DictionaryValue params; false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
std::string top_frame_id = client.GetId(); base::DictionaryValue params;
std::string top_frame_id = client_ptr->GetId();
params.SetString("frameId", top_frame_id); params.SetString("frameId", top_frame_id);
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStartedLoading", params).code()); kOk,
tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), true)); ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), true));
params.SetString("frameId", "2"); params.SetString("frameId", "2");
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStartedLoading", params).code()); kOk,
tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", true)); ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", true));
params.SetString("frameId", "2"); params.SetString("frameId", "2");
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStoppedLoading", params).code()); kOk,
tracker.OnEvent(client_ptr, "Page.frameStoppedLoading", params).code());
// Inner frame stops loading. loading_state_ should remain true // Inner frame stops loading. loading_state_ should remain true
// since top frame is still loading // since top frame is still loading
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", true)); ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", true));
params.SetString("frameId", top_frame_id); params.SetString("frameId", top_frame_id);
ASSERT_EQ(kOk, ASSERT_EQ(kOk,
tracker.OnEvent(&client, "Page.loadEventFired", params).code()); tracker.OnEvent(client_ptr, "Page.loadEventFired", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, top_frame_id, false)); ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, top_frame_id, false));
params.SetString("frameId", "3"); params.SetString("frameId", "3");
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStoppedLoading", params).code()); kOk,
tracker.OnEvent(client_ptr, "Page.frameStoppedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "3", false)); ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "3", false));
ASSERT_EQ( ASSERT_EQ(
kOk, tracker.OnEvent(&client, "Page.frameStartedLoading", params).code()); kOk,
tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "3", false)); ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "3", false));
} }
TEST(NavigationTracker, NavigationScheduledForOtherFrame) { TEST(NavigationTracker, NavigationScheduledForOtherFrame) {
base::DictionaryValue dict; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(false, true, std::string(), &dict);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker( std::make_unique<DeterminingLoadStateDevToolsClient>(
&client, NavigationTracker::kNotLoading, &browser_info, &dialog_manager); false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, NavigationTracker::kNotLoading,
&web_view, &browser_info, &dialog_manager);
base::DictionaryValue params_scheduled; base::DictionaryValue params_scheduled;
params_scheduled.SetInteger("delay", 0); params_scheduled.SetInteger("delay", 0);
params_scheduled.SetString("frameId", "other"); params_scheduled.SetString("frameId", "other");
ASSERT_EQ(kOk, tracker
.OnEvent(client_ptr, "Page.frameScheduledNavigation",
params_scheduled)
.code());
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), false));
}
TEST(NavigationTracker, CurrentFrameLoading) {
base::DictionaryValue dict;
BrowserInfo browser_info;
std::unique_ptr<DevToolsClient> client_uptr =
std::make_unique<DeterminingLoadStateDevToolsClient>(
false, false, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
EvaluateScriptWebView web_view(kOk);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
base::DictionaryValue params;
std::string top_frame_id = client_ptr->GetId();
std::string current_frame_id = "2";
params.SetString("frameId", current_frame_id);
// verify initial state
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, current_frame_id, true));
// loading state should respond to events from new frame after ClearState
tracker.ClearState(current_frame_id);
web_view.nextEvaluateScript("uninitialized", kOk);
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, current_frame_id, true));
ASSERT_EQ(
kOk,
tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
web_view.nextEvaluateScript("loading", kOk);
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, current_frame_id, true));
ASSERT_EQ(
kOk,
tracker.OnEvent(client_ptr, "Page.frameStoppedLoading", params).code());
web_view.nextEvaluateScript("complete", kOk);
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, current_frame_id, false));
// loading state should not respond to unknown frame events
params.SetString("frameId", "4");
ASSERT_EQ(
kOk,
tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, current_frame_id, false));
ASSERT_EQ( ASSERT_EQ(
kOk, kOk,
tracker.OnEvent( tracker.OnEvent(client_ptr, "Page.frameStoppedLoading", params).code());
&client, "Page.frameScheduledNavigation", params_scheduled).code()); ASSERT_NO_FATAL_FAILURE(
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), false)); AssertPendingState(&tracker, current_frame_id, false));
}
TEST(NavigationTracker, ClearStateNoFrame) {
base::DictionaryValue dict;
BrowserInfo browser_info;
std::unique_ptr<DevToolsClient> client_uptr =
std::make_unique<DeterminingLoadStateDevToolsClient>(
false, false, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
EvaluateScriptWebView web_view(kOk);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
base::DictionaryValue params;
std::string top_frame_id = client_ptr->GetId();
web_view.nextEvaluateScript("uninitialized", kOk);
ASSERT_NO_FATAL_FAILURE(tracker.ClearState(std::string()));
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, top_frame_id, true));
ASSERT_NO_FATAL_FAILURE(tracker.ClearState("2"));
ASSERT_NO_FATAL_FAILURE(tracker.ClearState(std::string()));
params.SetString("frameId", top_frame_id);
web_view.nextEvaluateScript("complete", kOk);
ASSERT_EQ(
kOk,
tracker.OnEvent(client_ptr, "Page.frameStoppedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", false));
// loading state should not respond to unknown frame events
params.SetString("frameId", "2");
ASSERT_EQ(
kOk,
tracker.OnEvent(client_ptr, "Page.frameStartedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", false));
ASSERT_EQ(
kOk,
tracker.OnEvent(client_ptr, "Page.frameStoppedLoading", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "2", false));
} }
namespace { namespace {
...@@ -223,64 +382,102 @@ class FailToEvalScriptDevToolsClient : public StubDevToolsClient { ...@@ -223,64 +382,102 @@ class FailToEvalScriptDevToolsClient : public StubDevToolsClient {
} // namespace } // namespace
TEST(NavigationTracker, UnknownStateFailsToDetermineState) { TEST(NavigationTracker, UnknownStateFailsToDetermineState) {
FailToEvalScriptDevToolsClient client;
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<FailToEvalScriptDevToolsClient>();
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
bool is_pending; bool is_pending;
ASSERT_EQ(kUnknownError, ASSERT_EQ(kUnknownError,
tracker.IsPendingNavigation("f", nullptr, &is_pending).code()); tracker.IsPendingNavigation("f", nullptr, &is_pending).code());
} }
TEST(NavigationTracker, UnknownStatePageNotLoadAtAll) { TEST(NavigationTracker, UnknownStatePageNotLoadAtAll) {
base::DictionaryValue params; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(
true, true, std::string(), &params);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<DeterminingLoadStateDevToolsClient>(
true, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "f", true)); ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, "f", true));
} }
TEST(NavigationTracker, UnknownStateForcesStart) { TEST(NavigationTracker, UnknownStateForcesStart) {
base::DictionaryValue params; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(
false, true, std::string(), &params);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<DeterminingLoadStateDevToolsClient>(
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), true)); false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
EvaluateScriptWebView web_view(kOk);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), true));
} }
TEST(NavigationTracker, UnknownStateForcesStartReceivesStop) { TEST(NavigationTracker, UnknownStateForcesStartReceivesStop) {
base::DictionaryValue dict; base::DictionaryValue dict;
DeterminingLoadStateDevToolsClient client(false, true, std::string(), &dict);
BrowserInfo browser_info; BrowserInfo browser_info;
JavaScriptDialogManager dialog_manager(&client, &browser_info); std::unique_ptr<DevToolsClient> client_uptr =
NavigationTracker tracker(&client, &browser_info, &dialog_manager); std::make_unique<DeterminingLoadStateDevToolsClient>(
false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
WebViewImpl web_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
NavigationTracker tracker(client_ptr, &web_view, &browser_info,
&dialog_manager);
base::DictionaryValue params; base::DictionaryValue params;
params.SetString("frameId", client.GetId()); params.SetString("frameId", client_ptr->GetId());
ASSERT_EQ(kOk, ASSERT_EQ(kOk,
tracker.OnEvent(&client, "Page.loadEventFired", params).code()); tracker.OnEvent(client_ptr, "Page.loadEventFired", params).code());
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), false)); ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), false));
} }
TEST(NavigationTracker, OnSuccessfulNavigate) { TEST(NavigationTracker, OnSuccessfulNavigate) {
base::DictionaryValue params;
DeterminingLoadStateDevToolsClient client(
false, true, std::string(), &params);
BrowserInfo browser_info; BrowserInfo browser_info;
std::string version_string = "{\"Browser\": \"Chrome/44.0.2403.125\"," std::string version_string = "{\"Browser\": \"Chrome/44.0.2403.125\","
" \"WebKit-Version\": \"537.36 (@199461)\"}"; " \"WebKit-Version\": \"537.36 (@199461)\"}";
ASSERT_TRUE(ParseBrowserInfo(version_string, &browser_info).IsOk()); ASSERT_TRUE(ParseBrowserInfo(version_string, &browser_info).IsOk());
JavaScriptDialogManager dialog_manager(&client, &browser_info);
NavigationTracker tracker( base::DictionaryValue dict;
&client, NavigationTracker::kNotLoading, &browser_info, &dialog_manager); std::unique_ptr<DevToolsClient> client_uptr =
std::make_unique<DeterminingLoadStateDevToolsClient>(
false, true, std::string(), &dict);
DevToolsClient* client_ptr = client_uptr.get();
JavaScriptDialogManager dialog_manager(client_ptr, &browser_info);
EvaluateScriptWebView web_view(kOk);
NavigationTracker tracker(client_ptr, NavigationTracker::kNotLoading,
&web_view, &browser_info, &dialog_manager);
base::DictionaryValue params;
base::DictionaryValue result; base::DictionaryValue result;
result.SetString("frameId", client.GetId()); result.SetString("frameId", client_ptr->GetId());
tracker.OnCommandSuccess(&client, "Page.navigate", result, Timeout()); web_view.nextEvaluateScript("loading", kOk);
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), true)); tracker.OnCommandSuccess(client_ptr, "Page.navigate", result, Timeout());
tracker.OnEvent(&client, "Page.loadEventFired", params); ASSERT_NO_FATAL_FAILURE(
ASSERT_NO_FATAL_FAILURE(AssertPendingState(&tracker, client.GetId(), false)); AssertPendingState(&tracker, client_ptr->GetId(), true));
web_view.nextEvaluateScript("complete", kOk);
tracker.OnEvent(client_ptr, "Page.loadEventFired", params);
ASSERT_NO_FATAL_FAILURE(
AssertPendingState(&tracker, client_ptr->GetId(), false));
} }
...@@ -16,6 +16,9 @@ Status NonBlockingNavigationTracker::IsPendingNavigation( ...@@ -16,6 +16,9 @@ Status NonBlockingNavigationTracker::IsPendingNavigation(
void NonBlockingNavigationTracker::set_timed_out(bool timed_out) {} void NonBlockingNavigationTracker::set_timed_out(bool timed_out) {}
void NonBlockingNavigationTracker::ClearState(const std::string& new_frame_id) {
}
bool NonBlockingNavigationTracker::IsNonBlocking() const { bool NonBlockingNavigationTracker::IsNonBlocking() const {
return true; return true;
} }
...@@ -11,7 +11,7 @@ class Timeout; ...@@ -11,7 +11,7 @@ class Timeout;
class Status; class Status;
class NonBlockingNavigationTracker : public PageLoadStrategy { class NonBlockingNavigationTracker : public PageLoadStrategy {
public: public:
NonBlockingNavigationTracker() {} NonBlockingNavigationTracker() {}
~NonBlockingNavigationTracker() override; ~NonBlockingNavigationTracker() override;
...@@ -21,6 +21,7 @@ public: ...@@ -21,6 +21,7 @@ public:
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) override; bool* is_pending) override;
void set_timed_out(bool timed_out) override; void set_timed_out(bool timed_out) override;
void ClearState(const std::string& new_frame_id) override;
bool IsNonBlocking() const override; bool IsNonBlocking() const override;
}; };
......
...@@ -15,14 +15,17 @@ const char PageLoadStrategy::kEager[] = "eager"; ...@@ -15,14 +15,17 @@ const char PageLoadStrategy::kEager[] = "eager";
PageLoadStrategy* PageLoadStrategy::Create( PageLoadStrategy* PageLoadStrategy::Create(
std::string strategy, std::string strategy,
DevToolsClient* client, DevToolsClient* client,
WebView* web_view,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager) { const JavaScriptDialogManager* dialog_manager) {
if (strategy == kNone) { if (strategy == kNone) {
return new NonBlockingNavigationTracker(); return new NonBlockingNavigationTracker();
} else if (strategy == kNormal) { } else if (strategy == kNormal) {
return new NavigationTracker(client, browser_info, dialog_manager, false); return new NavigationTracker(client, web_view, browser_info, dialog_manager,
false);
} else if (strategy == kEager) { } else if (strategy == kEager) {
return new NavigationTracker(client, browser_info, dialog_manager, true); return new NavigationTracker(client, web_view, browser_info, dialog_manager,
true);
} else { } else {
NOTREACHED() << "invalid strategy '" << strategy << "'"; NOTREACHED() << "invalid strategy '" << strategy << "'";
return nullptr; return nullptr;
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#define CHROME_TEST_CHROMEDRIVER_CHROME_PAGE_LOAD_STRATEGY_H_ #define CHROME_TEST_CHROMEDRIVER_CHROME_PAGE_LOAD_STRATEGY_H_
#include "chrome/test/chromedriver/chrome/status.h" #include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
struct BrowserInfo; struct BrowserInfo;
class DevToolsClient; class DevToolsClient;
...@@ -14,8 +15,7 @@ class Status; ...@@ -14,8 +15,7 @@ class Status;
class Timeout; class Timeout;
class PageLoadStrategy { class PageLoadStrategy {
public:
public:
enum LoadingState { enum LoadingState {
kUnknown, kUnknown,
kLoading, kLoading,
...@@ -27,6 +27,7 @@ public: ...@@ -27,6 +27,7 @@ public:
static PageLoadStrategy* Create( static PageLoadStrategy* Create(
std::string strategy, std::string strategy,
DevToolsClient* client, DevToolsClient* client,
WebView* web_view,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager); const JavaScriptDialogManager* dialog_manager);
...@@ -36,6 +37,8 @@ public: ...@@ -36,6 +37,8 @@ public:
virtual void set_timed_out(bool timed_out) = 0; virtual void set_timed_out(bool timed_out) = 0;
virtual void ClearState(const std::string& new_frame_id) = 0;
virtual bool IsNonBlocking() const = 0; virtual bool IsNonBlocking() const = 0;
// Types of page load strategies. // Types of page load strategies.
......
...@@ -174,7 +174,7 @@ Status StubWebView::WaitForPendingNavigations(const std::string& frame_id, ...@@ -174,7 +174,7 @@ Status StubWebView::WaitForPendingNavigations(const std::string& frame_id,
Status StubWebView::IsPendingNavigation(const std::string& frame_id, Status StubWebView::IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) { bool* is_pending) const {
return Status(kOk); return Status(kOk);
} }
...@@ -235,7 +235,7 @@ Status StubWebView::SynthesizeScrollGesture(int x, ...@@ -235,7 +235,7 @@ Status StubWebView::SynthesizeScrollGesture(int x,
return Status(kOk); return Status(kOk);
} }
bool StubWebView::IsNonBlocking() { bool StubWebView::IsNonBlocking() const {
return false; return false;
} }
...@@ -254,3 +254,5 @@ std::unique_ptr<base::Value> StubWebView::GetCastSinks() { ...@@ -254,3 +254,5 @@ std::unique_ptr<base::Value> StubWebView::GetCastSinks() {
std::unique_ptr<base::Value> StubWebView::GetCastIssueMessage() { std::unique_ptr<base::Value> StubWebView::GetCastIssueMessage() {
return std::make_unique<base::Value>(); return std::make_unique<base::Value>();
} }
void StubWebView::ClearNavigationState(const std::string& new_frame_id) {}
...@@ -93,7 +93,7 @@ class StubWebView : public WebView { ...@@ -93,7 +93,7 @@ class StubWebView : public WebView {
bool stop_load_on_timeout) override; bool stop_load_on_timeout) override;
Status IsPendingNavigation(const std::string& frame_id, Status IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) override; bool* is_pending) const override;
JavaScriptDialogManager* GetJavaScriptDialogManager() override; JavaScriptDialogManager* GetJavaScriptDialogManager() override;
Status OverrideGeolocation(const Geoposition& geoposition) override; Status OverrideGeolocation(const Geoposition& geoposition) override;
Status OverrideNetworkConditions( Status OverrideNetworkConditions(
...@@ -118,11 +118,12 @@ class StubWebView : public WebView { ...@@ -118,11 +118,12 @@ class StubWebView : public WebView {
int y, int y,
int xoffset, int xoffset,
int yoffset) override; int yoffset) override;
bool IsNonBlocking() override; bool IsNonBlocking() const override;
bool IsOOPIF(const std::string& frame_id) override; bool IsOOPIF(const std::string& frame_id) override;
FrameTracker* GetFrameTracker() const override; FrameTracker* GetFrameTracker() const override;
std::unique_ptr<base::Value> GetCastSinks() override; std::unique_ptr<base::Value> GetCastSinks() override;
std::unique_ptr<base::Value> GetCastIssueMessage() override; std::unique_ptr<base::Value> GetCastIssueMessage() override;
void ClearNavigationState(const std::string& new_frame_id) override;
private: private:
std::string id_; std::string id_;
......
...@@ -194,7 +194,7 @@ class WebView { ...@@ -194,7 +194,7 @@ class WebView {
// Returns whether the frame is pending navigation. // Returns whether the frame is pending navigation.
virtual Status IsPendingNavigation(const std::string& frame_id, virtual Status IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) = 0; bool* is_pending) const = 0;
// Returns the JavaScriptDialogManager. Never null. // Returns the JavaScriptDialogManager. Never null.
virtual JavaScriptDialogManager* GetJavaScriptDialogManager() = 0; virtual JavaScriptDialogManager* GetJavaScriptDialogManager() = 0;
...@@ -246,7 +246,7 @@ class WebView { ...@@ -246,7 +246,7 @@ class WebView {
int xoffset, int xoffset,
int yoffset) = 0; int yoffset) = 0;
virtual bool IsNonBlocking() = 0; virtual bool IsNonBlocking() const = 0;
virtual bool IsOOPIF(const std::string& frame_id) = 0; virtual bool IsOOPIF(const std::string& frame_id) = 0;
...@@ -255,6 +255,8 @@ class WebView { ...@@ -255,6 +255,8 @@ class WebView {
virtual std::unique_ptr<base::Value> GetCastSinks() = 0; virtual std::unique_ptr<base::Value> GetCastSinks() = 0;
virtual std::unique_ptr<base::Value> GetCastIssueMessage() = 0; virtual std::unique_ptr<base::Value> GetCastIssueMessage() = 0;
virtual void ClearNavigationState(const std::string& new_frame_id) = 0;
}; };
#endif // CHROME_TEST_CHROMEDRIVER_CHROME_WEB_VIEW_H_ #endif // CHROME_TEST_CHROMEDRIVER_CHROME_WEB_VIEW_H_
...@@ -155,6 +155,7 @@ std::unique_ptr<base::DictionaryValue> GenerateTouchPoint( ...@@ -155,6 +155,7 @@ std::unique_ptr<base::DictionaryValue> GenerateTouchPoint(
WebViewImpl::WebViewImpl(const std::string& id, WebViewImpl::WebViewImpl(const std::string& id,
const bool w3c_compliant, const bool w3c_compliant,
const WebViewImpl* parent,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
std::unique_ptr<DevToolsClient> client, std::unique_ptr<DevToolsClient> client,
const DeviceMetrics* device_metrics, const DeviceMetrics* device_metrics,
...@@ -164,15 +165,11 @@ WebViewImpl::WebViewImpl(const std::string& id, ...@@ -164,15 +165,11 @@ WebViewImpl::WebViewImpl(const std::string& id,
browser_info_(browser_info), browser_info_(browser_info),
is_locked_(false), is_locked_(false),
is_detached_(false), is_detached_(false),
parent_(nullptr), parent_(parent),
client_(std::move(client)), client_(std::move(client)),
dom_tracker_(new DomTracker(client_.get())), dom_tracker_(new DomTracker(client_.get())),
frame_tracker_(new FrameTracker(client_.get(), this, browser_info)), frame_tracker_(new FrameTracker(client_.get(), this, browser_info)),
dialog_manager_(new JavaScriptDialogManager(client_.get(), browser_info)), dialog_manager_(new JavaScriptDialogManager(client_.get(), browser_info)),
navigation_tracker_(PageLoadStrategy::Create(page_load_strategy,
client_.get(),
browser_info,
dialog_manager_.get())),
mobile_emulation_override_manager_( mobile_emulation_override_manager_(
new MobileEmulationOverrideManager(client_.get(), device_metrics)), new MobileEmulationOverrideManager(client_.get(), device_metrics)),
geolocation_override_manager_( geolocation_override_manager_(
...@@ -188,6 +185,13 @@ WebViewImpl::WebViewImpl(const std::string& id, ...@@ -188,6 +185,13 @@ WebViewImpl::WebViewImpl(const std::string& id,
if (browser_info->is_headless) if (browser_info->is_headless)
download_directory_override_manager_ = download_directory_override_manager_ =
std::make_unique<DownloadDirectoryOverrideManager>(client_.get()); std::make_unique<DownloadDirectoryOverrideManager>(client_.get());
// Child WebViews should not have their own navigation_tracker, but defer
// all related calls to their parent. All WebViews must have either parent_
// or navigation_tracker_
if (!parent_)
navigation_tracker_ = std::unique_ptr<PageLoadStrategy>(
PageLoadStrategy::Create(page_load_strategy, client_.get(), this,
browser_info, dialog_manager_.get()));
client_->SetOwner(this); client_->SetOwner(this);
} }
...@@ -203,12 +207,16 @@ WebViewImpl* WebViewImpl::CreateChild(const std::string& session_id, ...@@ -203,12 +207,16 @@ WebViewImpl* WebViewImpl::CreateChild(const std::string& session_id,
static_cast<DevToolsClientImpl*>(client_.get())->GetRootClient(); static_cast<DevToolsClientImpl*>(client_.get())->GetRootClient();
std::unique_ptr<DevToolsClient> child_client( std::unique_ptr<DevToolsClient> child_client(
std::make_unique<DevToolsClientImpl>(root_client, session_id)); std::make_unique<DevToolsClientImpl>(root_client, session_id));
WebViewImpl* child = new WebViewImpl(target_id, w3c_compliant_, browser_info_, WebViewImpl* child = new WebViewImpl(
std::move(child_client), nullptr, target_id, w3c_compliant_, this, browser_info_, std::move(child_client),
navigation_tracker_->IsNonBlocking() nullptr,
? PageLoadStrategy::kNone navigation_tracker_->IsNonBlocking() ? PageLoadStrategy::kNone
: PageLoadStrategy::kNormal); : PageLoadStrategy::kNormal);
child->parent_ = this; if (navigation_tracker_->IsNonBlocking()) {
PageLoadStrategy* pls = navigation_tracker_.get();
NavigationTracker* nt = static_cast<NavigationTracker*>(pls);
child->client_->AddListener(static_cast<DevToolsEventListener*>(nt));
}
return child; return child;
} }
...@@ -257,7 +265,7 @@ Status WebViewImpl::Load(const std::string& url, const Timeout* timeout) { ...@@ -257,7 +265,7 @@ Status WebViewImpl::Load(const std::string& url, const Timeout* timeout) {
return Status(kUnknownError, "unsupported protocol"); return Status(kUnknownError, "unsupported protocol");
base::DictionaryValue params; base::DictionaryValue params;
params.SetString("url", url); params.SetString("url", url);
if (navigation_tracker_->IsNonBlocking()) { if (IsNonBlocking()) {
// With non-bloakcing navigation tracker, the previous navigation might // With non-bloakcing navigation tracker, the previous navigation might
// still be in progress, and this can cause the new navigate command to be // still be in progress, and this can cause the new navigate command to be
// ignored on Chrome v63 and above. Stop previous navigation first. // ignored on Chrome v63 and above. Stop previous navigation first.
...@@ -748,6 +756,10 @@ Status WebViewImpl::AddCookie(const std::string& name, ...@@ -748,6 +756,10 @@ Status WebViewImpl::AddCookie(const std::string& name,
Status WebViewImpl::WaitForPendingNavigations(const std::string& frame_id, Status WebViewImpl::WaitForPendingNavigations(const std::string& frame_id,
const Timeout& timeout, const Timeout& timeout,
bool stop_load_on_timeout) { bool stop_load_on_timeout) {
// This function should not be called for child WebViews
if (parent_ != nullptr)
return Status(kUnsupportedOperation,
"Call WaitForPendingNavigations only on the parent WebView");
VLOG(0) << "Waiting for pending navigations..."; VLOG(0) << "Waiting for pending navigations...";
const auto not_pending_navigation = const auto not_pending_navigation =
base::Bind(&WebViewImpl::IsNotPendingNavigation, base::Unretained(this), base::Bind(&WebViewImpl::IsNotPendingNavigation, base::Unretained(this),
...@@ -775,9 +787,12 @@ Status WebViewImpl::WaitForPendingNavigations(const std::string& frame_id, ...@@ -775,9 +787,12 @@ Status WebViewImpl::WaitForPendingNavigations(const std::string& frame_id,
Status WebViewImpl::IsPendingNavigation(const std::string& frame_id, Status WebViewImpl::IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) { bool* is_pending) const {
return if (navigation_tracker_)
navigation_tracker_->IsPendingNavigation(frame_id, timeout, is_pending); return navigation_tracker_->IsPendingNavigation(frame_id, timeout,
is_pending);
else
return parent_->IsPendingNavigation(frame_id, timeout, is_pending);
} }
JavaScriptDialogManager* WebViewImpl::GetJavaScriptDialogManager() { JavaScriptDialogManager* WebViewImpl::GetJavaScriptDialogManager() {
...@@ -1105,6 +1120,10 @@ Status WebViewImpl::CallAsyncFunctionInternal( ...@@ -1105,6 +1120,10 @@ Status WebViewImpl::CallAsyncFunctionInternal(
} }
} }
void WebViewImpl::ClearNavigationState(const std::string& new_frame_id) {
navigation_tracker_->ClearState(new_frame_id);
}
Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id, Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_not_pending) { bool* is_not_pending) {
...@@ -1126,8 +1145,11 @@ Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id, ...@@ -1126,8 +1145,11 @@ Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id,
return Status(kOk); return Status(kOk);
} }
bool WebViewImpl::IsNonBlocking() { bool WebViewImpl::IsNonBlocking() const {
return navigation_tracker_->IsNonBlocking(); if (navigation_tracker_)
return navigation_tracker_->IsNonBlocking();
else
return parent_->IsNonBlocking();
} }
bool WebViewImpl::IsOOPIF(const std::string& frame_id) { bool WebViewImpl::IsOOPIF(const std::string& frame_id) {
......
...@@ -40,6 +40,7 @@ class WebViewImpl : public WebView { ...@@ -40,6 +40,7 @@ class WebViewImpl : public WebView {
public: public:
WebViewImpl(const std::string& id, WebViewImpl(const std::string& id,
const bool w3c_compliant, const bool w3c_compliant,
const WebViewImpl* parent,
const BrowserInfo* browser_info, const BrowserInfo* browser_info,
std::unique_ptr<DevToolsClient> client, std::unique_ptr<DevToolsClient> client,
const DeviceMetrics* device_metrics, const DeviceMetrics* device_metrics,
...@@ -133,7 +134,7 @@ class WebViewImpl : public WebView { ...@@ -133,7 +134,7 @@ class WebViewImpl : public WebView {
bool stop_load_on_timeout) override; bool stop_load_on_timeout) override;
Status IsPendingNavigation(const std::string& frame_id, Status IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout, const Timeout* timeout,
bool* is_pending) override; bool* is_pending) const override;
JavaScriptDialogManager* GetJavaScriptDialogManager() override; JavaScriptDialogManager* GetJavaScriptDialogManager() override;
Status OverrideGeolocation(const Geoposition& geoposition) override; Status OverrideGeolocation(const Geoposition& geoposition) override;
Status OverrideNetworkConditions( Status OverrideNetworkConditions(
...@@ -158,11 +159,12 @@ class WebViewImpl : public WebView { ...@@ -158,11 +159,12 @@ class WebViewImpl : public WebView {
int y, int y,
int xoffset, int xoffset,
int yoffset) override; int yoffset) override;
bool IsNonBlocking() override; bool IsNonBlocking() const override;
bool IsOOPIF(const std::string& frame_id) override; bool IsOOPIF(const std::string& frame_id) override;
FrameTracker* GetFrameTracker() const override; FrameTracker* GetFrameTracker() const override;
std::unique_ptr<base::Value> GetCastSinks() override; std::unique_ptr<base::Value> GetCastSinks() override;
std::unique_ptr<base::Value> GetCastIssueMessage() override; std::unique_ptr<base::Value> GetCastIssueMessage() override;
void ClearNavigationState(const std::string& new_frame_id) override;
const WebViewImpl* GetParent() const; const WebViewImpl* GetParent() const;
bool Lock(); bool Lock();
......
...@@ -8,11 +8,18 @@ ...@@ -8,11 +8,18 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "base/bind.h"
#include "base/compiler_specific.h" #include "base/compiler_specific.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "base/values.h" #include "base/values.h"
#include "chrome/test/chromedriver/chrome/browser_info.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h" #include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/page_load_strategy.h"
#include "chrome/test/chromedriver/chrome/status.h" #include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/net/sync_websocket.h"
#include "chrome/test/chromedriver/net/sync_websocket_factory.h"
#include "chrome/test/chromedriver/net/timeout.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
namespace { namespace {
...@@ -248,3 +255,125 @@ TEST(ParseCallFunctionResult, ScriptError) { ...@@ -248,3 +255,125 @@ TEST(ParseCallFunctionResult, ScriptError) {
ASSERT_EQ(1, status.code()); ASSERT_EQ(1, status.code());
ASSERT_FALSE(result); ASSERT_FALSE(result);
} }
namespace {
class MockSyncWebSocket : public SyncWebSocket {
public:
explicit MockSyncWebSocket(SyncWebSocket::StatusCode next_status)
: connected_(true),
id_(-1),
queued_messages_(1),
next_status_(next_status) {}
~MockSyncWebSocket() override {}
bool IsConnected() override { return connected_; }
bool Connect(const GURL& url) override {
EXPECT_STREQ("http://url/", url.possibly_invalid_spec().c_str());
connected_ = true;
return true;
}
bool Send(const std::string& message) override { return false; }
SyncWebSocket::StatusCode ReceiveNextMessage(
std::string* message,
const Timeout& timeout) override {
return next_status_;
}
bool HasNextMessage() override { return queued_messages_ > 0; }
protected:
bool connected_;
int id_;
int queued_messages_;
SyncWebSocket::StatusCode next_status_;
};
std::unique_ptr<SyncWebSocket> CreateMockSyncWebSocket(
SyncWebSocket::StatusCode next_status) {
return std::make_unique<MockSyncWebSocket>(next_status);
}
} // namespace
TEST(CreateChild, IsNonBlocking_NoErrors) {
SyncWebSocketFactory factory =
base::Bind(&CreateMockSyncWebSocket, SyncWebSocket::kOk);
// CreateChild relies on client_ being a DevToolsClientImpl, so no mocking
std::unique_ptr<DevToolsClientImpl> client_uptr =
std::make_unique<DevToolsClientImpl>(factory, "http://url", "id");
DevToolsClientImpl* client_ptr = client_uptr.get();
BrowserInfo browser_info;
WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kEager);
ASSERT_FALSE(parent_view.IsNonBlocking());
std::string sessionid = "2";
std::unique_ptr<WebViewImpl> child_view =
std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234"));
ASSERT_NO_FATAL_FAILURE(child_view->IsNonBlocking());
ASSERT_FALSE(child_view->IsNonBlocking());
}
TEST(CreateChild, Load_NoErrors) {
SyncWebSocketFactory factory =
base::Bind(&CreateMockSyncWebSocket, SyncWebSocket::kOk);
// CreateChild relies on client_ being a DevToolsClientImpl, so no mocking
std::unique_ptr<DevToolsClientImpl> client_uptr =
std::make_unique<DevToolsClientImpl>(factory, "http://url", "id");
DevToolsClientImpl* client_ptr = client_uptr.get();
BrowserInfo browser_info;
WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNone);
std::string sessionid = "2";
std::unique_ptr<WebViewImpl> child_view =
std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234"));
ASSERT_NO_FATAL_FAILURE(child_view->Load("chrome://version", nullptr));
}
TEST(CreateChild, WaitForPendingNavigations_NoErrors) {
SyncWebSocketFactory factory =
base::Bind(&CreateMockSyncWebSocket, SyncWebSocket::kTimeout);
// CreateChild relies on client_ being a DevToolsClientImpl, so no mocking
std::unique_ptr<DevToolsClientImpl> client_uptr =
std::make_unique<DevToolsClientImpl>(factory, "http://url", "id");
DevToolsClientImpl* client_ptr = client_uptr.get();
BrowserInfo browser_info;
WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNone);
std::string sessionid = "2";
std::unique_ptr<WebViewImpl> child_view =
std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234"));
// child_view gets no socket...
ASSERT_NO_FATAL_FAILURE(child_view->WaitForPendingNavigations(
"1234", Timeout(base::TimeDelta::FromMilliseconds(10)), true));
}
TEST(CreateChild, IsPendingNavigation_NoErrors) {
SyncWebSocketFactory factory =
base::Bind(&CreateMockSyncWebSocket, SyncWebSocket::kOk);
// CreateChild relies on client_ being a DevToolsClientImpl, so no mocking
std::unique_ptr<DevToolsClientImpl> client_uptr =
std::make_unique<DevToolsClientImpl>(factory, "http://url", "id");
DevToolsClientImpl* client_ptr = client_uptr.get();
BrowserInfo browser_info;
WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info,
std::move(client_uptr), nullptr,
PageLoadStrategy::kNormal);
std::string sessionid = "2";
std::unique_ptr<WebViewImpl> child_view =
std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234"));
Timeout timeout(base::TimeDelta::FromMilliseconds(10));
bool result;
ASSERT_NO_FATAL_FAILURE(
child_view->IsPendingNavigation("1234", &timeout, &result));
}
...@@ -104,11 +104,13 @@ Status Session::GetTargetWindow(WebView** web_view) { ...@@ -104,11 +104,13 @@ Status Session::GetTargetWindow(WebView** web_view) {
void Session::SwitchToTopFrame() { void Session::SwitchToTopFrame() {
frames.clear(); frames.clear();
ClearNavigationState(true);
} }
void Session::SwitchToParentFrame() { void Session::SwitchToParentFrame() {
if (!frames.empty()) if (!frames.empty())
frames.pop_back(); frames.pop_back();
ClearNavigationState(false);
} }
void Session::SwitchToSubFrame(const std::string& frame_id, void Session::SwitchToSubFrame(const std::string& frame_id,
...@@ -117,6 +119,22 @@ void Session::SwitchToSubFrame(const std::string& frame_id, ...@@ -117,6 +119,22 @@ void Session::SwitchToSubFrame(const std::string& frame_id,
if (!frames.empty()) if (!frames.empty())
parent_frame_id = frames.back().frame_id; parent_frame_id = frames.back().frame_id;
frames.push_back(FrameInfo(parent_frame_id, frame_id, chromedriver_frame_id)); frames.push_back(FrameInfo(parent_frame_id, frame_id, chromedriver_frame_id));
ClearNavigationState(false);
}
void Session::ClearNavigationState(bool for_top_frame) {
WebView* web_view = nullptr;
Status status = GetTargetWindow(&web_view);
if (!status.IsError()) {
if (for_top_frame)
web_view->ClearNavigationState(std::string());
else
web_view->ClearNavigationState(GetCurrentFrameId());
} else {
// Do nothing; this should be very rare because callers of this function
// have already called GetTargetWindow.
// Let later code handle issues that arise from the invalid state.
}
} }
std::string Session::GetCurrentFrameId() const { std::string Session::GetCurrentFrameId() const {
......
...@@ -78,6 +78,7 @@ struct Session { ...@@ -78,6 +78,7 @@ struct Session {
void SwitchToParentFrame(); void SwitchToParentFrame();
void SwitchToSubFrame(const std::string& frame_id, void SwitchToSubFrame(const std::string& frame_id,
const std::string& chromedriver_frame_id); const std::string& chromedriver_frame_id);
void ClearNavigationState(bool for_top_frame);
std::string GetCurrentFrameId() const; std::string GetCurrentFrameId() const;
std::vector<WebDriverLog*> GetAllLogs() const; std::vector<WebDriverLog*> GetAllLogs() const;
......
...@@ -2002,6 +2002,56 @@ class ChromeDriverTest(ChromeDriverBaseTestWithWebServer): ...@@ -2002,6 +2002,56 @@ class ChromeDriverTest(ChromeDriverBaseTestWithWebServer):
self._driver.Load(self._http_server.GetUrl('localhost') self._driver.Load(self._http_server.GetUrl('localhost')
+ '/chromedriver/empty.html') + '/chromedriver/empty.html')
def testWaitForCurrentFrameToLoad(self):
"""Verify ChromeDriver waits for loading events of current frame
Regression test for bug
https://bugs.chromium.org/p/chromedriver/issues/detail?id=3164
Clicking element in frame triggers reload of that frame, click should not
return until loading is complete.
"""
def waitAndRespond():
# test may not detect regression without small sleep.
# locally, .2 didn't fail before code change, .3 did
time.sleep(.5)
self._sync_server.RespondWithContent(
"""
<html>
<body>
<p id='valueToRead'>11</p>
</body>
</html>
""")
self._http_server.SetDataForPath('/page10.html',
"""
<html>
<head>
<title>
Frame
</title>
<script>
function reloadWith(i) {
window.location.assign('%s');
}
</script>
</head>
<body>
<button id='prev' onclick="reloadWith(9)">-1</button>
<button id='next' onclick="reloadWith(11)">+1</button>
<p id='valueToRead'>10</p>
</body>
</html>
""" % self._sync_server.GetUrl())
self._driver.Load(self.GetHttpUrlForFile(
'/chromedriver/page_for_next_iframe.html'))
frame = self._driver.FindElement('tag name', 'iframe')
self._driver.SwitchToFrame(frame);
thread = threading.Thread(target=waitAndRespond)
thread.start()
self._driver.FindElement('css selector', '#next').Click()
value_display = self._driver.FindElement('css selector', '#valueToRead')
self.assertEquals('11', value_display.GetText())
def testSlowIFrame(self): def testSlowIFrame(self):
"""Verify ChromeDriver does not wait for slow frames to load. """Verify ChromeDriver does not wait for slow frames to load.
Regression test for bugs Regression test for bugs
...@@ -2011,7 +2061,8 @@ class ChromeDriverTest(ChromeDriverBaseTestWithWebServer): ...@@ -2011,7 +2061,8 @@ class ChromeDriverTest(ChromeDriverBaseTestWithWebServer):
def waitAndRespond(): def waitAndRespond():
# Send iframe contents slowly # Send iframe contents slowly
time.sleep(2) time.sleep(2)
self._sync_server.RespondWithContent('<html>IFrame contents</html>') self._sync_server.RespondWithContent(
'<html><div id=iframediv>IFrame contents</div></html>')
self._http_server.SetDataForPath('/top.html', self._http_server.SetDataForPath('/top.html',
""" """
...@@ -2031,15 +2082,18 @@ class ChromeDriverTest(ChromeDriverBaseTestWithWebServer): ...@@ -2031,15 +2082,18 @@ class ChromeDriverTest(ChromeDriverBaseTestWithWebServer):
self._driver.Load(self._http_server.GetUrl() + '/top.html') self._driver.Load(self._http_server.GetUrl() + '/top.html')
thread = threading.Thread(target=waitAndRespond) thread = threading.Thread(target=waitAndRespond)
thread.start() thread.start()
start = time.time()
# Click should not wait for frame to load, so elapsed time from this
# command should be < 2 seconds.
self._driver.FindElement('css selector', '#button').Click() self._driver.FindElement('css selector', '#button').Click()
# Correct ChromeDriver behavior should not wait for iframe to self.assertLess(time.time() - start, 2.0)
# load. Therefore, SwitchToFrame should fail, we remain in the top
# frame, and FindElement should succeed. If ChromeDriver incorrectly
# waits for slow iframe to load, then SwitchToFrame succeeds,
# and element with id='top' won't be found.
frame = self._driver.FindElement('css selector', '#iframe') frame = self._driver.FindElement('css selector', '#iframe')
# WaitForPendingNavigations examines the load state of the current frame
# so ChromeDriver will wait for frame to load after SwitchToFrame
# start is reused because that began the pause for the frame load
self._driver.SwitchToFrame(frame) self._driver.SwitchToFrame(frame)
self._driver.FindElement('css selector', '#top') self.assertGreaterEqual(time.time() - start, 2.0)
self._driver.FindElement('css selector', '#iframediv')
thread.join() thread.join()
@staticmethod @staticmethod
......
<html>
<head>
<title>Test driver</title>
</head>
<body>
<iframe id="inlineFrameExample"
title="Inline Frame Example"
width="300"
height="200"
src="/page10.html">
</iframe>
</body>
</html>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment