1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992
|
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/render_view_host.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/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/switches.h"
#include "url/gurl.h"
// RunUntilInputProcessed will force a Blink lifecycle which is needed
// because did_scroll is set in an onscroll handler which may be delayed from
// the scroll by a frame.
#define EXPECT_DID_SCROLL(scrolled) \
RunUntilInputProcessed(GetWidgetHost()); \
EXPECT_EQ(scrolled, EvalJs(main_contents, "did_scroll;", \
EXECUTE_SCRIPT_NO_USER_GESTURE));
#define ASSERT_DID_SCROLL(scrolled) \
RunUntilInputProcessed(GetWidgetHost()); \
ASSERT_EQ(scrolled, EvalJs(main_contents, "did_scroll;", \
EXECUTE_SCRIPT_NO_USER_GESTURE));
namespace content {
class TextFragmentAnchorBrowserTest : public ContentBrowserTest {
protected:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"TextFragmentIdentifiers");
// Slow bots are flaky due to slower loading interacting with
// deferred commits.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
// Simulates a click on the middle of the DOM element with the given |id|.
void ClickElementWithId(WebContents* web_contents, const std::string& id) {
// Get the center coordinates of the DOM element.
const int x = EvalJs(web_contents,
JsReplace("const bounds = "
"document.getElementById($1)."
"getBoundingClientRect();"
"Math.floor(bounds.left + bounds.width / 2)",
id))
.ExtractInt();
const int y = EvalJs(web_contents,
JsReplace("const bounds = "
"document.getElementById($1)."
"getBoundingClientRect();"
"Math.floor(bounds.top + bounds.height / 2)",
id))
.ExtractInt();
SimulateMouseClickAt(web_contents, 0, blink::WebMouseEvent::Button::kLeft,
gfx::Point(x, y));
RunUntilInputProcessed(GetWidgetHost());
}
void WaitForPageLoad(WebContents* contents) {
EXPECT_TRUE(WaitForLoadStop(contents));
EXPECT_TRUE(WaitForRenderFrameReady(contents->GetPrimaryMainFrame()));
}
RenderWidgetHostImpl* GetWidgetHost() {
return RenderWidgetHostImpl::From(shell()
->web_contents()
->GetPrimaryMainFrame()
->GetRenderViewHost()
->GetWidget());
}
};
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledOnUserNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target_text_link.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContents* main_contents = shell()->web_contents();
TestNavigationObserver observer(main_contents);
// We need to wait until hit test data is available.
HitTestRegionObserver hittest_observer(GetWidgetHost()->GetFrameSinkId());
hittest_observer.WaitForHitTestData();
ClickElementWithId(main_contents, "link");
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
// Observe the frame after page is loaded. Note that we need to initialize
// this after navigation because the main RenderFrameHost might have changed
// from before the navigation started.
RenderFrameSubmissionObserver frame_observer(main_contents);
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
EnabledOnBrowserNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
WebContents* main_contents = shell()->web_contents();
RenderFrameSubmissionObserver frame_observer(main_contents);
EXPECT_TRUE(NavigateToURL(shell(), url));
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
EnabledOnUserGestureScriptNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/empty.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContents* main_contents = shell()->web_contents();
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(
ExecJs(main_contents, "location = '" + target_text_url.spec() + "';"));
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
// Observe the frame after page is loaded. Note that we need to initialize
// this after navigation because the main RenderFrameHost might have changed
// from before the navigation started.
RenderFrameSubmissionObserver frame_observer(main_contents);
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
// Ensures that a simulated redirect service works correctly. That is, only the
// initial NavigateToURL has a user gesture but this should be propagated
// through the window.location navigation which doesn't have a user gesture.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
UserGesturePassedThroughRedirect) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/empty.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
// This navigtion is simulated as if it came from the omnibox, hence it is
// considered to be user initiated.
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContents* main_contents = shell()->web_contents();
TestNavigationObserver observer(main_contents);
// This navigation occurs without a user gesture, simulating a client
// redirect. However, because the above navigation didn't activate a text
// fragment, permission should be propagated to this navigation.
EXPECT_TRUE(ExecJs(main_contents,
"location = '" + target_text_url.spec() + "';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
WaitForPageLoad(main_contents);
RenderFrameSubmissionObserver frame_observer(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
// Ensures that a text fragment activation consumes a user gesture so that
// future navigations cannot activate a text fragment without a new user
// gesture.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, UserGestureConsumed) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL empty_page_url(embedded_test_server()->GetURL("/empty.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
WebContents* main_contents = shell()->web_contents();
// This navigtion is simulated as if it came from the omnibox, hence it is
// considered to be user initiated.
{
TestNavigationObserver observer(main_contents);
ASSERT_TRUE(NavigateToURL(shell(), target_text_url));
observer.Wait();
ASSERT_EQ(target_text_url, main_contents->GetLastCommittedURL());
// Ensure the page did scroll to the text fragment. Note, we can't use
// WaitForPageLoad since the WaitForRenderFrameReady executes javascript
// with a user gesture.
WaitForLoadStop(main_contents);
RenderFrameSubmissionObserver frame_observer(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
ASSERT_DID_SCROLL(true);
}
// We now want to try a second text fragment navigation. Same document
// navigations are blocked so we'll navigate away first.
{
TestNavigationObserver observer(main_contents);
ASSERT_TRUE(ExecJs(main_contents,
"location = '" + empty_page_url.spec() + "';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
ASSERT_EQ(empty_page_url, main_contents->GetLastCommittedURL());
WaitForLoadStop(main_contents);
}
// Now try another text fragment navigation. Since we haven't had a user
// gesture since the last one, it should be blocked.
{
TestNavigationObserver observer(main_contents);
ASSERT_TRUE(ExecJs(main_contents,
"location = '" + target_text_url.spec() + "';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
ASSERT_EQ(target_text_url, main_contents->GetLastCommittedURL());
WaitForLoadStop(main_contents);
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
EXPECT_DID_SCROLL(false);
}
}
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
DisabledOnScriptHistoryNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL target_text_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
GURL url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), target_text_url));
WebContents* main_contents = shell()->web_contents();
// The test assumes the previous page gets deleted after navigation and will
// be recreated with did_scroll == false. Disable back/forward cache to ensure
// that it doesn't get preserved in the cache.
DisableBackForwardCacheForTesting(
main_contents, BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
{
// The RenderFrameSubmissionObserver destructor expects the RenderFrameHost
// stays the same until it gets destructed, so we need to scope this to make
// sure it gets destructed before the next navigation.
RenderFrameSubmissionObserver frame_observer(main_contents);
frame_observer.WaitForScrollOffsetAtTop(false);
// Scroll the page back to top so scroll restoration does not scroll the
// target back into view.
EXPECT_TRUE(ExecJs(main_contents, "window.scrollTo(0, 0)"));
frame_observer.WaitForScrollOffsetAtTop(true);
}
EXPECT_TRUE(NavigateToURL(shell(), url));
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(
ExecJs(main_contents, "history.back()", EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
WaitForPageLoad(main_contents);
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
// Note: we use a scroll handler in the page to check whether any scrolls
// happened at all, rather than checking the current scroll offset. This is
// to ensure that if the offset is reset back to the top for other reasons
// (e.g. history restoration) we still fail this test. See
// https://crbug.com/1042986 for why this matters.
EXPECT_DID_SCROLL(false);
}
// crbug.com/1470712: Flaky on CrOS Debug
#if BUILDFLAG(IS_CHROMEOS) && !defined(NDEBUG)
#define MAYBE_SameDocumentBrowserNavigation \
DISABLED_SameDocumentBrowserNavigation
#else
#define MAYBE_SameDocumentBrowserNavigation SameDocumentBrowserNavigation
#endif
// Ensure a same-document navigation from browser UI scrolls to the text
// fragment.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
MAYBE_SameDocumentBrowserNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
WebContents* main_contents = shell()->web_contents();
RenderFrameSubmissionObserver frame_observer(main_contents);
EXPECT_TRUE(NavigateToURL(shell(), url));
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(false);
// Scroll the page back to top. Make sure we reset the |did_scroll| variable
// we'll use below to ensure the same-document navigation invokes the text
// fragment.
EXPECT_TRUE(ExecJs(main_contents, "window.scrollTo(0, 0)"));
frame_observer.WaitForScrollOffsetAtTop(true);
RunUntilInputProcessed(GetWidgetHost());
EXPECT_TRUE(ExecJs(main_contents, "did_scroll = false;"));
// Perform a same-document browser initiated navigation
GURL same_doc_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=some"));
EXPECT_TRUE(NavigateToURL(shell(), same_doc_url));
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
// crbug.com/1470712: Flaky on CrOS Debug
#if BUILDFLAG(IS_CHROMEOS) && !defined(NDEBUG)
#define MAYBE_SameDocumentBrowserNavigationOnScriptNavigatedDocument \
DISABLED_SameDocumentBrowserNavigationOnScriptNavigatedDocument
#else
#define MAYBE_SameDocumentBrowserNavigationOnScriptNavigatedDocument \
SameDocumentBrowserNavigationOnScriptNavigatedDocument
#endif
IN_PROC_BROWSER_TEST_F(
TextFragmentAnchorBrowserTest,
MAYBE_SameDocumentBrowserNavigationOnScriptNavigatedDocument) {
ASSERT_TRUE(embedded_test_server()->Start());
WebContents* main_contents = shell()->web_contents();
// The test assumes the RenderWidgetHost stays the same after navigation,
// which won't happen if same-site back/forward-cache is enabled. Disable it
// so that we will keep RenderWidgetHost even after navigation.
DisableBackForwardCacheForTesting(
main_contents, BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);
// Load an initial page
{
GURL initial_url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), initial_url));
WaitForPageLoad(main_contents);
}
// Now navigate to the target document without a user gesture. We provide a
// text-fragment here and expect it to be invoked because the initial load
// was browser-initiated so its transferred to this load via the text fragment
// token. This navigation ensures the token is consumed.
{
GURL target_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, "location = '" + target_url.spec() + "';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
RenderFrameSubmissionObserver frame_observer(main_contents);
EXPECT_EQ(target_url, main_contents->GetLastCommittedURL());
frame_observer.WaitForScrollOffsetAtTop(false);
EXPECT_DID_SCROLL(true);
}
// Scroll the page back to top. Make sure we reset the |did_scroll| variable
// we'll use below to ensure the same-document navigation invokes the text
// fragment.
{
RenderFrameSubmissionObserver frame_observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, "window.scrollTo(0, 0)"));
frame_observer.WaitForScrollOffsetAtTop(true);
RunUntilInputProcessed(GetWidgetHost());
EXPECT_TRUE(ExecJs(main_contents, "did_scroll = false;"));
}
// Perform a same-document browser initiated navigation. This should cause a
// scroll because the navigation is browser-initiated, despite the fact that
// the document was loaded without a user gesture.
{
GURL same_doc_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=some"));
EXPECT_TRUE(NavigateToURL(shell(), same_doc_url));
RenderFrameSubmissionObserver frame_observer(main_contents);
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
}
// Ensure a text fragment token isn't generated via history.back() navigation.
// This is a tricky case because all history navigations (including script
// initiated) appear to the renderer as "browser-initiated".
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
HistoryDoesntGenerateToken) {
ASSERT_TRUE(embedded_test_server()->Start());
WebContents* main_contents = shell()->web_contents();
GURL url(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_content.html#:~:text=text"));
{
// RenderFrameSubmissionObserver must not outlive the RenderWidgetHostImpl
// so ensure it's destructed before we navigate to a new page.
RenderFrameSubmissionObserver frame_observer(main_contents);
// Load a page with a text-fragment
EXPECT_TRUE(NavigateToURL(shell(), url));
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(false);
// Scroll the page back to top. Make sure we reset the |did_scroll| variable
// we'll use below to ensure the same-document navigation invokes the text
// fragment.
EXPECT_TRUE(ExecJs(main_contents, "window.scrollTo(0, 0)",
EXECUTE_SCRIPT_NO_USER_GESTURE));
frame_observer.WaitForScrollOffsetAtTop(true);
EXPECT_TRUE(ExecJs(main_contents, "did_scroll = false;",
EXECUTE_SCRIPT_NO_USER_GESTURE));
// Perform a scripted same-document navigation to a non-existent fragment to
// generate a history entry.
{
GURL temp_url(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_content.html#doesntexist"));
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, JsReplace("location = $1;", temp_url),
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(temp_url, main_contents->GetLastCommittedURL());
}
// Navigate back using history.back().
{
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, "history.back();",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(url, main_contents->GetLastCommittedURL());
// The page should be restored to where we left off at the top.
RunUntilInputProcessed(GetWidgetHost());
ASSERT_EQ(EvalJs(main_contents, "window.scrollY;",
EXECUTE_SCRIPT_NO_USER_GESTURE)
.ExtractInt(),
0);
ASSERT_DID_SCROLL(false);
}
}
// Now try to navigate to a new page with a text-fragment. This should be
// blocked because the token was consumed in the initial load at the top and
// a new one must not have been generated by the same document navigations
// above.
{
GURL new_url(embedded_test_server()->GetURL(
"b.com", "/scrollable_page_with_content.html#:~:text=Some"));
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, "location = '" + new_url.spec() + "';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(new_url, main_contents->GetLastCommittedURL());
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
EXPECT_DID_SCROLL(false);
}
}
// Ensure same-document navigation to a text-fragment works when initiated from
// the document itself.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
SameDocumentScriptNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(
embedded_test_server()->GetURL("/scrollable_page_with_content.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=some"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContents* main_contents = shell()->web_contents();
TestNavigationObserver observer(main_contents);
// User gesture not required since the script is running in the same origin
// as the page.
EXPECT_TRUE(ExecJs(main_contents,
"location = '" + target_text_url.spec() + "';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
WaitForPageLoad(main_contents);
RenderFrameSubmissionObserver frame_observer(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
// Ensure same-document navigation to a text-fragment works when initiated from
// the same origin.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
SameDocumentScriptNavigationSameOrigin) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_content.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_content.html#:~:text=some"));
GURL cross_origin_inner_url(
embedded_test_server()->GetURL("a.com", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* main_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = main_contents->GetPrimaryFrameTree().root();
// Insert a same-origin iframe from which we'll execute script.
{
const auto script = JsReplace(
R"JS(
let f = document.createElement("iframe");
f.src=$1;
document.body.appendChild(f);
)JS",
cross_origin_inner_url);
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
ASSERT_EQ(1u, root->child_count());
}
// Try navigating the top frame to a same-document text fragment from inside
// the iframe. This should be allowed, even without user-gesture, since it's
// same-origin; script is able to see all its content anyway.
{
TestNavigationObserver observer(main_contents);
RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(child_rfh,
JsReplace("window.top.location = $1;", target_text_url),
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
RenderFrameSubmissionObserver frame_observer(main_contents);
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
}
// Ensure same-document navigation to a text-fragment is blocked when initiated
// from a different origin.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
SameDocumentScriptNavigationCrossOrigin) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_content.html"));
GURL target_text_url(embedded_test_server()->GetURL(
"a.com", "/scrollable_page_with_content.html#:~:text=some"));
GURL cross_origin_inner_url(
embedded_test_server()->GetURL("b.com", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* main_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = main_contents->GetPrimaryFrameTree().root();
// Insert a cross-origin iframe from which we'll execute script.
{
const auto script = JsReplace(
R"JS(
let f = document.createElement("iframe");
f.src=$1;
document.body.appendChild(f);
)JS",
cross_origin_inner_url);
TestNavigationObserver observer(main_contents);
EXPECT_TRUE(ExecJs(main_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
ASSERT_EQ(1u, root->child_count());
}
// Try navigating the top frame to a same-document text fragment from inside
// the iframe. This should be blocked as its cross-origin. Note, the script
// executes with a user gesture but this is still blocked. Same-document
// navigations are allowed only when initiated from same-origin or
// browser-UI.
{
TestNavigationObserver observer(main_contents);
RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(
child_rfh, JsReplace("window.top.location = $1;", target_text_url)));
observer.Wait();
EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
WaitForPageLoad(main_contents);
EXPECT_DID_SCROLL(false);
}
}
// Test that when ForceLoadAtTop document policy is explicitly turned off,
// scrolling to a text fragment is allowed.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledByDocumentPolicy) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/target.html");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target.html#:~:text=text"));
WebContents* main_contents = shell()->web_contents();
RenderFrameSubmissionObserver frame_observer(main_contents);
// Load the target document
TestNavigationManager navigation_manager(main_contents, url);
shell()->LoadURL(url);
// Start navigation
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
// Send Document-Policy header
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top=?0\r\n"
"\r\n"
"<script>"
" let did_scroll = false;"
" window.addEventListener('scroll', () => {"
" did_scroll = true;"
" });"
"</script>"
"<p style='position: absolute; top: 10000px;'>Some text</p>");
response.Done();
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
}
// Test that the ForceLoadAtTop document policy disables scrolling to a text
// fragment.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
DisabledByDocumentPolicy) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/target.html");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target.html#:~:text=text"));
WebContents* main_contents = shell()->web_contents();
// Load the target document
TestNavigationManager navigation_manager(main_contents, url);
shell()->LoadURL(url);
// Start navigation
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
// Send Document-Policy header
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top\r\n"
"\r\n"
"<script>"
" let did_scroll = false;"
" window.addEventListener('scroll', () => {"
" did_scroll = true;"
" });"
"</script>"
"<p style='position: absolute; top: 10000px;'>Some text</p>");
response.Done();
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
WaitForPageLoad(main_contents);
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
EXPECT_DID_SCROLL(false);
}
// Test that Tab key press puts focus from the start of the text directive that
// was scrolled into view.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, TabFocus) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(
embedded_test_server()->GetURL("/scrollable_page_with_anchor.html#:~:"
"text=nonexistent&text=text&text=more"));
WebContents* main_contents = shell()->web_contents();
RenderFrameSubmissionObserver frame_observer(main_contents);
EXPECT_TRUE(NavigateToURL(shell(), url));
WaitForPageLoad(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
DOMMessageQueue msg_queue(main_contents);
SimulateKeyPress(main_contents, ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
// Wait for focus to happen.
std::string message;
EXPECT_TRUE(msg_queue.WaitForMessage(&message));
EXPECT_EQ("\"FocusDone2\"", message);
}
class ForceLoadAtTopBrowserTest : public TextFragmentAnchorBrowserTest {
protected:
// Loads the given path as predetermined HTML response with a
// |Document-Policy: force-load-at-top| header and waits for the navigation
// to finish.
void LoadScrollablePageWithContent(const std::string& path) {
std::size_t hash_pos = path.find("#");
std::string path_without_fragment = path;
if (hash_pos != std::string::npos) {
path_without_fragment = path.substr(0, hash_pos);
}
net::test_server::ControllableHttpResponse response(embedded_test_server(),
path_without_fragment);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL(path));
RenderFrameSubmissionObserver frame_observer(shell()->web_contents());
// Load the target document.
TestNavigationManager navigation_manager(shell()->web_contents(), url);
shell()->LoadURL(url);
// Start navigation
ASSERT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
// Send Document-Policy header
response.WaitForRequest();
std::string response_string =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top\r\n"
"\r\n"
R"HTML(
<html>
<head>
<meta name="viewport" content="width=device-width">
<script>
let did_scroll = false;
window.addEventListener('scroll', () => {
did_scroll = true;
});
</script>
<style>
p {
position: absolute;
top: 10000px;
}
</style>
</head>
<body>
<a id="link" href="#text">Go Down</a>
<p id="text">Some text</p>
</body>
</html>
)HTML";
response.Send(response_string);
response.Done();
ASSERT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
WaitForPageLoad(shell()->web_contents());
}
};
// Test that scroll restoration is disabled with ForceLoadAtTop
IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, ScrollRestorationDisabled) {
ASSERT_NO_FATAL_FAILURE(LoadScrollablePageWithContent("/index.html"));
WebContents* main_contents = shell()->web_contents();
// This test expects the document is freshly loaded on the back navigation
// so that the document policy to force-load-at-top will run. This will not
// happen if the document is back-forward cached, so we need to disable it.
DisableBackForwardCacheForTesting(main_contents,
BackForwardCache::TEST_REQUIRES_NO_CACHING);
// Scroll down the page a bit
EXPECT_TRUE(ExecJs(main_contents, "window.scrollTo(0, 1000)"));
// Navigate away
EXPECT_TRUE(ExecJs(main_contents, "window.location = 'about:blank'"));
EXPECT_TRUE(WaitForLoadStop(main_contents));
// Navigate back
EXPECT_TRUE(ExecJs(main_contents, "history.back()"));
EXPECT_TRUE(WaitForLoadStop(main_contents));
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
RunUntilInputProcessed(GetWidgetHost());
EXPECT_EQ(EvalJs(main_contents, "window.scrollY;").ExtractInt(), 0);
}
// Test that element fragment anchor scrolling is disabled with ForceLoadAtTop
IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, FragmentAnchorDisabled) {
ASSERT_NO_FATAL_FAILURE(LoadScrollablePageWithContent("/index.html#text"));
WebContents* main_contents = shell()->web_contents();
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
RunUntilInputProcessed(GetWidgetHost());
EXPECT_DID_SCROLL(false);
}
// Ensure the ForceLoadAtTop policy doesn't prevent same-document fragment
// navigations.
IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, SameDocumentNavigation) {
ASSERT_NO_FATAL_FAILURE(LoadScrollablePageWithContent("/index.html"));
WebContents* main_contents = shell()->web_contents();
ASSERT_DID_SCROLL(false);
// Click on a link with a fragment id. Ensure we scroll to the targeted
// element.
ClickElementWithId(main_contents, "link");
EXPECT_DID_SCROLL(true);
}
// Ensure the ForceLoadAtTop policy prevents scrolling to a navigated text
// directive.
IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, TextFragmentAnchorDisabled) {
ASSERT_NO_FATAL_FAILURE(
LoadScrollablePageWithContent("/index.html#:~:text=text"));
WebContents* main_contents = shell()->web_contents();
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
RunUntilInputProcessed(GetWidgetHost());
EXPECT_DID_SCROLL(false);
}
// Tests that text fragments opened after a client redirect are considered as
// coming from an unknown source, even if the redirect is through a known
// search engine URL.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
LinkOpenSourceMetrics_GoogleClientRedirect) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(embedded_test_server()->Start());
GURL first_url(embedded_test_server()->GetURL("google.com", "/empty.html"));
GURL final_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
// This navigtion is simulated as if it came from the omnibox, hence it is
// considered to be user initiated.
EXPECT_TRUE(NavigateToURL(shell(), first_url));
WebContents* main_contents = shell()->web_contents();
TestNavigationObserver observer(main_contents);
EXPECT_EQ(first_url, main_contents->GetLastCommittedURL());
// This navigation occurs without a user gesture, simulating a client
// redirect. However, because the above navigation didn't activate a text
// fragment, permission should be propagated to this navigation.
EXPECT_TRUE(ExecJs(main_contents,
"location.replace('" + final_url.spec() + "');",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
EXPECT_EQ(final_url, main_contents->GetLastCommittedURL());
WaitForPageLoad(main_contents);
RenderFrameSubmissionObserver frame_observer(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
// Bucket 0 is unknown source.
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
// Tests that text fragments opened after a server redirect are considered as
// coming from an unknown source, even if the redirect is through a known
// search engine URL.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest,
LinkOpenSourceMetrics_GoogleServerRedirect) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(embedded_test_server()->Start());
GURL initial_url(embedded_test_server()->GetURL("/simple_page.html"));
GURL redirected_url(embedded_test_server()->GetURL(
"/scrollable_page_with_content.html#:~:text=text"));
GURL redirector_url(embedded_test_server()->GetURL(
"google.com", "/server-redirect?" + redirected_url.spec()));
// This navigtion is simulated as if it came from the omnibox, hence it is
// considered to be user initiated.
EXPECT_TRUE(NavigateToURL(shell(), initial_url));
WebContents* main_contents = shell()->web_contents();
TestNavigationObserver observer(main_contents);
EXPECT_EQ(initial_url, main_contents->GetLastCommittedURL());
// Simulate a user clicking on a link to the redirector url.
EXPECT_TRUE(ExecJs(main_contents,
"var hyperLinkTag = document.createElement('a'); "
"hyperLinkTag.setAttribute('id','fragmentLink'); "
"hyperLinkTag.setAttribute('href','" +
redirector_url.spec() +
"'); document.body.appendChild(hyperLinkTag); "
"hyperLinkTag.appendChild(document.createTextNode('"
"Text Fragment Link.'));"
"document.getElementById('fragmentLink').click();"));
observer.Wait();
EXPECT_EQ(redirected_url, main_contents->GetLastCommittedURL());
RenderFrameSubmissionObserver frame_observer(main_contents);
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
EXPECT_DID_SCROLL(true);
// Bucket 0 is unknown source.
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
} // namespace content
|