blob: b59281634e93f9a5b1b60bb0ea1e6395f7fc1011 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/html/parser/html_document_parser.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support_with_mock_scheduler.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
namespace blink {
class HTMLDocumentParserSimTest : public SimTest {
protected:
HTMLDocumentParserSimTest() {
ResetDiscardedTokenCountForTesting();
Document::SetThreadedParsingEnabledForTesting(true);
}
};
class HTMLDocumentParserLoadingTest
: public HTMLDocumentParserSimTest,
public testing::WithParamInterface<ParserSynchronizationPolicy> {
protected:
HTMLDocumentParserLoadingTest() {
if (GetParam() == ParserSynchronizationPolicy::kForceSynchronousParsing) {
Document::SetThreadedParsingEnabledForTesting(false);
} else {
Document::SetThreadedParsingEnabledForTesting(true);
}
if (GetParam() == ParserSynchronizationPolicy::kAllowDeferredParsing) {
RuntimeEnabledFeatures::SetForceSynchronousHTMLParsingEnabled(true);
} else {
RuntimeEnabledFeatures::SetForceSynchronousHTMLParsingEnabled(false);
}
platform_->SetAutoAdvanceNowToPendingTasks(false);
}
static bool SheetInHeadBlocksParser() {
return RuntimeEnabledFeatures::BlockHTMLParserOnStyleSheetsEnabled();
}
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform_;
};
INSTANTIATE_TEST_SUITE_P(HTMLDocumentParserLoadingTest,
HTMLDocumentParserLoadingTest,
testing::Values(kAllowDeferredParsing,
kAllowAsynchronousParsing,
kForceSynchronousParsing));
TEST_P(HTMLDocumentParserLoadingTest,
PrefetchedDeferScriptDoesNotDeadlockParser) {
// Maximum size string chunk to feed to the parser.
constexpr unsigned kPumpSize = 2048;
// <div>hello</div> is conveniently 16 chars in length.
constexpr int kInitialDivCount = 1.5 * kPumpSize / 16;
SimRequest::Params params;
params.response_http_status = 200;
SimRequest main_resource("https://example.com/test.html", "text/html");
SimRequest deferred_js("https://example.com/deferred-script.js",
"application/javascript", params);
SimRequest sync_js("https://example.com/sync-script.js",
"application/javascript", params);
LoadURL("https://example.com/test.html");
// Building a big HTML document that the parser cannot handle in one go.
// The idea is that we do
// PumpTokenizer PumpTokenizer Insert PumpTokenizer PumpTokenizer ...
// But _without_ calling Append, to replicate the deadlock situation
// encountered in crbug.com/1132508. First, build some problematic input in a
// StringBuilder.
WTF::StringBuilder sb;
sb.Append("<html>");
sb.Append(R"HTML(
<head>
<meta charset="utf-8">
<!-- Preload deferred-script.js so that a Client ends up backing
the deferred_js SimRequest. -->
<link rel="preload" href="deferred-script.js" as="script">
</head><body>
)HTML");
for (int i = 0; i < kInitialDivCount; i++) {
// Add a large blob of HTML to the parser to give it something to work with.
// Must cross the first and second Append calls.
sb.Append("<div>hello</div>");
}
// Next inject a synchronous, parser-blocking script and a div
// for the defer script to work with.
sb.Append(R"HTML(
<script src="sync-script.js"></script>
<div id="internalDiv"></div>
)HTML");
unsigned script_end = sb.length();
for (int i = 0; i < kInitialDivCount; i++) {
// Stress the parser more by requiring nested tokenization pumps.
sb.Append("<script>document.write('hello');</script>");
}
// At the end of the document, add the deferred script.
// When this runs, it'll add a worldDiv into the internalDiv created above.
sb.Append(R"HTML(
<script src="deferred-script.js" defer></script>
)HTML");
// Next, chop up the StringBuilder into realistic chunks.
String s = sb.ToString();
int testing_phase = 0;
ASSERT_GT(s.length(), 1u);
for (unsigned i = 0; i < s.length(); i += kPumpSize) {
unsigned extent = kPumpSize - 1;
if (i + extent > (s.length()) - 1) {
extent = s.length() - 1 - i;
ASSERT_LT(extent, kPumpSize);
}
String chunk(s.Characters8() + i, extent);
main_resource.Write(chunk);
if (i >= script_end) {
// Simulate the deferred script arriving before the parser-blocking one.
if (testing_phase == 1) {
deferred_js.Complete(R"JS(
document.getElementById("internalDiv").innerHTML = "<div id='worldDiv'>hi</div>";
)JS");
}
testing_phase++;
platform_->RunUntilIdle();
}
}
// Everything's now Append()'d. Complete the main resource.
ASSERT_GT(testing_phase, 2);
main_resource.Complete();
platform_->RunUntilIdle(); // Parse up until the parser blocking script.
// Complete the parser blocking script.
sync_js.Complete(R"JS(
document.write("<div id='helloDiv'></div>");
)JS");
// Resume execution up until the parser-blocking script at the end.
platform_->RunUntilIdle();
// Expect both the element generated by the parser blocking script
// and the element created by the deferred script to be present.
EXPECT_TRUE(GetDocument().getElementById("helloDiv"));
EXPECT_TRUE(GetDocument().getElementById("worldDiv"));
}
TEST_P(HTMLDocumentParserLoadingTest, IFrameDoesNotRenterParser) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimRequest::Params params;
params.response_http_status = 200;
SimSubresourceRequest js("https://example.com/non-existent.js",
"application/javascript", params);
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<script src="non-existent.js"></script>
<iframe onload="document.write('This test passes if it does not crash'); document.close();"></iframe>
)HTML");
platform_->RunUntilIdle();
js.Complete("");
platform_->RunUntilIdle();
}
TEST_P(HTMLDocumentParserLoadingTest,
PauseParsingForExternalStylesheetsInHead) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="bodyDiv"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(),
!GetDocument().getElementById("bodyDiv"));
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("bodyDiv"));
}
TEST_P(HTMLDocumentParserLoadingTest,
BlockingParsingForExternalStylesheetsImportedInHead) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<style>
@import 'testHead.css'
</style>
</head><body>
<div id="bodyDiv"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(),
!GetDocument().getElementById("bodyDiv"));
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("bodyDiv"));
}
TEST_P(HTMLDocumentParserLoadingTest,
ShouldPauseParsingForExternalStylesheetsInBody) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
SimSubresourceRequest css_body_resource("https://example.com/testBody.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<link rel=stylesheet href=testBody.css>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after"));
// Completing the head css should progress parsing past #before.
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after"));
// Completing the body resource and pumping the tasks should continue parsing
// and create the "after" div.
css_body_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
}
TEST_P(HTMLDocumentParserLoadingTest,
ShouldPauseParsingForExternalStylesheetsInBodyIncremental) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
SimSubresourceRequest css_body_resource1("https://example.com/testBody1.css",
"text/css");
SimSubresourceRequest css_body_resource2("https://example.com/testBody2.css",
"text/css");
SimSubresourceRequest css_body_resource3("https://example.com/testBody3.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Write(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<link rel=stylesheet href=testBody1.css>
<div id="after1"></div>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after1"));
EXPECT_FALSE(GetDocument().getElementById("after2"));
EXPECT_FALSE(GetDocument().getElementById("after3"));
main_resource.Write(
"<link rel=stylesheet href=testBody2.css>"
"<div id=\"after2\"></div>");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after1"));
EXPECT_FALSE(GetDocument().getElementById("after2"));
EXPECT_FALSE(GetDocument().getElementById("after3"));
main_resource.Complete(R"HTML(
<link rel=stylesheet href=testBody3.css>
<div id="after3"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after1"));
EXPECT_FALSE(GetDocument().getElementById("after2"));
EXPECT_FALSE(GetDocument().getElementById("after3"));
// Completing the head css should progress parsing past #before.
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after1"));
EXPECT_FALSE(GetDocument().getElementById("after2"));
EXPECT_FALSE(GetDocument().getElementById("after3"));
// Completing the second css shouldn't change anything
css_body_resource2.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after1"));
EXPECT_FALSE(GetDocument().getElementById("after2"));
EXPECT_FALSE(GetDocument().getElementById("after3"));
// Completing the first css should allow the parser to continue past it and
// the second css which was already completed and then pause again before the
// third css.
css_body_resource1.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after1"));
EXPECT_TRUE(GetDocument().getElementById("after2"));
EXPECT_FALSE(GetDocument().getElementById("after3"));
// Completing the third css should let it continue to the end.
css_body_resource3.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after1"));
EXPECT_TRUE(GetDocument().getElementById("after2"));
EXPECT_TRUE(GetDocument().getElementById("after3"));
}
TEST_P(HTMLDocumentParserLoadingTest,
ShouldNotPauseParsingForExternalNonMatchingStylesheetsInBody) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<link rel=stylesheet href=testBody.css type='print'>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("after"));
// Completing the head css should progress parsing past both #before and
// #after.
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
}
TEST_P(HTMLDocumentParserLoadingTest,
ShouldPauseParsingForExternalStylesheetsImportedInBody) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
SimSubresourceRequest css_body_resource("https://example.com/testBody.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<style>
@import 'testBody.css'
</style>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after"));
// Completing the head css should progress parsing past #before.
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after"));
// Completing the body resource and pumping the tasks should continue parsing
// and create the "after" div.
css_body_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
}
TEST_P(HTMLDocumentParserLoadingTest,
ShouldPauseParsingForExternalStylesheetsWrittenInBody) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
SimSubresourceRequest css_body_resource("https://example.com/testBody.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<script>
document.write('<link rel=stylesheet href=testBody.css>');
</script>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after"));
// Completing the head css should progress parsing past #before.
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_FALSE(GetDocument().getElementById("after"));
// Completing the body resource and pumping the tasks should continue parsing
// and create the "after" div.
css_body_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
}
TEST_P(HTMLDocumentParserLoadingTest,
PendingHeadStylesheetBlockingParserForBodyInlineStyle) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<style>
</style>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("after"));
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
}
TEST_P(HTMLDocumentParserLoadingTest,
PendingHeadStylesheetBlockingParserForBodyShadowDom) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_head_resource("https://example.com/testHead.css",
"text/css");
LoadURL("https://example.com/test.html");
// The marquee tag has a shadow DOM that synchronously applies a stylesheet.
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
<link rel=stylesheet href=testHead.css>
</head><body>
<div id="before"></div>
<marquee>Marquee</marquee>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("before"));
EXPECT_EQ(SheetInHeadBlocksParser(), !GetDocument().getElementById("after"));
css_head_resource.Complete("");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
}
TEST_P(HTMLDocumentParserLoadingTest,
ShouldNotPauseParsingForExternalStylesheetsAttachedInBody) {
SimRequest main_resource("https://example.com/test.html", "text/html");
SimSubresourceRequest css_async_resource("https://example.com/testAsync.css",
"text/css");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><head>
</head><body>
<div id="before"></div>
<script>
var attach = document.getElementsByTagName('script')[0];
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'testAsync.css';
link.media = 'all';
attach.appendChild(link);
</script>
<div id="after"></div>
</body></html>
)HTML");
platform_->RunUntilIdle();
EXPECT_TRUE(GetDocument().getElementById("before"));
EXPECT_TRUE(GetDocument().getElementById("after"));
css_async_resource.Complete("");
platform_->RunUntilIdle();
}
TEST_F(HTMLDocumentParserSimTest, NoRewindNoDocWrite) {
SimRequest main_resource("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html><body>no doc write
</body></html>
)HTML");
test::RunPendingTasks();
EXPECT_EQ(0U, GetDiscardedTokenCountForTesting());
}
TEST_F(HTMLDocumentParserSimTest, RewindBrokenToken) {
SimRequest main_resource("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<script>
document.write('<a');
</script>
)HTML");
test::RunPendingTasks();
EXPECT_EQ(2U, GetDiscardedTokenCountForTesting());
}
TEST_F(HTMLDocumentParserSimTest, RewindDifferentNamespace) {
SimRequest main_resource("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<script>
document.write('<svg>');
</script>
)HTML");
test::RunPendingTasks();
EXPECT_EQ(2U, GetDiscardedTokenCountForTesting());
}
TEST_F(HTMLDocumentParserSimTest, NoRewindSaneDocWrite1) {
SimRequest main_resource("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
main_resource.Complete(
"<!DOCTYPE html>"
"<script>"
"document.write('<script>console.log(\'hello world\');<\\/script>');"
"</script>");
test::RunPendingTasks();
EXPECT_EQ(0U, GetDiscardedTokenCountForTesting());
}
TEST_F(HTMLDocumentParserSimTest, NoRewindSaneDocWrite2) {
SimRequest main_resource("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<script>
document.write('<p>hello world<\\/p><a>yo');
</script>
)HTML");
test::RunPendingTasks();
EXPECT_EQ(0U, GetDiscardedTokenCountForTesting());
}
TEST_F(HTMLDocumentParserSimTest, NoRewindSaneDocWriteWithTitle) {
SimRequest main_resource("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<html>
<head>
<title></title>
<script>document.write('<p>testing');</script>
</head>
<body>
</body>
</html>
)HTML");
test::RunPendingTasks();
EXPECT_EQ(0U, GetDiscardedTokenCountForTesting());
}
} // namespace blink