blob: 35f3640a7d189d204e5588745a28ed37484e77af [file] [log] [blame]
/*
* Copyright 2010 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: mdsteele@google.com (Matthew D. Steele)
#include "net/instaweb/rewriter/public/cache_extender.h"
#include "net/instaweb/rewriter/public/css_inline_filter.h"
#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_filter.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 "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/charset_util.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/semantic_type.h"
namespace net_instaweb {
namespace {
class CssInlineFilterTest : public RewriteTestBase {
protected:
CssInlineFilterTest() : filters_added_(false) {}
void TestInlineCssWithOutputUrl(
const GoogleString& html_url,
const GoogleString& head_extras,
const GoogleString& css_url,
const GoogleString& css_out_url,
const GoogleString& other_attrs,
const GoogleString& css_original_body,
bool expect_inline,
const GoogleString& css_rewritten_body,
const GoogleString& debug_string) {
if (!filters_added_) {
AddFilter(RewriteOptions::kInlineCss);
filters_added_ = true;
}
GoogleString html_template = StrCat(
"<head>\n",
head_extras,
" <link rel=\"stylesheet\" href=\"%s\"",
(other_attrs.empty() ? "" : " " + other_attrs) + ">",
"%s\n</head>\n"
"<body>Hello, world!</body>\n");
const GoogleString html_input =
StringPrintf(html_template.c_str(), css_url.c_str(), "");
const GoogleString outline_html_output =
StringPrintf(html_template.c_str(), css_out_url.c_str(), "");
const GoogleString outline_debug_html_output = debug_string.empty()
? outline_html_output
: StringPrintf(html_template.c_str(), css_out_url.c_str(),
StrCat("<!--", debug_string, "-->").c_str());
// Put original CSS file into our fetcher.
ResponseHeaders default_css_header;
SetDefaultLongCacheHeaders(&kContentTypeCss, &default_css_header);
SetFetchResponse(css_url, default_css_header, css_original_body);
// Rewrite the HTML page.
ParseUrl(html_url, html_input);
const GoogleString expected_output =
(!expect_inline ? outline_html_output :
StrCat("<head>\n",
head_extras,
StrCat(" <style",
(other_attrs.empty() ? "" : " " + other_attrs),
">"),
css_rewritten_body, "</style>\n"
"</head>\n"
"<body>Hello, world!</body>\n"));
EXPECT_EQ(AddHtmlBody(expected_output), output_buffer_);
if (!expect_inline) {
output_buffer_.clear();
TurnOnDebug();
ParseUrl(html_url, html_input);
EXPECT_EQ(AddHtmlBody(outline_debug_html_output), output_buffer_);
}
}
void TestInlineCss(const GoogleString& html_url,
const GoogleString& css_url,
const GoogleString& other_attrs,
const GoogleString& css_original_body,
bool expect_inline,
const GoogleString& css_rewritten_body) {
TestInlineCssWithOutputUrl(
html_url, "", css_url, css_url, other_attrs, css_original_body,
expect_inline, css_rewritten_body, "");
}
void TestNoInlineCss(const GoogleString& html_url,
const GoogleString& css_url,
const GoogleString& other_attrs,
const GoogleString& css_original_body,
const GoogleString& css_rewritten_body,
const GoogleString& debug_string) {
TestInlineCssWithOutputUrl(
html_url, "", css_url, css_url, other_attrs, css_original_body,
false, css_rewritten_body, debug_string);
}
void VerifyNoInliningForClosingStyleTag(
const GoogleString& closing_style_tag) {
AddFilter(RewriteOptions::kInlineCss);
SetResponseWithDefaultHeaders("foo.css", kContentTypeCss,
StrCat("a{margin:0}", closing_style_tag),
100);
// We don't mess with links that contain a closing style tag.
ValidateNoChanges("no_inlining_of_close_style_tag",
"<link rel='stylesheet' href='foo.css'>");
TurnOnDebug();
ValidateExpected("no_inlining_of_close_style_tag+debug",
"<link rel='stylesheet' href='foo.css'>",
"<link rel='stylesheet' href='foo.css'>"
"<!--CSS not inlined since it contains "
"style closing tag-->");
}
void TurnOnDebug() {
options()->ClearSignatureForTesting();
options()->EnableFilter(RewriteOptions::kDebug);
server_context()->ComputeSignature(options());
}
private:
bool filters_added_;
};
TEST_F(CssInlineFilterTest, InlineCssSimple) {
const GoogleString css = "BODY { color: red; }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"", css, true, css);
}
class CssInlineFilterTestCustomOptions : public CssInlineFilterTest {
protected:
// Derived classes should add their options and then call
// CssInlineFilterTest::SetUp().
virtual void SetUp() {}
};
TEST_F(CssInlineFilterTest, InlineCssUnhealthy) {
lru_cache()->set_is_healthy(false);
const GoogleString css = "BODY { color: red; }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"", css, false, css);
}
TEST_F(CssInlineFilterTest, InlineCss404) {
// Test to make sure that a missing input is handled well.
SetFetchResponse404("404.css");
ValidateNoChanges("404", "<link rel=stylesheet href='404.css'>");
// Second time, to make sure caching doesn't break it.
ValidateNoChanges("404", "<link rel=stylesheet href='404.css'>");
}
TEST_F(CssInlineFilterTest, InlineCssCached) {
// Doing it twice should be safe, too.
const GoogleString css = "BODY { color: red; }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"", css, true, css);
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"", css, true, css);
}
TEST_F(CssInlineFilterTest, InlineCssRewriteUrls1) {
// CSS with a relative URL that needs to be changed:
const GoogleString css1 =
"BODY { background-image: url('bg.png'); }\n";
const GoogleString css2 =
"BODY { background-image: url('foo/bar/bg.png'); }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/foo/bar/baz.css",
"", css1, true, css2);
}
TEST_F(CssInlineFilterTest, InlineCssRewriteUrls2) {
// CSS with a relative URL, this time with ".." in it:
const GoogleString css1 =
"BODY { background-image: url('../quux/bg.png'); }\n";
const GoogleString css2 =
"BODY { background-image: url('foo/quux/bg.png'); }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/foo/bar/baz.css",
"", css1, true, css2);
}
TEST_F(CssInlineFilterTest, NoRewriteUrlsSameDir) {
const GoogleString css = "BODY { background-image: url('bg.png'); }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/baz.css",
"", css, true, css);
}
TEST_F(CssInlineFilterTest, ShardSubresources) {
UseMd5Hasher();
DomainLawyer* lawyer = options()->WriteableDomainLawyer();
lawyer->AddShard("www.example.com", "shard1.com,shard2.com",
&message_handler_);
const GoogleString css_in =
".p1 { background-image: url('b1.png'); }"
".p2 { background-image: url('b2.png'); }";
const GoogleString css_out =
".p1 { background-image: url('http://shard2.com/b1.png'); }"
".p2 { background-image: url('http://shard1.com/b2.png'); }";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/baz.css",
"", css_in, true, css_out);
}
TEST_F(CssInlineFilterTest, DoNotInlineCssWithMediaNotScreen) {
const GoogleString css = "BODY { color: red; }\n";
TestNoInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"media=\"print\"", css, "",
"CSS not inlined because media does not match screen");
}
TEST_F(CssInlineFilterTest, DoInlineCssWithMediaAll) {
const GoogleString css = "BODY { color: red; }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"media=\"all\"", css, true, css);
}
TEST_F(CssInlineFilterTest, DoInlineCssWithMediaScreen) {
const GoogleString css = "BODY { color: red; }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"media=\"print, audio ,, ,sCrEeN \"", css, true, css);
}
TEST_F(CssInlineFilterTest, DoInlineCssWithMediaQuery) {
// Media queries are tested more exhaustively in css_tag_scanner_test.
const GoogleString css = "BODY { color: red; }\n";
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"media=\"only (color)\"", css, true, css);
}
TEST_F(CssInlineFilterTest, Empty) {
// Don't inline empty resources. This is defensive programming against
// issues like: https://github.com/pagespeed/mod_pagespeed/issues/1050
const GoogleString css = "";
TestNoInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
"", css, "",
"Resource is empty, preventing rewriting of "
"http://www.example.com/styles.css");
}
TEST_F(CssInlineFilterTest, InlineCssWithInvalidMedia) {
// Use an invalid media tag, but one that's still decipherable.
// Trying to deal with indecipherable media tags turned out to be
// more trouble than it's worth.
const char kNotValid[] = "not!?#?;valid";
const GoogleString css = "BODY { color: red; }\n";
GoogleString media;
// Now do the actual test that we don't inline the CSS with an invalid
// media type (and not screen or all as well).
media = StrCat("media=\"", kNotValid, "\"");
TestNoInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
media, css, "",
"CSS not inlined because media does not match screen");
// And now test that we DO inline the CSS with an invalid media type
// if there's also an instance of "screen" in the media attribute.
media = StrCat("media=\"", kNotValid, ",screen\"");
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css",
media, css, true, css);
}
TEST_F(CssInlineFilterTest, DoNotInlineCssTooBig) {
// CSS too large to inline:
const int64 length = 2 * RewriteOptions::kDefaultCssInlineMaxBytes;
TestNoInlineCss("http://www.example.com/index.html",
"http://www.example.com/styles.css", "",
("BODY { background-image: url('" +
GoogleString(length, 'z') + ".png'); }\n"),
"", "CSS not inlined since it&#39;s bigger than 2048 bytes");
}
TEST_F(CssInlineFilterTest, DoInlineCssDifferentDomain) {
const GoogleString css = "BODY { color: red; }\n";
options()->AddInlineUnauthorizedResourceType(semantic_type::kStylesheet);
TestInlineCss("http://www.example.com/index.html",
"http://unauth.com/styles.css",
"", css, true, css);
EXPECT_EQ(1,
statistics()->GetVariable(CssInlineFilter::kNumCssInlined)->Get());
}
TEST_F(CssInlineFilterTest, DoNotInlineCssDifferentDomain) {
// Note: This only fails because we haven't authorized unauth.com
GoogleUrl gurl("http://unauth.com/styles.css");
TestNoInlineCss("http://www.example.com/index.html", gurl.Spec().as_string(),
"", "BODY { color: red; }\n", "",
RewriteDriver::GenerateUnauthorizedDomainDebugComment(gurl));
EXPECT_EQ(0,
statistics()->GetVariable(CssInlineFilter::kNumCssInlined)->Get());
}
TEST_F(CssInlineFilterTest, CorrectlyInlineCssWithImports) {
TestInlineCss("http://www.example.com/index.html",
"http://www.example.com/dir/styles.css", "",
"@import \"foo.css\"; BODY { color: red; }\n", true,
"@import \"dir/foo.css\"; BODY { color: red; }\n");
}
// http://code.google.com/p/modpagespeed/issues/detail?q=css&id=252
TEST_F(CssInlineFilterTest, ClaimsXhtmlButHasUnclosedLink) {
// XHTML text should not have unclosed links. But if they do, like
// in Issue 252, then we should leave them alone.
static const char html_format[] =
"<head>\n"
" %s\n"
" %s\n"
" <script type='text/javascript' src='c.js'></script>" // 'in' <link>
"</head>\n"
"<body><div class=\"c1\"><div class=\"c2\"><p>\n"
" Yellow on Blue</p></div></div></body>";
static const char unclosed_css[] =
" <link rel='stylesheet' href='a.css' type='text/css'>\n"; // unclosed
static const char inlined_css[] = " <style>.a {}</style>\n";
// Put original CSS files into our fetcher.
ResponseHeaders default_css_header;
SetDefaultLongCacheHeaders(&kContentTypeCss, &default_css_header);
SetFetchResponse(StrCat(kTestDomain, "a.css"), default_css_header, ".a {}");
AddFilter(RewriteOptions::kInlineCss);
ValidateExpected("claims_xhtml_but_has_unclosed_links",
StringPrintf(html_format, kXhtmlDtd, unclosed_css),
StringPrintf(html_format, kXhtmlDtd, inlined_css));
}
TEST_F(CssInlineFilterTest, DontInlineInNoscript) {
options()->EnableFilter(RewriteOptions::kInlineCss);
rewrite_driver()->AddFilters();
const char kCssUrl[] = "a.css";
const char kCss[] = "div {display:block;}";
SetResponseWithDefaultHeaders(kCssUrl, kContentTypeCss, kCss, 3000);
GoogleString html_input =
StrCat("<noscript><link rel=stylesheet href=\"", kCssUrl,
"\"></noscript>");
ValidateNoChanges("noscript_noinline", html_input);
}
TEST_F(CssInlineFilterTest, InlineAndPrioritizeCss) {
// Make sure we interact with Critical CSS properly, including in cached
// case.
options()->EnableFilter(RewriteOptions::kInlineCss);
options()->EnableFilter(RewriteOptions::kPrioritizeCriticalCss);
rewrite_driver()->AddFilters();
const char kCssUrl[] = "a.css";
const char kCss[] = "div {display:block;}";
SetResponseWithDefaultHeaders(kCssUrl, kContentTypeCss, kCss, 3000);
GoogleString html_input =
StrCat("<link rel=stylesheet href=\"", kCssUrl, "\">");
GoogleString html_output = StrCat("<style>", kCss, "</style>");
ValidateExpected("inline_prioritize", html_input, html_output);
}
TEST_F(CssInlineFilterTest, InlineCombined) {
// Make sure we interact with CSS combiner properly, including in cached
// case.
options()->EnableFilter(RewriteOptions::kInlineCss);
options()->EnableFilter(RewriteOptions::kCombineCss);
rewrite_driver()->AddFilters();
const char kCssUrl[] = "a.css";
const char kCss[] = "div {display:block;}";
SetResponseWithDefaultHeaders(kCssUrl, kContentTypeCss, kCss, 3000);
GoogleString html_input =
StrCat("<link rel=stylesheet href=\"", kCssUrl, "\">",
"<link rel=stylesheet href=\"", kCssUrl, "\">");
GoogleString html_output = StrCat("<style>", kCss, "\n", kCss, "</style>");
ValidateExpected("inline_combined", html_input, html_output);
ValidateExpected("inline_combined", html_input, html_output);
}
TEST_F(CssInlineFilterTest, InlineMinimizeInteraction) {
// There was a bug in async mode where we would accidentally prevent
// minification results from rendering when inlining was not to be done.
options()->EnableFilter(RewriteOptions::kRewriteCss);
options()->set_css_inline_max_bytes(4);
TestInlineCssWithOutputUrl(
StrCat(kTestDomain, "minimize_but_not_inline.html"), "",
StrCat(kTestDomain, "a.css"),
// Note: Original URL was absolute, so rewritten one is as well.
Encode(kTestDomain, "cf", "0", "a.css", "css"),
"", /* no other attributes*/
"div{display: none;}",
false,
"div{display: none}",
"CSS not inlined since it&#39;s bigger than 4 bytes");
}
TEST_F(CssInlineFilterTest, InlineCacheExtendInteraction) {
options()->set_css_inline_max_bytes(400);
options()->EnableFilter(RewriteOptions::kInlineCss);
options()->EnableFilter(RewriteOptions::kExtendCacheCss);
rewrite_driver()->AddFilters();
const char kCssUrl[] = "a.css";
const char kCss[] = "div {display:block;}";
SetResponseWithDefaultHeaders(kCssUrl, kContentTypeCss, kCss, 3000);
ValidateExpected("inline_plus_ce", CssLinkHref(kCssUrl),
StrCat("<style>", kCss, "</style>"));
// Cache extender should not have successfully produced an output on this
// CSS, as it got inlined --- in the past it would have.
EXPECT_EQ(0,
rewrite_driver()->statistics()->GetVariable(
CacheExtender::kCacheExtensions)->Get());
// Now try again (as this should be hitting cache hit paths for the inliner).
ValidateExpected("inline_plus_ce", CssLinkHref(kCssUrl),
StrCat("<style>", kCss, "</style>"));
EXPECT_EQ(0,
rewrite_driver()->statistics()->GetVariable(
CacheExtender::kCacheExtensions)->Get());
}
TEST_F(CssInlineFilterTest, InlineCacheExtendInteractionRepeated) {
// As above, but also with a repeated link
options()->set_css_inline_max_bytes(400);
options()->EnableFilter(RewriteOptions::kInlineCss);
options()->EnableFilter(RewriteOptions::kExtendCacheCss);
rewrite_driver()->AddFilters();
const char kCssUrl[] = "a.css";
const char kCss[] = "div {display:block;}";
SetResponseWithDefaultHeaders(kCssUrl, kContentTypeCss, kCss, 3000);
GoogleString inlined_css = StrCat("<style>", kCss, "</style>");
ValidateExpected("inline_plus_ce_repeated",
StrCat(CssLinkHref(kCssUrl), CssLinkHref(kCssUrl)),
StrCat(inlined_css, inlined_css));
// Cache extender should not have successfully produced an output on this
// CSS, as it got inlined --- in the past it would have.
EXPECT_EQ(0,
rewrite_driver()->statistics()->GetVariable(
CacheExtender::kCacheExtensions)->Get());
// Now try again (as this should be hitting cache hit paths for the inliner).
ValidateExpected("inline_plus_ce_repeated",
StrCat(CssLinkHref(kCssUrl), CssLinkHref(kCssUrl)),
StrCat(inlined_css, inlined_css));
EXPECT_EQ(0,
rewrite_driver()->statistics()->GetVariable(
CacheExtender::kCacheExtensions)->Get());
}
TEST_F(CssInlineFilterTest, CharsetDetermination) {
// Sigh. rewrite_filter.cc doesn't have its own unit test so we test this
// method here since we're the only ones that use it.
GoogleString x_css_url = "x.css";
GoogleString y_css_url = "y.css";
GoogleString z_css_url = "z.css";
const char x_css_body[] = "BODY { color: red; }";
const char y_css_body[] = "BODY { color: green; }";
const char z_css_body[] = "BODY { color: blue; }";
GoogleString y_bom_body = StrCat(kUtf8Bom, y_css_body);
GoogleString z_bom_body = StrCat(kUtf8Bom, z_css_body);
// x.css has no charset header nor a BOM.
// y.css has no charset header but has a BOM.
// z.css has a charset header and a BOM.
ResponseHeaders default_header;
SetDefaultLongCacheHeaders(&kContentTypeJavascript, &default_header);
SetFetchResponse(StrCat(kTestDomain, x_css_url), default_header, x_css_body);
SetFetchResponse(StrCat(kTestDomain, y_css_url), default_header, y_bom_body);
default_header.MergeContentType("text/css; charset=iso-8859-1");
SetFetchResponse(StrCat(kTestDomain, z_css_url), default_header, z_bom_body);
ResourcePtr x_css_resource(CreateResource(kTestDomain, x_css_url));
ResourcePtr y_css_resource(CreateResource(kTestDomain, y_css_url));
ResourcePtr z_css_resource(CreateResource(kTestDomain, z_css_url));
EXPECT_TRUE(ReadIfCached(x_css_resource));
EXPECT_TRUE(ReadIfCached(y_css_resource));
EXPECT_TRUE(ReadIfCached(z_css_resource));
GoogleString result;
const StringPiece kUsAsciiCharset("us-ascii");
// Nothing set: charset should be empty.
result = RewriteFilter::GetCharsetForStylesheet(x_css_resource.get(), "", "");
EXPECT_TRUE(result.empty());
// Only the containing charset is set.
result = RewriteFilter::GetCharsetForStylesheet(x_css_resource.get(),
"", kUsAsciiCharset);
EXPECT_STREQ(result, kUsAsciiCharset);
// The containing charset is trumped by the element's charset attribute.
result = RewriteFilter::GetCharsetForStylesheet(x_css_resource.get(),
"gb", kUsAsciiCharset);
EXPECT_STREQ("gb", result);
// The element's charset attribute is trumped by the resource's BOM.
result = RewriteFilter::GetCharsetForStylesheet(y_css_resource.get(),
"gb", kUsAsciiCharset);
EXPECT_STREQ("utf-8", result);
// The resource's BOM is trumped by the resource's header.
result = RewriteFilter::GetCharsetForStylesheet(z_css_resource.get(),
"gb", kUsAsciiCharset);
EXPECT_STREQ("iso-8859-1", result);
}
TEST_F(CssInlineFilterTest, InlineWithCompatibleBom) {
const GoogleString css = "BODY { color: red; }\n";
const GoogleString css_with_bom = StrCat(kUtf8Bom, css);
TestInlineCssWithOutputUrl("http://www.example.com/index.html",
" <meta charset=\"UTF-8\">\n",
"http://www.example.com/styles.css",
"http://www.example.com/styles.css",
"", css_with_bom, true, css, "");
}
TEST_F(CssInlineFilterTest, DoNotInlineWithIncompatibleBomAndNonAscii) {
const GoogleString css = "BODY { color: red; /* \xD2\x90 */ }\n";
const GoogleString css_with_bom = StrCat(kUtf8Bom, css);
TestInlineCssWithOutputUrl("http://www.example.com/index.html",
" <meta charset=\"ISO-8859-1\">\n",
"http://www.example.com/styles.css",
"http://www.example.com/styles.css",
"", css_with_bom, false, "",
"CSS not inlined due to apparent charset "
"incompatibility; we think the HTML is ISO-8859-1 "
"while the CSS is utf-8");
}
TEST_F(CssInlineFilterTest, DoInlineWithIncompatibleBomAndAscii) {
// Even though the content is labeled utf-8, it keeps to ASCII subset, so it's
// safe to inline.
const GoogleString css = "BODY { color: red; }\n";
const GoogleString css_with_bom = StrCat(kUtf8Bom, css);
TestInlineCssWithOutputUrl("http://www.example.com/index.html",
" <meta charset=\"ISO-8859-1\">\n",
"http://www.example.com/styles.css",
"http://www.example.com/styles.css",
"", css_with_bom, true, css,
"");
}
// See: http://www.alistapart.com/articles/alternate/
// and http://www.w3.org/TR/html4/present/styles.html#h-14.3.1
TEST_F(CssInlineFilterTest, AlternateStylesheet) {
AddFilter(RewriteOptions::kInlineCss);
SetResponseWithDefaultHeaders("foo.css", kContentTypeCss, "a{margin:0}", 100);
// Normal (persistent) CSS links are inlined.
ValidateExpected(
"persistent",
"<link rel='stylesheet' href='foo.css'>",
"<style>a{margin:0}</style>");
// Make sure we accept mixed case for the keyword.
ValidateExpected(
"mixed_case",
"<link rel=' StyleSheet ' href='foo.css'>",
"<style>a{margin:0}</style>");
// Preferred CSS links are not because inline styles cannot be given
// a title (AFAICT). The title attribute indicates that the given
// CSS can be overridden by an alternate style sheet.
ValidateNoChanges(
"preferred",
"<link rel='stylesheet' href='foo.css' title='foo'>");
// Alternate CSS links, likewise.
ValidateNoChanges(
"alternate",
"<link rel='alternate stylesheet' href='foo.css' title='foo'>");
}
TEST_F(CssInlineFilterTest, CarryAcrossOtherAttributes) {
// Carry across attributes such as id and class to the inlined style tag.
AddFilter(RewriteOptions::kInlineCss);
SetResponseWithDefaultHeaders("foo.css", kContentTypeCss, "a{margin:0}", 100);
ValidateExpected(
"CarryAcross",
"<link rel='stylesheet' href='foo.css' id='my-stylesheet' class='a b c'"
" lulz='!@$@#$%@4lulz'>",
"<style id='my-stylesheet' class='a b c' lulz='!@$@#$%@4lulz'>"
"a{margin:0}</style>");
// But respect pagespeed_no_transform
ValidateNoChanges(
"NoTransform",
"<link rel='stylesheet' href='foo.css' id='my-stylesheet' class='a b c' "
"pagespeed_no_transform>");
ValidateNoChanges(
"NoTransform",
"<link rel='stylesheet' href='foo.css' id='my-stylesheet' class='a b c' "
"data-pagespeed-no-transform>");
}
TEST_F(CssInlineFilterTest, NoRel) {
AddFilter(RewriteOptions::kInlineCss);
SetResponseWithDefaultHeaders("foo.css", kContentTypeCss, "a{margin:0}", 100);
// We don't mess with links that lack rel attributes.
ValidateNoChanges("no_rel", "<link href='foo.css'>");
}
TEST_F(CssInlineFilterTest, NonCss) {
AddFilter(RewriteOptions::kInlineCss);
SetResponseWithDefaultHeaders("foo.xsl", kContentTypeXml,
"<xsl:variable name='foo' select='bar'>", 100);
ValidateNoChanges("non_css",
"<link rel='stylesheet' href='foo.xsl' type='text/xsl'/>");
}
TEST_F(CssInlineFilterTest, NoInliningOfCloseStyleTag) {
VerifyNoInliningForClosingStyleTag("</style>");
}
TEST_F(CssInlineFilterTest, NoInliningOfCloseStyleTagWithCapitalization) {
VerifyNoInliningForClosingStyleTag("</Style>");
}
TEST_F(CssInlineFilterTest, NoInliningOfCloseStyleTagWithSpaces) {
VerifyNoInliningForClosingStyleTag("</style abc>");
}
} // namespace
} // namespace net_instaweb