blob: ca9a85225004ee642ca16460ec0ebacba4c9dbcc [file] [log] [blame]
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: jmaessen@google.com (Jan-Willem Maessen)
#include "net/instaweb/rewriter/public/critical_css_beacon_filter.h"
#include "net/instaweb/rewriter/public/critical_finder_support_util.h"
#include "net/instaweb/rewriter/public/critical_selector_finder.h"
#include "net/instaweb/rewriter/public/css_summarizer_base.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/static_asset_manager.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/semantic_type.h"
#include "pagespeed/kernel/http/user_agent_matcher_test_base.h"
namespace net_instaweb {
namespace {
const char kInlineStyle[] =
"<style media='not print'>"
"a{color:red}"
"a:visited{color:green}"
"p{color:green}"
"</style>";
const char kStyleA[] =
"div ul:hover>li{color:red}"
":hover{color:red}"
".sec h1#id{color:green}";
const char kStyleB[] =
"a{color:green}"
"@media screen { p:hover{color:red} }"
"@media print { span{color:green} }"
"div ul > li{color:green}";
// The styles above produce the following beacon initialization selector lists.
const char kSelectorsInline[] = "\"a\",\"p\"";
const char kSelectorsInlineWithUnauthSelectors[] = "\"a\",\"div\",\"p\"";
const char kSelectorsA[] = "\".sec h1#id\",\"div ul > li\"";
const char kSelectorsB[] = "\"a\",\"div ul > li\",\"p\"";
const char kSelectorsInlineAB[] = "\".sec h1#id\",\"a\",\"div ul > li\",\"p\"";
// The following styles do not add selectors to the beacon initialization.
const char kInlinePrint[] =
"<style media='print'>"
"span{color:red}"
"</style>";
const char kStyleCorrupt[] =
"span{color:";
const char kStyleEmpty[] =
"/* This has no selectors */";
const char kStyleForUnauthCss[] =
"div{display:inline}";
const char kUnauthDomainUrl[] = "http://unauthorized.com/d.css";
// Common setup / result generation code for all tests
class CriticalCssBeaconFilterTestBase : public RewriteTestBase {
public:
CriticalCssBeaconFilterTestBase() { }
virtual ~CriticalCssBeaconFilterTestBase() { }
protected:
// Set everything up except for filter configuration.
virtual void SetUp() {
RewriteTestBase::SetUp();
SetCurrentUserAgent(
UserAgentMatcherTestBase::kChrome18UserAgent);
SetHtmlMimetype(); // Don't wrap scripts in <![CDATA[ ]]>
factory()->set_use_beacon_results_in_filters(true);
rewrite_driver()->set_property_page(NewMockPage(kTestDomain));
// Set up pcache for page.
const PropertyCache::Cohort* cohort =
SetupCohort(page_property_cache(), RewriteDriver::kBeaconCohort);
server_context()->set_beacon_cohort(cohort);
page_property_cache()->Read(rewrite_driver()->property_page());
// Set up and register a beacon finder.
CriticalSelectorFinder* finder = new BeaconCriticalSelectorFinder(
server_context()->beacon_cohort(), factory()->nonce_generator(),
statistics());
server_context()->set_critical_selector_finder(finder);
// Set up contents of CSS files.
SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
kStyleA, 100);
SetResponseWithDefaultHeaders("b.css", kContentTypeCss,
kStyleB, 100);
SetResponseWithDefaultHeaders("corrupt.css", kContentTypeCss,
kStyleCorrupt, 100);
SetResponseWithDefaultHeaders("empty.css", kContentTypeCss,
kStyleEmpty, 100);
SetResponseWithDefaultHeaders(kUnauthDomainUrl, kContentTypeCss,
kStyleForUnauthCss, 100);
}
// Return a css_filter optimized url.
GoogleString UrlOpt(StringPiece url) {
return Encode("", RewriteOptions::kCssFilterId, "0", url, "css");
}
// Return a link tag with a css_filter optimized url.
GoogleString CssLinkHrefOpt(StringPiece url) {
return CssLinkHref(UrlOpt(url));
}
GoogleString InputHtml(StringPiece head) {
return StrCat("<head>", head, "</head><body><p>content</p></body>");
}
GoogleString BeaconHtml(StringPiece head, StringPiece selectors) {
GoogleString html = StrCat(
"<head>", head, "</head><body><p>content</p>"
"<script data-pagespeed-no-defer type=\"text/javascript\">",
server_context()->static_asset_manager()->GetAsset(
StaticAssetEnum::CRITICAL_CSS_BEACON_JS, options()),
"pagespeed.selectors=[", selectors, "];");
StrAppend(&html,
"pagespeed.criticalCssBeaconInit('",
options()->beacon_url().http, "','", kTestDomain,
"','0','", ExpectedNonce(), "',pagespeed.selectors);"
"</script></body>");
return html;
}
GoogleString SelectorsOnlyHtml(StringPiece head, StringPiece selectors) {
return StrCat(
"<head>", head, "</head><body><p>content</p>"
"<script data-pagespeed-no-defer type=\"text/javascript\">",
CriticalCssBeaconFilter::kInitializePageSpeedJs,
"pagespeed.selectors=[", selectors, "];"
"</script></body>");
}
private:
DISALLOW_COPY_AND_ASSIGN(CriticalCssBeaconFilterTestBase);
};
// Standard test setup enables filter via RewriteOptions.
class CriticalCssBeaconFilterTest : public CriticalCssBeaconFilterTestBase {
public:
CriticalCssBeaconFilterTest() { }
virtual ~CriticalCssBeaconFilterTest() { }
protected:
virtual void SetUp() {
CriticalCssBeaconFilterTestBase::SetUp();
options()->EnableFilter(RewriteOptions::kPrioritizeCriticalCss);
rewrite_driver()->AddFilters();
}
private:
DISALLOW_COPY_AND_ASSIGN(CriticalCssBeaconFilterTest);
};
TEST_F(CriticalCssBeaconFilterTest, ExtractFromInlineStyle) {
ValidateExpectedUrl(
kTestDomain,
InputHtml(kInlineStyle),
BeaconHtml(kInlineStyle, kSelectorsInline));
}
TEST_F(CriticalCssBeaconFilterTest, DisabledForIE) {
SetCurrentUserAgent(UserAgentMatcherTestBase::kIe7UserAgent);
ValidateNoChanges(kTestDomain, InputHtml(kInlineStyle));
}
TEST_F(CriticalCssBeaconFilterTest, DisabledForBots) {
SetCurrentUserAgent(UserAgentMatcherTestBase::kGooglebotUserAgent);
ValidateNoChanges(kTestDomain, InputHtml(kInlineStyle));
}
TEST_F(CriticalCssBeaconFilterTest, ExtractFromUnopt) {
ValidateExpectedUrl(
kTestDomain,
InputHtml(CssLinkHref("a.css")),
BeaconHtml(CssLinkHref("a.css"), kSelectorsA));
}
TEST_F(CriticalCssBeaconFilterTest, ExtractFromOpt) {
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("b.css"), kInlineStyle));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHrefOpt("b.css"), kInlineStyle), kSelectorsB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, DontExtractFromNoScript) {
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"),
"<noscript>", CssLinkHref("b.css"), "</noscript>"));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHref("a.css"),
"<noscript>", CssLinkHrefOpt("b.css"), "</noscript>"),
kSelectorsA);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, DontExtractFromAlternate) {
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"),
"<link rel=\"alternate stylesheet\" href=b.css>"));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHref("a.css"),
"<link rel=\"alternate stylesheet\" href=", UrlOpt("b.css"), ">"),
kSelectorsA);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, Unauthorized) {
GoogleString css = StrCat(CssLinkHref(kUnauthDomainUrl), kInlineStyle);
ValidateExpectedUrl(
kTestDomain, InputHtml(css), BeaconHtml(css, kSelectorsInline));
EXPECT_EQ(1, statistics()->GetVariable(
CssSummarizerBase::kNumCssUsedForCriticalCssComputation)->Get());
EXPECT_EQ(1, statistics()->GetVariable(
CssSummarizerBase::kNumCssNotUsedForCriticalCssComputation)->Get());
}
TEST_F(CriticalCssBeaconFilterTest, AllowUnauthorized) {
options()->ClearSignatureForTesting();
options()->AddInlineUnauthorizedResourceType(semantic_type::kStylesheet);
options()->ComputeSignature();
GoogleString css = StrCat(CssLinkHref(kUnauthDomainUrl), kInlineStyle);
ValidateExpectedUrl(
kTestDomain, InputHtml(css),
BeaconHtml(css, kSelectorsInlineWithUnauthSelectors));
EXPECT_EQ(2, statistics()->GetVariable(
CssSummarizerBase::kNumCssUsedForCriticalCssComputation)->Get());
EXPECT_EQ(0, statistics()->GetVariable(
CssSummarizerBase::kNumCssNotUsedForCriticalCssComputation)->Get());
}
TEST_F(CriticalCssBeaconFilterTest, Missing) {
SetFetchFailOnUnexpected(false);
GoogleString css = StrCat(CssLinkHref("404.css"), kInlineStyle);
ValidateExpectedUrl(
kTestDomain, InputHtml(css), BeaconHtml(css, kSelectorsInline));
}
TEST_F(CriticalCssBeaconFilterTest, Corrupt) {
GoogleString css = StrCat(CssLinkHref("corrupt.css"), kInlineStyle);
ValidateExpectedUrl(
kTestDomain, InputHtml(css), BeaconHtml(css, kSelectorsInline));
}
TEST_F(CriticalCssBeaconFilterTest, EmptyCssIgnored) {
// This failed when SummariesDone called SplitStringPieceToVector()
// with "omit_empty_strings" set to false. The beacon selector list
// looked like the following: [,".sec h1#id","a","div ul > li","p"].
// That caused the beacon JavaScript to take the length of 'undefined'.
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHref("empty.css")));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHrefOpt("empty.css")),
kSelectorsInlineAB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, EmptyCssDoesNotTriggerBeaconCode) {
GoogleString input_html = InputHtml(CssLinkHref("empty.css"));
GoogleString expected_html = InputHtml(CssLinkHrefOpt("empty.css"));
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, NonScreenMediaInline) {
ValidateNoChanges("non-screen-inline", InputHtml(kInlinePrint));
}
TEST_F(CriticalCssBeaconFilterTest, NonScreenMediaExternal) {
ValidateNoChanges(
"non-screen-external",
InputHtml("<link rel=stylesheet href='a.css' media='print'>"));
}
TEST_F(CriticalCssBeaconFilterTest, MixOfGoodAndBad) {
// Make sure we don't see any strange interactions / missed connections.
SetFetchFailOnUnexpected(false);
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"), CssLinkHref("404.css"), kInlineStyle,
CssLinkHref(kUnauthDomainUrl), CssLinkHref("corrupt.css"),
kInlinePrint, CssLinkHref("b.css")));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHref("a.css"), CssLinkHref("404.css"), kInlineStyle,
CssLinkHref(kUnauthDomainUrl), CssLinkHref("corrupt.css"),
kInlinePrint, CssLinkHrefOpt("b.css")),
kSelectorsInlineAB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, EverythingThatParses) {
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHref("b.css")));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHrefOpt("b.css")),
kSelectorsInlineAB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
TEST_F(CriticalCssBeaconFilterTest, FalseBeaconResultsGivesEmptyBeaconUrl) {
factory()->set_use_beacon_results_in_filters(false);
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHref("b.css")));
GoogleString expected_html = SelectorsOnlyHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHrefOpt("b.css")),
kSelectorsInlineAB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
// This class explicitly only includes the beacon filter and its prerequisites;
// this lets us test the presence of beacon results without the critical
// selector filter injecting a lot of stuff in the output.
class CriticalCssBeaconOnlyTest : public CriticalCssBeaconFilterTestBase {
public:
CriticalCssBeaconOnlyTest() {}
virtual ~CriticalCssBeaconOnlyTest() {}
protected:
virtual void SetUp() {
CriticalCssBeaconFilterTestBase::SetUp();
// Need to set up filters that are normally auto-enabled by
// kPrioritizeCriticalCss: we're switching on CriticalCssBeaconFilter by
// hand so that we don't turn on CriticalSelectorFilter.
options()->EnableFilter(RewriteOptions::kRewriteCss);
options()->EnableFilter(RewriteOptions::kFlattenCssImports);
options()->EnableFilter(RewriteOptions::kInlineImportToLink);
CriticalCssBeaconFilter::InitStats(statistics());
filter_ = new CriticalCssBeaconFilter(rewrite_driver());
rewrite_driver()->AddFilters();
rewrite_driver()->AppendOwnedPreRenderFilter(filter_);
}
private:
CriticalCssBeaconFilter* filter_; // Owned by rewrite_driver()
DISALLOW_COPY_AND_ASSIGN(CriticalCssBeaconOnlyTest);
};
// Make sure we re-beacon if candidate data changes.
TEST_F(CriticalCssBeaconOnlyTest, ExtantPCache) {
// Inject pcache entry.
StringSet selectors;
selectors.insert("div ul > li");
selectors.insert("p");
selectors.insert("span"); // Doesn't occur in our CSS
CriticalSelectorFinder* finder = server_context()->critical_selector_finder();
RewriteDriver* driver = rewrite_driver();
BeaconMetadata metadata =
finder->PrepareForBeaconInsertion(selectors, driver);
ASSERT_TRUE(metadata.status == kBeaconWithNonce);
EXPECT_EQ(ExpectedNonce(), metadata.nonce);
finder->WriteCriticalSelectorsToPropertyCache(
selectors, metadata.nonce, driver);
// Force cohort to persist.
rewrite_driver()->property_page()
->WriteCohort(server_context()->beacon_cohort());
// Now do the test.
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHref("b.css")));
GoogleString expected_html = BeaconHtml(
StrCat(CssLinkHref("a.css"), kInlineStyle, CssLinkHrefOpt("b.css")),
kSelectorsInlineAB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
class CriticalCssBeaconWithCombinerFilterTest
: public CriticalCssBeaconFilterTest {
public:
virtual void SetUp() {
options()->EnableFilter(RewriteOptions::kCombineCss);
CriticalCssBeaconFilterTest::SetUp();
}
};
TEST_F(CriticalCssBeaconWithCombinerFilterTest, Interaction) {
// Make sure that beacon insertion interacts with combine CSS properly.
GoogleString input_html = InputHtml(
StrCat(CssLinkHref("a.css"), CssLinkHref("b.css")));
GoogleString combined_url = Encode("", RewriteOptions::kCssCombinerId, "0",
MultiUrl("a.css", "b.css"), "css");
// css_filter applies after css_combine and adds to the url encoding.
GoogleString expected_url = UrlOpt(combined_url);
GoogleString expected_html = BeaconHtml(
CssLinkHref(expected_url), kSelectorsInlineAB);
ValidateExpectedUrl(kTestDomain, input_html, expected_html);
}
} // namespace
} // namespace net_instaweb