blob: 08358163003807e175eb5c8436bf720ac302da65 [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: morlovich@google.com (Maksim Orlovich)
#include "net/instaweb/rewriter/public/critical_selector_filter.h"
#include "net/instaweb/http/public/log_record.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/flush_early.pb.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/hasher.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/timer.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"
#include "pagespeed/opt/logging/enums.pb.h"
namespace net_instaweb {
namespace {
const char kRequestUrl[] = "http://www.example.com/";
class CriticalSelectorFilterTest : public RewriteTestBase {
protected:
virtual void SetUpBeforeSelectorsFilter() {
rewrite_driver()->AddFilters();
}
virtual void SetUpAfterSelectorsFilter() {
server_context()->ComputeSignature(options());
}
virtual void SetUp() {
RewriteTestBase::SetUp();
// Disable flush early for tests (unless they turn it on).
options()->set_enable_flush_early_critical_css(false);
// Enable critical selector filter alone so that
// testing isn't disrupted by beacon injection.
SetUpBeforeSelectorsFilter();
filter_ = new CriticalSelectorFilter(rewrite_driver());
rewrite_driver()->AppendOwnedPreRenderFilter(filter_);
SetUpAfterSelectorsFilter();
// Setup pcache.
pcache_ = rewrite_driver()->server_context()->page_property_cache();
const PropertyCache::Cohort* beacon_cohort =
SetupCohort(pcache_, RewriteDriver::kBeaconCohort);
const PropertyCache::Cohort* dom_cohort =
SetupCohort(pcache_, RewriteDriver::kDomCohort);
server_context()->set_dom_cohort(dom_cohort);
server_context()->set_beacon_cohort(beacon_cohort);
server_context()->set_critical_selector_finder(
new BeaconCriticalSelectorFinder(server_context()->beacon_cohort(),
factory()->nonce_generator(),
statistics()));
ResetDriver();
// Set up initial candidates for critical selector beacon
candidates_.insert("div");
candidates_.insert("*");
candidates_.insert("span");
// Write out some initial critical selectors for us to work with.
StringSet selectors;
selectors.insert("div");
selectors.insert("*");
WriteCriticalSelectorsToPropertyCache(selectors);
// Some weird but valid CSS.
SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
"div,span,*::first-letter { display: block; }"
"p { display: inline; }", 100);
SetResponseWithDefaultHeaders("b.css", kContentTypeCss,
"@media screen,print { * { margin: 0px; } }",
100);
SetResponseWithDefaultHeaders("http://unauthorized.com/unauth.css",
kContentTypeCss,
"div { background-color: blue }"
"random { color: white }", 100);
}
void ResetDriver() {
rewrite_driver()->Clear();
rewrite_driver()->set_request_context(
RequestContext::NewTestRequestContext(factory()->thread_system()));
page_ = NewMockPage(kRequestUrl);
rewrite_driver()->set_property_page(page_);
pcache_->Read(page_);
SetHtmlMimetype(); // Don't wrap scripts in <![CDATA[ ]]>
}
void WriteCriticalSelectorsToPropertyCache(const StringSet& selectors) {
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs);
last_beacon_metadata_ =
server_context()->critical_selector_finder()->
PrepareForBeaconInsertion(candidates_, rewrite_driver());
ASSERT_EQ(kBeaconWithNonce, last_beacon_metadata_.status);
ResetDriver();
server_context()->critical_selector_finder()->
WriteCriticalSelectorsToPropertyCache(
selectors, last_beacon_metadata_.nonce, rewrite_driver());
page_->WriteCohort(server_context()->beacon_cohort());
}
GoogleString WrapForJsLoad(StringPiece orig_css) {
return StrCat("<noscript class=\"psa_add_styles\">", orig_css,
"</noscript>");
}
GoogleString JsLoader() {
return StrCat(
"<script data-pagespeed-no-defer type=\"text/javascript\">",
rewrite_driver()->server_context()->static_asset_manager()->GetAsset(
StaticAssetEnum::CRITICAL_CSS_LOADER_JS,
rewrite_driver()->options()),
"pagespeed.CriticalCssLoader.Run();</script>");
}
GoogleString LoadRestOfCss(StringPiece orig_css) {
return StrCat(WrapForJsLoad(orig_css), JsLoader());
}
GoogleString CssLinkHrefMedia(StringPiece url, StringPiece media) {
return StrCat("<link rel=stylesheet href=", url,
" media=\"", media, "\">");
}
virtual bool AddHtmlTags() const { return false; }
void ValidateRewriterLogging(RewriterHtmlApplication::Status html_status) {
rewrite_driver()->log_record()->WriteLog();
const LoggingInfo& logging_info =
*rewrite_driver()->log_record()->logging_info();
ASSERT_EQ(1, logging_info.rewriter_stats_size());
const RewriterStats& rewriter_stats = logging_info.rewriter_stats(0);
EXPECT_EQ("pr", rewriter_stats.id());
EXPECT_EQ(html_status, rewriter_stats.html_status());
}
CriticalSelectorFilter* filter_; // owned by the driver;
PropertyCache* pcache_;
PropertyPage* page_;
StringSet candidates_;
BeaconMetadata last_beacon_metadata_;
};
TEST_F(CriticalSelectorFilterTest, BasicOperation) {
GoogleString css = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("a.css"),
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>*{display:none}</style>" // from the inline
"<style>div,*::first-letter{display:block}</style>" // from a.css
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"basic", html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
}
TEST_F(CriticalSelectorFilterTest, UnauthorizedCss) {
GoogleString css = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("http://unauthorized.com/unauth.css"),
CssLinkHref("a.css"),
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>*{display:none}</style>" // from the inline
"<link rel=stylesheet href=http://unauthorized.com/unauth.css>"
"<style>div,*::first-letter{display:block}</style>" // from a.css
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"basic", html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
EXPECT_EQ(3, statistics()->GetVariable(
CssSummarizerBase::kNumCssUsedForCriticalCssComputation)->Get());
EXPECT_EQ(1, statistics()->GetVariable(
CssSummarizerBase::kNumCssNotUsedForCriticalCssComputation)->Get());
}
TEST_F(CriticalSelectorFilterTest, AllowUnauthorizedCss) {
options()->ClearSignatureForTesting();
options()->AddInlineUnauthorizedResourceType(semantic_type::kStylesheet);
options()->ComputeSignature();
GoogleString css = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("http://unauthorized.com/unauth.css"),
CssLinkHref("a.css"),
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>*{display:none}</style>" // from the inline
"<style>div{background-color:#00f}</style>" // from unauth.css
"<style>div,*::first-letter{display:block}</style>" // from a.css
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"basic", html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
EXPECT_EQ(4, statistics()->GetVariable(
CssSummarizerBase::kNumCssUsedForCriticalCssComputation)->Get());
EXPECT_EQ(0, statistics()->GetVariable(
CssSummarizerBase::kNumCssNotUsedForCriticalCssComputation)->Get());
}
TEST_F(CriticalSelectorFilterTest, StylesInBody) {
GoogleString css_a = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("a.css"));
GoogleString css_b = CssLinkHref("b.css");
GoogleString critical_css_a =
"<style>*{display:none}</style>" // from the inline
"<style>div,*::first-letter{display:block}</style>"; // from a.css
GoogleString critical_css_b =
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString html = StrCat(
"<head></head><body>", css_a,
"<div>Stuff</div>", css_b,
"</body>");
ValidateExpected(
"style_in_body", html,
StrCat("<head></head><body>", critical_css_a,
"<div>Stuff</div>", critical_css_b,
LoadRestOfCss(css_a + css_b), "</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
}
TEST_F(CriticalSelectorFilterTest, EmptyBlock) {
// Do not insert empty <style> blocks. Our critical selector sets do not
// talk about 'i' so this should do nothing..
GoogleString css = "<style>i { font-style:italic; }</style>";
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"empty_block", html,
StrCat("<head></head>"
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
}
TEST_F(CriticalSelectorFilterTest, DisabledForIE) {
SetCurrentUserAgent(UserAgentMatcherTestBase::kIe7UserAgent);
GoogleString css = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("a.css"),
CssLinkHref("b.css"));
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateNoChanges("on_ie", html);
ValidateRewriterLogging(RewriterHtmlApplication::USER_AGENT_NOT_SUPPORTED);
}
TEST_F(CriticalSelectorFilterTest, NoScript) {
GoogleString css1 =
"<style>*,p {display: none; } span {display: inline; }</style>";
GoogleString css2 =
StrCat("<noscript>", CssLinkHref("a.css"), "</noscript>");
GoogleString css3 =
CssLinkHref("b.css");
GoogleString css = StrCat(css1, css2, css3);
GoogleString critical_css =
"<style>*{display:none}</style>" // from the inline
"<noscript></noscript>" // from a.css
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"noscript", html,
StrCat("<head>", critical_css, "</head>"
"<body><div>Stuff</div>",
WrapForJsLoad(css1),
css2, // noscript, so not marked for JS load.
WrapForJsLoad(css3),
JsLoader(), "</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
}
TEST_F(CriticalSelectorFilterTest, Alternate) {
GoogleString css = StrCat(
"<link rel=\"alternate stylesheet\" href=\"a.css\">",
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"alternate", html,
StrCat("<head>", critical_css, "</head>"
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
}
TEST_F(CriticalSelectorFilterTest, Media) {
GoogleString css = StrCat(
"<style media=screen,print>*,p {display: none; } "
"span {display: inline; }</style>",
CssLinkHrefMedia("a.css", "screen"),
CssLinkHrefMedia("b.css", "screen and (color), aural"));
GoogleString critical_css =
"<style media=\"screen\">"
"*{display:none}" // from the inline
"</style>"
"<style media=\"screen\">"
"div,*::first-letter{display:block}" // from a.css
"</style>"
"<style media=\"screen and (color)\">"
"@media screen{*{margin:0}}" // from b.css
"</style>";
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"foo", html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
}
TEST_F(CriticalSelectorFilterTest, NonScreenMedia) {
GoogleString css = StrCat(
"<style media=print>*,p {display: none; } "
"span {display: inline; }</style>",
CssLinkHrefMedia("a.css", "screen"),
CssLinkHrefMedia("b.css", "screen and (color), aural"));
GoogleString critical_css =
"<style media=\"screen\">"
"div,*::first-letter{display:block}" // from a.css
"</style>"
"<style media=\"screen and (color)\">"
"@media screen{*{margin:0}}" // from b.css
"</style>";
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected(
"non_screen_media", html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div>",
LoadRestOfCss(css), "</body>"));
}
TEST_F(CriticalSelectorFilterTest, SameCssDifferentSelectors) {
// We should not reuse results for same CSS when selectors are different.
GoogleString css = "<style>div,span { display: inline-block; }</style>";
GoogleString critical_css_div = "div{display:inline-block}";
GoogleString critical_css_span = "span{display:inline-block}";
GoogleString critical_css_div_span = "div,span{display:inline-block}";
// Check what we compute for a page with div.
ValidateExpected("with_div", StrCat(css, "<div>Foo</div>"),
StrCat("<style>", critical_css_div, "</style>",
"<div>Foo</div>", LoadRestOfCss(css)));
// Update the selector list with span. Because we are storing the last N
// beacon entries, both div and span should now be in the critical set. We
// also clear the property cache entry for our result, which is needed because
// the test harness is not really keying the pcache by the URL like the real
// system would.
StringSet selectors;
selectors.insert("span");
WriteCriticalSelectorsToPropertyCache(selectors);
// Note that calling ResetDriver() just resets the state in the
// driver. Whatever has been written to the property & metadata caches so far
// will persist. Upon rewriting, the property cache contents will be read and
// the critical selector info in the driver will be repopulated.
ResetDriver();
ValidateExpected("with_div_span", StrCat(css, "<span>Foo</span>"),
StrCat("<style>", critical_css_div_span, "</style>",
"<span>Foo</span>", LoadRestOfCss(css)));
// Now send enough beacons to eliminate support for div; only span should be
// left.
CriticalSelectorFinder* finder = server_context()->critical_selector_finder();
for (int i = 0; i < finder->SupportInterval(); ++i) {
WriteCriticalSelectorsToPropertyCache(selectors);
// We are sending enough beacons with the same selector set here that we
// will enter low frequency beaconing mode, so advance time more to ensure
// rebeaconing actually occurs.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs *
kLowFreqBeaconMult);
}
ResetDriver();
ValidateExpected("with_span", StrCat(css, "<span>Foo</span>"),
StrCat("<style>", critical_css_span, "</style>",
"<span>Foo</span>", LoadRestOfCss(css)));
}
TEST_F(CriticalSelectorFilterTest, RetainPseudoOnly) {
// Make sure we handle things like :hover OK.
GoogleString css = ":hover { border: 2px solid red; }";
SetResponseWithDefaultHeaders("c.css", kContentTypeCss,
css, 100);
ValidateExpected("hover", CssLinkHref("c.css"),
StrCat("<style>:hover{border:2px solid red}</style>",
LoadRestOfCss(CssLinkHref("c.css"))));
}
TEST_F(CriticalSelectorFilterTest, RetainUnparseable) {
// Make sure we keep unparseable fragments around, particularly when
// the problem is with the selector, as well as with the entire region.
GoogleString css = "!huh! {background: white; } @huh { display: block; }";
SetResponseWithDefaultHeaders("c.css", kContentTypeCss,
css, 100);
ValidateExpected(
"partly_unparseable", CssLinkHref("c.css"),
StrCat("<style>!huh! {background:#fff}@huh { display: block; }</style>",
LoadRestOfCss(CssLinkHref("c.css"))));
}
TEST_F(CriticalSelectorFilterTest, NoSelectorInfo) {
// Particular CSS doesn't matter here, just want some.
GoogleString css = "<style>div,span { display: inline-block; }</style>";
// We shouldn't change things when there is no info on selectors available.
page_->DeleteProperty(
server_context()->beacon_cohort(),
CriticalSelectorFinder::kCriticalSelectorsPropertyName);
page_->WriteCohort(server_context()->beacon_cohort());
ResetDriver();
ValidateNoChanges("no_sel_info", StrCat(css, "<div>Foo</div>"));
ValidateNoChanges("no_sel_info", StrCat(css, "<div>Foo</div>"));
ValidateRewriterLogging(RewriterHtmlApplication::PROPERTY_CACHE_MISS);
}
TEST_F(CriticalSelectorFilterTest, ResolveUrlsProperly) {
SetResponseWithDefaultHeaders("dir/c.css", kContentTypeCss,
"* { background-image: url(d.png); }", 100);
ValidateExpected("rel_path", CssLinkHref("dir/c.css"),
StrCat("<style>*{background-image:url(dir/d.png)}</style>",
LoadRestOfCss(CssLinkHref("dir/c.css"))));
}
TEST_F(CriticalSelectorFilterTest, DoNotLazyLoadIfNothingRewritten) {
// Make sure we don't do the whole 'lazy load rest of CSS' schpiel if we
// did not end up changing the main CSS.
SetupWaitFetcher();
ValidateNoChanges("not_loaded", StrCat(CssLinkHref("a.css"),
CssLinkHref("b.css")));
CallFetcherCallbacks();
// Skip ValidateRewriterLogging because fetcher interferes with WriteLog.
}
TEST_F(CriticalSelectorFilterTest, InlineAndAddStyleForFlushingEarly) {
SetResponseWithDefaultHeaders("noscript.css", kContentTypeCss,
"noscript { border: 1px solid red; }", 100);
GoogleString css = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("a.css"),
"<noscript>", CssLinkHref("noscript.css"), "</noscript>",
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>*{display:none}</style>" // from the inline
"<style data-pagespeed-flush-style=\"0\">"
"div,*::first-letter{display:block}</style>" // from a.css
"<noscript></noscript>" // from noscript.css
"<style data-pagespeed-flush-style=\"0\">"
"@media screen{*{margin:0}}</style>"; // from b.css
GoogleString input_html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
options()->ClearSignatureForTesting();
options()->set_enable_flush_early_critical_css(true);
options()->ComputeSignature();
rewrite_driver()->set_flushing_early(true);
// The "flushing_early" driver expects the critical CSS without the
// full CSS at the bottom (no LoadRestOfCss). This allows the flush
// early flow to collect the CSS to flush. The filter output is not
// sent to the end user. (The "flushed_early" driver filters the
// content that is sent to the user.)
ValidateExpected(
"flushing_early", input_html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div></body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
}
TEST_F(CriticalSelectorFilterTest, InlineFlushEarly) {
SetResponseWithDefaultHeaders("noscript.css", kContentTypeCss,
"noscript { border: 1px solid red; }", 100);
GoogleString css_script = StrCat(
"<style>*,p {display: none; } span {display: inline; }</style>",
CssLinkHref("a.css"),
CssLinkHrefMedia("b.css", "print,screen"));
GoogleString css_noscript = StrCat(
"<noscript>", CssLinkHref("noscript.css"), "</noscript>");
GoogleString a_url = "http://test.com/a.css";
GoogleString b_url = "http://test.com/b.css";
GoogleString a_style_id =
rewrite_driver()->server_context()->hasher()->Hash(a_url);
GoogleString b_style_id =
rewrite_driver()->server_context()->hasher()->Hash(b_url);
GoogleString critical_css = StrCat(
"<style>*{display:none}</style>" // from the inline
"<script id=\"psa_flush_style_early\" data-pagespeed-no-defer"
" type=\"text/javascript\">",
CriticalSelectorFilter::kApplyFlushEarlyCss,
"</script>"
"<script data-pagespeed-no-defer type=\"text/javascript\">",
StringPrintf(CriticalSelectorFilter::kInvokeFlushEarlyCssTemplate,
a_style_id.c_str(), "" /* media */),
"</script>" // from a.css
"<script data-pagespeed-no-defer type=\"text/javascript\">",
StringPrintf(CriticalSelectorFilter::kInvokeFlushEarlyCssTemplate,
b_style_id.c_str(), "screen" /* media */),
"</script>" // from b.css
"<noscript></noscript>"); // from noscript.css
GoogleString html = StrCat(
"<head>",
css_script,
css_noscript,
"</head>"
"<body><div>Stuff</div></body>");
options()->ClearSignatureForTesting();
options()->set_enable_flush_early_critical_css(true);
options()->ComputeSignature();
rewrite_driver()->flush_early_info()->set_resource_html(
CssLinkHref("\"" + a_url + "\"") +
CssLinkHref("\"" + b_url + "\""));
rewrite_driver()->set_flushed_early(true);
ValidateExpected(
"flushed_early", html,
StrCat("<head>", critical_css, "</head>",
"<body><div>Stuff</div>",
WrapForJsLoad(css_script),
css_noscript,
JsLoader(),
"</body>"));
ValidateRewriterLogging(RewriterHtmlApplication::ACTIVE);
}
class CriticalSelectorWithRewriteCssFilterTest
: public CriticalSelectorFilterTest {
protected:
virtual void SetUp() {
options()->EnableFilter(RewriteOptions::kRewriteCss);
CriticalSelectorFilterTest::SetUp();
}
};
TEST_F(CriticalSelectorWithRewriteCssFilterTest, ProperlyUsedOptimized) {
// Make sure that the lazy loading code for rest of CSS actually uses
// optimized resources.
GoogleString css = StrCat(
CssLinkHref("a.css"),
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>div,*::first-letter{display:block}</style>" // from a.css
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString optimized_css = StrCat(
CssLinkHref(Encode("", "cf", "0", "a.css", "css")),
CssLinkHref(Encode("", "cf", "0", "b.css", "css")));
GoogleString html = StrCat(
"<head>",
css,
"</head>"
"<body><div>Stuff</div></body>");
ValidateExpected("with_rewrite_css",
html,
StrCat("<head>", critical_css, "</head>"
"<body><div>Stuff</div>",
LoadRestOfCss(optimized_css), "</body>"));
}
class CriticalSelectorWithCombinerFilterTest
: public CriticalSelectorFilterTest {
protected:
virtual void SetUp() {
options()->EnableFilter(RewriteOptions::kCombineCss);
CriticalSelectorFilterTest::SetUp();
}
};
TEST_F(CriticalSelectorWithCombinerFilterTest, Interaction) {
GoogleString css = StrCat(
CssLinkHref("a.css"),
CssLinkHref("b.css"));
// Only one <style> element since combine_css ran before us.
GoogleString critical_css =
"<style>div,*::first-letter{display:block}" // from a.css
"@media screen{*{margin:0}}</style>"; // from b.css
GoogleString combined_url = Encode("", "cc", "0",
MultiUrl("a.css", "b.css"), "css");
ValidateExpected("with_combiner",
css,
StrCat(critical_css,
LoadRestOfCss(CssLinkHref(combined_url))));
}
TEST_F(CriticalSelectorWithCombinerFilterTest, ResolveWhenCombineAcrossPaths) {
// Make sure we get proper URL resolution when doing combine-across-paths.
SetResponseWithDefaultHeaders("dir/a.css", kContentTypeCss,
"* { background-image: url(/dir/d.png); }",
100);
GoogleString css = StrCat(
CssLinkHref("dir/a.css"),
CssLinkHref("b.css"));
// Only one <style> element since combine_css ran before us.
GoogleString critical_css =
"<style>*{background-image:url(dir/d.png)}" // from dir/a.css
"@media screen{*{margin:0}}</style>"; // from b.css
GoogleString combined_url = "dir,_a.css+b.css.pagespeed.cc.0.css";
ValidateExpected("with_combiner_rel",
css,
StrCat(critical_css,
LoadRestOfCss(CssLinkHref(combined_url))));
}
class CriticalSelectorWithInlineCssFilterTest
: public CriticalSelectorFilterTest {
protected:
virtual void SetUpBeforeSelectorsFilter() {}
// Add the inline css filter after the critical selector filter so
// it matches the order that is in RewriteDriver.
virtual void SetUpAfterSelectorsFilter() {
options()->EnableFilter(RewriteOptions::kInlineCss);
rewrite_driver()->AddFilters();
}
};
// When CriticalSelectorFilter and InlineCss are both enabled, only
// CriticalSelectorFilter will be applied.
TEST_F(CriticalSelectorWithInlineCssFilterTest, AvoidTryingToInlineTwice) {
GoogleString css = StrCat(
CssLinkHref("a.css"),
CssLinkHref("b.css"));
GoogleString critical_css =
"<style>div,*::first-letter{display:block}</style>" // from a.css
"<style>@media screen{*{margin:0}}</style>"; // from b.css
GoogleString input_html = StrCat(
"<head>", css, "</head>"
"<body><div>Stuff</div></body>");
GoogleString expected_html = StrCat(
"<head>", critical_css, "</head>"
"<body><div>Stuff</div>", LoadRestOfCss(css), "</body>");
ValidateExpected("with_inline_css", input_html, expected_html);
}
class CriticalSelectorWithBackgroundImageCacheExtensionTest
: public CriticalSelectorFilterTest {
protected:
virtual void SetUp() {
options()->EnableFilter(RewriteOptions::kExtendCacheImages);
options()->EnableFilter(RewriteOptions::kRewriteCss);
CriticalSelectorFilterTest::SetUp();
}
};
// Specific test for a bug where inline style blocks would not be treated
// correctly. Even though they should have been rewritten by rewrite_css,
// the unoptimized <style> block would be copied to the bottom of the page.
TEST_F(CriticalSelectorWithBackgroundImageCacheExtensionTest,
DontMixUnoptAndOptStyles) {
// Triggers calling InlineOutputResource::url().
options()->ClearSignatureForTesting();
options()->set_enable_cache_purge(true);
options()->ComputeSignature();
SetResponseWithDefaultHeaders("background.png", kContentTypePng,
"Dummy contents", 100);
SetResponseWithDefaultHeaders("extern.css", kContentTypeCss,
"* {background: url(background.png)}\n"
".not-critical { margin: 0 }", 100);
GoogleString input_css = StrCat(
" <style>div { background: url(background.png) }"
".not-critical { color: red; }</style>\n"
" ", CssLinkHref("extern.css"), "\n");
GoogleString critical_css = StrCat(
" <style>div{background:url(",
Encode("", "ce", "0", "background.png", "png"), ")}</style>\n"
" <style>*{background:url(",
Encode("", "ce", "0", "background.png", "png"), ")}</style>\n");
// Both inline and external CSS should be rewritten here.
GoogleString rewritten_css = StrCat(
"<style>div{background:url(",
Encode("", "ce", "0", "background.png", "png"), ")}"
".not-critical{color:red}</style>",
CssLinkHref(Encode("", "cf", "0", "extern.css", "css")));
GoogleString input_html = StrCat(
"<head>\n",
input_css,
"</head>\n"
"<body>\n"
" <div>Foo</div>\n"
" <span>Bar</span>\n"
"</body>\n");
GoogleString expected_html = StrCat(
"<head>\n",
critical_css,
"</head>\n"
"<body>\n"
" <div>Foo</div>\n"
" <span>Bar</span>\n",
LoadRestOfCss(rewritten_css),
"</body>\n");
ValidateExpected("dont_mix_unopt_and_opt", input_html, expected_html);
}
} // namespace
} // namespace net_instaweb