blob: 7488872da08fdef4504c8dc107102ca6c407de8d [file] [log] [blame]
/*
* Copyright 2011 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: matterbury@google.com (Matt Atterbury)
#include <cstddef>
#include "base/logging.h"
#include "net/instaweb/rewriter/public/css_rewrite_test_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 "pagespeed/kernel/base/basictypes.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/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/response_headers.h"
namespace net_instaweb {
namespace {
// Filenames of resource files.
const char kTopCssFile[] = "assets/styles.css";
const char kOneLevelDownFile1[] = "assets/nested1.css";
const char kOneLevelDownFile2[] = "assets/nested2.css";
const char kTwoLevelsDownFile1[] = "assets/nested/nested1.css";
const char kTwoLevelsDownFile2[] = "assets/nested/nested2.css";
const char k404CssFile[] = "404.css";
const char kSimpleCssFile[] = "simple.css";
const char kComplexCssFile[] = "complex.css";
// Contents of resource files. Already minimized. NOTE relative paths!
static const char kTwoLevelsDownContents1[] =
".background_cyan{background-color:#0ff}"
".foreground_pink{color:#ffc0cb}";
static const char kTwoLevelsDownContents2[] =
".background_green{background-color:#0f0}"
".foreground_rose{color:rose}";
static const char kOneLevelDownCss1[] =
".background_blue{background-color:#00f}"
".foreground_gray{color:gray}";
static const char kOneLevelDownCss2[] =
".background_white{background-color:#fff}"
".foreground_black{color:#000}";
static const char kTopCss[] =
".background_red{background-color:red}"
".foreground_yellow{color:#ff0}";
static const char kSimpleCss[] =
".background_red{background-color:red}"
".foreground_yellow{color:#ff0}";
static const char kComplexCss[] =
" @media screen and (min-width: 240px) {"
" .background_red{background-color:red}"
"}";
class CssFlattenImportsTest : public CssRewriteTestBase {
protected:
CssFlattenImportsTest() {
kOneLevelDownContents1 = StrCat(
"@import url(nested/nested1.css);",
kOneLevelDownCss1);
kOneLevelDownContents2 = StrCat(
"@import url(nested/nested2.css);",
kOneLevelDownCss2);
kTopCssContents = StrCat(
"@import url(nested1.css);",
"@import url(nested2.css);",
kTopCss);
kFlattenedTopCssContents = StrCat(
kTwoLevelsDownContents1, kOneLevelDownCss1,
kTwoLevelsDownContents2, kOneLevelDownCss2,
kTopCss);
kFlattenedOneLevelDownContents1 = StrCat(
kTwoLevelsDownContents1,
kOneLevelDownCss1);
}
virtual void SetUpFilters() {
options()->EnableFilter(RewriteOptions::kFlattenCssImports);
options()->EnableFilter(RewriteOptions::kExtendCacheImages);
options()->set_always_rewrite_css(true);
rewrite_driver()->AddFilters();
}
virtual void SetUpResponses() {
SetResponseWithDefaultHeaders(kTopCssFile, kContentTypeCss,
kTopCssContents, 100);
SetResponseWithDefaultHeaders(kOneLevelDownFile1, kContentTypeCss,
kOneLevelDownContents1, 100);
SetResponseWithDefaultHeaders(kOneLevelDownFile2, kContentTypeCss,
kOneLevelDownContents2, 100);
SetResponseWithDefaultHeaders(kTwoLevelsDownFile1, kContentTypeCss,
kTwoLevelsDownContents1, 100);
SetResponseWithDefaultHeaders(kTwoLevelsDownFile2, kContentTypeCss,
kTwoLevelsDownContents2, 100);
SetResponseWithDefaultHeaders(kComplexCssFile, kContentTypeCss,
kComplexCss, 100);
SetFetchResponse404(k404CssFile);
}
virtual void SetUp() {
// We don't use the parent class setup, because we want to make sure that
// RewriteCss is enabled implicitly by enabling FlattenCssImports. We
// skip to the setup for the parent of our parent class.
RewriteTestBase::SetUp();
SetUpFilters();
SetUpResponses();
}
// General routine to test flattening of nested resources referenced with
// relative (trim_urls == true) or absolute (trim_urls == false) paths and
// optional post-flattening cache extension (cache_extend == true).
void TestFlattenNested(bool trim_urls, bool cache_extend) {
// /foo.png
const char kFooPngFilename[] = "foo.png";
const char kImageData[] = "Invalid PNG but does not matter for this test";
SetResponseWithDefaultHeaders(kFooPngFilename, kContentTypePng,
kImageData, 100);
GoogleString foo_domain(trim_urls ? "" : kTestDomain);
GoogleString foo_path =
(cache_extend
? Encode(foo_domain, "ce", "0", kFooPngFilename, "png")
: StrCat(foo_domain, kFooPngFilename));
// /image1.css loads /foo.png as a background image.
const char kCss1Filename[] = "image1.css";
const GoogleString css1_before =
StrCat("body {\n"
" background-image: url(", kFooPngFilename, ");\n"
"}\n");
const GoogleString css1_after =
StrCat("body{background-image:url(", foo_path, ")}");
SetResponseWithDefaultHeaders(kCss1Filename, kContentTypeCss,
css1_before, 100);
// /nested/bar.png
const char kBarPngFilename[] = "bar.png";
SetResponseWithDefaultHeaders(StrCat("nested/", kBarPngFilename),
kContentTypePng, kImageData, 100);
GoogleString bar_domain(trim_urls ? "nested/" :
StrCat(kTestDomain, "nested/"));
GoogleString bar_path =
(cache_extend
? Encode(bar_domain, "ce", "0", kBarPngFilename, "png")
: StrCat(bar_domain, kBarPngFilename));
// /nested/image2.css loads /nested/bar.png & /foo.png as background images.
const char kCss2Filename[] = "nested/image2.css"; // because its CSS is!
const GoogleString css2_before =
StrCat("body {\n"
" background-image: url(", kBarPngFilename, ");\n"
"}\n"
"div {\n"
" background-image: url(../", kFooPngFilename, ");\n"
"}\n");
const GoogleString css2_after =
StrCat("body{background-image:url(", bar_path, ")}"
"div{background-image:url(", foo_path, ")}");
SetResponseWithDefaultHeaders(kCss2Filename, kContentTypeCss,
css2_before, 100);
// /foo-then-bar.css @imports /nested/image1.css then /nested/image2.css
const char kTop1CssFilename[] = "foo-then-bar.css";
const GoogleString top1_before =
StrCat("@import url(", kCss1Filename, ");",
"@import url(", kCss2Filename, ");");
const GoogleString top1_after = StrCat(css1_after, css2_after);
SetResponseWithDefaultHeaders(kTop1CssFilename, kContentTypeCss,
top1_before, 100);
// /bar-then-foo.css @imports /nested/image2.css then /nested/image1.css
const char kTop2CssFilename[] = "bar-then-foo.css";
const GoogleString top2_before =
StrCat("@import url(", kCss2Filename, ");",
"@import url(", kCss1Filename, ");");
const GoogleString top2_after = StrCat(css2_after, css1_after);
SetResponseWithDefaultHeaders(kTop2CssFilename, kContentTypeCss,
top2_before, 100);
// Phew! Load them both. bar-then-foo.css should use cached data.
ValidateRewriteExternalCss("flatten_then_cache_extend_nested1",
top1_before, top1_after,
kExpectSuccess | kNoClearFetcher);
ValidateRewriteExternalCss("flatten_then_cache_extend_nested2",
top2_before, top2_after,
kExpectSuccess | kNoClearFetcher);
}
// General routine to test that we flatten -then- cache extend the PNG in
// the resulting CSS while absolutifying the PNGs' URLs while flattening
// then [not] relativizing them while rewriting them.
void TestCacheExtendsAfterFlatteningNested(bool trim_urls) {
TestFlattenNested(trim_urls, true);
}
// General routine to test charset handling. The header_charset argument
// specifies the charset we stick into the HTML page's headers, if any, while
// the meta_tag_charset and http_equiv_charset arguments specify the charset
// we stick into a meta tag in the <head> element; these control the charset
// of the HTML page that starts the flattening import. The imported css files
// all specify @charset utf-8, and the default HTML charset, if none is
// specified by one of these arguments, is iso-8859-1, so, unless the result
// is for a HTML charset of utf-8, the test will fail. The bool says whether
// we expect to succeed.
void TestFlattenWithHtmlCharset(const StringPiece& header_charset,
const StringPiece& meta_tag_charset,
const StringPiece& http_equiv_charset,
bool should_succeed) {
const char kStylesFilename[] = "styles.css";
const GoogleString kStylesContents = StrCat(
"@charset \"UTF-8\";",
"@import url(print.css);",
"@import url(screen.css);",
kSimpleCss);
// Next block is a reimplementation of SetResponseWithDefaultHeaders()
// but setting the charset in the Content-Type header.
GoogleString url = AbsolutifyUrl(kStylesFilename);
int64 ttl_sec = 100;
ResponseHeaders response_headers;
DefaultResponseHeaders(kContentTypeCss, ttl_sec, &response_headers);
response_headers.Replace(HttpAttributes::kContentType,
"text/css; charset=utf-8");
response_headers.ComputeCaching();
SetFetchResponse(url, response_headers, kStylesContents);
// Now we set the charset in the driver headers which is how we as a test
// program set the HTML's charset.
ResponseHeaders driver_headers;
if (!header_charset.empty()) {
driver_headers.Add(HttpAttributes::kContentType,
StrCat("text/css; charset=", header_charset));
}
ValidationFlags meta_tag_flag = kNoFlags;
if (!meta_tag_charset.empty()) {
if (meta_tag_charset == "utf-8") {
meta_tag_flag = kMetaCharsetUTF8;
} else if (meta_tag_charset == "iso-8859-1") {
meta_tag_flag = kMetaCharsetISO88591;
} else {
DCHECK(meta_tag_charset == "utf-8" || meta_tag_charset == "iso-8859-1");
}
DCHECK(http_equiv_charset.empty());
}
if (!http_equiv_charset.empty()) {
if (http_equiv_charset == "utf-8") {
meta_tag_flag = kMetaHttpEquiv;
} else if (http_equiv_charset == "iso-8859-1") {
meta_tag_flag = kMetaHttpEquivUnquoted;
} else {
DCHECK(http_equiv_charset == "utf-8" ||
http_equiv_charset == "iso-8859-1");
}
DCHECK(meta_tag_charset.empty());
}
driver_headers.ComputeCaching();
rewrite_driver()->set_response_headers_ptr(&driver_headers);
const char kPrintFilename[] = "print.css";
const char kPrintCss[] =
".background_cyan{background-color:#0ff}"
".foreground_pink{color:#ffc0cb}";
const GoogleString kPrintContents = kPrintCss;
SetResponseWithDefaultHeaders(kPrintFilename, kContentTypeCss,
kPrintContents, 100);
const char kScreenFilename[] = "screen.css";
const char kScreenCss[] =
".background_blue{background-color:#00f}"
".foreground_gray{color:gray}";
const GoogleString kScreenContents = StrCat(
"@charset \"UtF-8\";",
kScreenCss);
SetResponseWithDefaultHeaders(kScreenFilename, kContentTypeCss,
kScreenContents, 100);
const char css_in[] = "@import url(http://test.com/styles.css);";
if (should_succeed) {
const GoogleString css_out = StrCat(kPrintCss, kScreenCss, kSimpleCss);
// TODO(sligocki): Why do we need kNoOtherContexts here?
ValidateRewriteExternalCss("flatten_nested_media",
css_in, css_out,
kExpectSuccess | kNoOtherContexts |
kNoClearFetcher | meta_tag_flag);
// Check things work when data is already cached.
ValidateRewriteExternalCss("flatten_nested_media",
css_in, css_out,
kExpectCached | kNoOtherContexts |
kNoClearFetcher | meta_tag_flag);
} else {
ValidateRewriteExternalCss("flatten_nested_media",
css_in, css_in,
kExpectSuccess | kNoOtherContexts |
kNoClearFetcher | meta_tag_flag |
kFlattenImportsCharsetMismatch);
ValidateRewriteExternalCss("flatten_nested_media",
css_in, css_in,
kExpectCached | kNoOtherContexts |
kNoClearFetcher | meta_tag_flag);
}
}
// Test the css_flatten_max_bytes() setting.
void TestLimit(const StringPiece test_id,
bool limit_exceeded,
int flattening_limit,
int actual_amount,
const GoogleString& css_in,
const GoogleString& css_out) {
options()->ClearSignatureForTesting();
options()->set_css_flatten_max_bytes(flattening_limit);
server_context()->ComputeSignature(options());
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss,
kSimpleCss, 100);
ValidationFlags extra_flag = kNoFlags;
if (limit_exceeded) {
extra_flag = kFlattenImportsLimitExceeded;
DebugWithMessage(StringPrintf("<!--Flattening failed: "
"Flattening limit (%d) exceeded (%d)-->",
flattening_limit, actual_amount));
} else {
DebugWithMessage("");
}
ValidateRewriteExternalCss(test_id, css_in, css_out,
kExpectSuccess | kNoClearFetcher | extra_flag);
// We do not specify kNoClearFetcher, so the fetcher is cleared. Thus,
// content must be pulled from the cache. kNoOtherContexts because
// other contexts won't have this value cached.
ValidateRewriteExternalCss(test_id, css_in, css_out,
kExpectCached | kNoOtherContexts | extra_flag);
}
// Test relative URLs in CSS that itself is referenced via a relative URL.
void TestRelativeImageUrlInRelativeCssUrl(bool trim_urls, bool cache_extend) {
// Setup the image we refer to.
const char kFooPng[] = "images/foo.png";
const GoogleString foo_png_path = StrCat(kTestDomain, "a/", kFooPng);
const char kImageData[] = "Invalid PNG but does not matter for this test";
SetResponseWithDefaultHeaders(foo_png_path, kContentTypePng,
kImageData, 100);
// Setup the CSS that refers to it.
const char kSimpleCssTemplate[] =
".background_red{background-color:red}"
".foreground_yellow{color:#ff0}"
".body{background-image:url(%s)}";
// The input CSS refers to ../images/test.jpg from the file /a/b/simple.css,
// so the image's path is /a/images/test.jpg, which is what should be used
// when the CSS is flattened into the base document (with base of '/').
const GoogleString simple_css_path =
StrCat(kTestDomain, "a/b/", kSimpleCssFile);
const GoogleString relative_simple_css_in =
StringPrintf(kSimpleCssTemplate, StrCat("../", kFooPng).c_str());
SetResponseWithDefaultHeaders(simple_css_path, kContentTypeCss,
relative_simple_css_in, 100);
const GoogleString import_simple_css =
StrCat("@import url(", simple_css_path, ");");
const GoogleString foo_png_output =
(cache_extend
? Encode(StrCat(trim_urls ? "" : kTestDomain, "a/images/"),
"ce", "0", "foo.png", "png")
: StrCat(trim_urls ? "" : kTestDomain, "a/", kFooPng));
const GoogleString simple_css_out =
StringPrintf(kSimpleCssTemplate, foo_png_output.c_str());
ValidateRewriteInlineCss("flatten_relative",
import_simple_css, simple_css_out,
kExpectSuccess);
}
GoogleString kOneLevelDownContents1;
GoogleString kOneLevelDownContents2;
GoogleString kTopCssContents;
GoogleString kFlattenedTopCssContents;
GoogleString kFlattenedOneLevelDownContents1;
};
TEST_F(CssFlattenImportsTest, FlattenInlineCss) {
const char css_in[] =
"@import url(http://test.com/simple.css);";
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss,
kSimpleCss, 100);
ValidateRewriteInlineCss("flatten_simple", css_in, kSimpleCss,
kExpectSuccess);
// TODO(sligocki): This suggests that we grew the number of bytes, which is
// misleading because originally, the user would have loaded both files
// and now they will only load one. So total bytes are less.
// I think this should be listing bytes saved as STATIC_STRLEN(css_in).
int64 expected_savings =
static_cast<int64>(STATIC_STRLEN(css_in)) -
static_cast<int64>(STATIC_STRLEN(kSimpleCss));
EXPECT_EQ(expected_savings, total_bytes_saved_->Get());
}
TEST_F(CssFlattenImportsTest, DontFlattenAttributeCss) {
options()->ClearSignatureForTesting();
options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
server_context()->ComputeSignature(options());
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss,
kSimpleCss, 100);
// Test that rewriting of attributes is enabled and working.
ValidateExpected("rewrite-attribute-setup",
"<div style='background-color: #f00; color: yellow;'/>",
"<div style='background-color:red;color:#ff0'/>");
// Test that we don't rewrite @import's in attributes since that's invalid.
ValidateNoChanges("rewrite-attribute",
"<div style='@import url(http://test.com/simple.css)'/>");
}
TEST_F(CssFlattenImportsTest, FlattenNoop) {
ValidateRewriteExternalCss("flatten_noop", kSimpleCss, kSimpleCss,
kExpectSuccess | kNoClearFetcher);
}
TEST_F(CssFlattenImportsTest, Flatten404) {
DebugWithMessage("<!--4xx status code, preventing rewriting of"
" http://test.com/404.css-->");
const char css_in[] =
"@import url(http://test.com/404.css);";
ValidateRewriteExternalCss("flatten_404", css_in, css_in,
kExpectSuccess | kNoClearFetcher);
}
TEST_F(CssFlattenImportsTest, DontFlattenWithUnauthorizedCSS) {
// Turn on debug to get the flattening failure reason in an HTML comment.
const char kFailureReason[] = "<!--Flattening failed: Cannot import "
"http://unauth.com/assets/styles.css "
"as it is on an unauthorized domain-->";
DebugWithMessage(kFailureReason);
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss,
kSimpleCss, 100);
const char kUnauthorizedImportCss[] =
"@import url(http://unauth.com/assets/styles.css);\n"
"@import url(http://test.com/simple.css);\n"
"a { color:red }";
const char kRewrittenUnauthorizedImportCss[] =
"@import url(http://unauth.com/assets/styles.css);"
"@import url(http://test.com/simple.css);"
"a{color:red}";
ValidateRewriteExternalCss("dont_flatten_unauthorized_css_import",
kUnauthorizedImportCss, kRewrittenUnauthorizedImportCss,
kExpectSuccess | kNoClearFetcher);
const char kAuthorizedTopLevelCss[] =
"@import url(auth_parent_with_unauth_child_import.css);"
"b { color: blue }";
const char kRewrittenAuthorizedTopLevelCss[] =
"@import url(auth_parent_with_unauth_child_import.css);"
"b{color:#00f}";
SetResponseWithDefaultHeaders("auth_parent_with_unauth_child_import.css",
kContentTypeCss,
kUnauthorizedImportCss, 100);
ValidateRewriteExternalCss("dont_flatten_nested_unauthorized_css_import",
kAuthorizedTopLevelCss, kRewrittenAuthorizedTopLevelCss,
kExpectSuccess | kNoClearFetcher);
}
TEST_F(CssFlattenImportsTest, FlattenInvalidCSS) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--CSS rewrite failed: Parse error in %url%-->");
const char kInvalidMediaCss[] = "@media }}";
ValidateRewriteExternalCss("flatten_invalid_css_media",
kInvalidMediaCss, kInvalidMediaCss,
kExpectFailure);
const char kFilename[] = "styles.css";
SetResponseWithDefaultHeaders(kFilename, kContentTypeCss, kSimpleCss, 100);
// This gets a parse error but thanks to the idea of "unparseable sections"
// in the CSS parser it's not treated as an error and the "bad" text is kept.
// Because the error was in the bogus @import statement, we do NOT flatten.
DebugWithMessage("");
const char kUnparseableImportCss[] = "@import styles.css; a { color:red }";
const char kFlattenedImportCss[] = "@import styles.css;a{color:red}";
ValidateRewriteExternalCss("flatten_unparseable_css_import",
kUnparseableImportCss, kFlattenedImportCss,
kExpectSuccess | kNoClearFetcher);
// Same as above, but since the @import itself is valid we DO flatten.
const char kUnparseableCss[] = "@import url(styles.css) ;a{ #color: 333 }";
GoogleString kFlattenedInvalidCss = StrCat(kSimpleCss, "a{#color: 333 }");
DebugWithMessage("");
ValidateRewriteExternalCss("flatten_unparseable_css_rule",
kUnparseableCss, kFlattenedInvalidCss,
kExpectSuccess | kNoClearFetcher);
// This gets a non-recoverable parse error because of mismatched {}s.
// We do not want to recover from these types of parse errors because
// combining/flattening files like this would spread the breakage.
// Note: This specific case is probably technically safe to flatten
// because the broken CSS is at the end, but we choose not to dance
// on that knife edge and just disallow flattening.
DebugWithMessage("<!--CSS rewrite failed: Parse error in %url%-->");
const char kErrorCss[] = "@import url(styles.css);a{{ color:red }";
ValidateRewriteExternalCss("no_flatten_error_css_rule",
kErrorCss, kErrorCss,
kExpectFailure | kNoClearFetcher);
// Make sure we don't flatten if the @imported CSS has a non-recoverable
// parse error.
SetResponseWithDefaultHeaders("error.css", kContentTypeCss,
"a {{ color: red }", 100);
const char kImportErrorCss[] =
"@import url(error.css); body { color: #000 }";
const char kRewrittenImportErrorCss[] =
"@import url(error.css);body{color:#000}";
// Note: Rewrite succeeds, but flatten fails.
// TODO(jmaessen): Should contain parse error for nested file as well,
// once nested rewrites propagate correctly.
DebugWithMessage("<!--Flattening failed: Cannot parse the CSS "
"in http://test.com/error.css-->");
ValidateRewriteExternalCss("no_flatten_error_in_import",
kImportErrorCss, kRewrittenImportErrorCss,
kExpectSuccess | kFlattenImportsMinifyFailed |
kNoClearFetcher);
// Note that this is not run as ValidateRerwite(), because that would re-use
// the rewritten value and thus not increment kFlattenImportsMinifyFailed
// the second time.
}
TEST_F(CssFlattenImportsTest, FlattenEmptyMedia) {
ValidateRewriteExternalCss("flatten_empty_media",
"@media {}", "", kExpectSuccess);
}
TEST_F(CssFlattenImportsTest, FlattenSimple) {
const char css_in[] =
"@import url(http://test.com/simple.css);";
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss,
kSimpleCss, 100);
ValidateRewriteExternalCss("flatten_simple", css_in, kSimpleCss,
kExpectSuccess | kNoClearFetcher);
// Check things work when data is already cached.
ValidateRewriteExternalCss("flatten_simple", css_in, kSimpleCss,
kExpectCached | kNoOtherContexts);
}
TEST_F(CssFlattenImportsTest, FlattenUnderLargeLimit) {
// The default limit is 2k, large enough to flatten everything into.
// Note that the top level CSS is not minified on input but is on output.
const char css_in[] =
"@import url(http://test.com/simple.css);\n"
"@import url(http://test.com/simple.css);\n";
const GoogleString css_out = StrCat(kSimpleCss, kSimpleCss);
TestLimit("flatten_under_limit", false /* limit_exceeded */,
1 + css_out.size() /* flattening_limit */,
0 /* no limit exceeded comment */,
css_in, css_out);
}
TEST_F(CssFlattenImportsTest, DontFlattenOverMediumLimit) {
// This limit will result in simple.css being flattened OK, but the outer
// CSS that @imports it twice won't fit so flattening will fail.
// Note that the top level CSS is not minified on input but is on output.
const char css_in[] =
"@import url(http://test.com/simple.css);\n"
"@import url(http://test.com/simple.css);\n";
const char css_out[] =
"@import url(http://test.com/simple.css);"
"@import url(http://test.com/simple.css);";
TestLimit("dont_flatten_over_limit", true /* limit_exceeded */,
1 + STATIC_STRLEN(css_out) /* flattening_limit */,
54 + STATIC_STRLEN(css_out) /* actual_amount */,
css_in, css_out);
}
TEST_F(CssFlattenImportsTest, DontFlattenOverTinyLimit) {
// This limit will result in even simple.css not being flattened.
// Note that the top level CSS is not minified on input but is on output.
const char css_in[] =
"@import url(http://test.com/simple.css);\n"
"@import url(http://test.com/simple.css);\n";
const char css_out[] =
"@import url(http://test.com/simple.css);"
"@import url(http://test.com/simple.css);";
TestLimit("dont_flatten_over_tiny_limit", true /* limit_exceeded */,
10 /* flattening_limit */, 67 /* actual_amount */,
css_in, css_out);
}
TEST_F(CssFlattenImportsTest, FlattenEmpty) {
// We intentionally do not inline any empty resources.
const char kFilename[] = "empty.css";
const char css_in[] = "@import url(http://test.com/empty.css);";
const char empty_content[] = "";
SetResponseWithDefaultHeaders(kFilename, kContentTypeCss, empty_content, 100);
ValidateRewriteExternalCss("flatten_empty", css_in, css_in,
kExpectSuccess | kNoClearFetcher);
// Check things work when data is already cached.
// We do not specify kNoClearFetcher, so the fetcher is cleared. Thus,
// content must be pulled from the cache. kNoOtherContexts because
// other contexts won't have this value cached.
ValidateRewriteExternalCss("flatten_empty", css_in, css_in,
kExpectCached | kNoOtherContexts);
}
TEST_F(CssFlattenImportsTest, FlattenSimpleRewriteOnTheFly) {
// import.css @import's simple.css
// simple.css contains some simple CSS
// Fetch the rewritten filename of import.css and we should get the
// flattened and minimized contents, namely simple.css's contents.
const char kImportFilename[] = "import.css";
const char css_import[] =
"@import url(http://test.com/simple.css);";
SetResponseWithDefaultHeaders(kImportFilename, kContentTypeCss,
css_import, 100);
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss,
kSimpleCss, 100);
// Check that nothing is up my sleeve ...
EXPECT_EQ(0, lru_cache()->num_elements());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
GoogleString content;
EXPECT_TRUE(FetchResource(kTestDomain, RewriteOptions::kCssFilterId,
"import.css", "css", &content));
EXPECT_EQ(kSimpleCss, content);
// Check for 6 misses and 6 inserts giving 6 elements at the end:
// 3 URLs (import.css/simple.css/rewritten) x 2 (partition key + contents).
EXPECT_EQ(6, lru_cache()->num_elements());
EXPECT_EQ(6, lru_cache()->num_inserts());
EXPECT_EQ(5, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
}
TEST_F(CssFlattenImportsTest, FlattenNested) {
const GoogleString css_in = StrCat("@import url(http://test.com/",
kTopCssFile, ");");
ValidateRewriteExternalCss("flatten_nested",
css_in, kFlattenedTopCssContents,
kExpectSuccess | kNoClearFetcher);
}
TEST_F(CssFlattenImportsTest, FlattenFromCacheDirectly) {
// Prime the pumps by loading all the CSS files into the cache.
// Verifying that the resources fetched below _are_ cached is non-trivial
// because they are stored against their partition key and determining that
// from this level requires access to and reimplementation of the inner
// working of RewriteContext and various sub-classes. At the time of writing
// I verified in the debugger that they are cached.
GoogleString css_in = StrCat("@import url(http://test.com/",
kTopCssFile, ");");
ValidateRewriteExternalCss("flatten_from_cache_directly",
css_in, kFlattenedTopCssContents,
kExpectSuccess | kNoClearFetcher);
// Check cache activity: everything cached has been inserted, no reinserts,
// no deletes. Then note values we check against below.
EXPECT_EQ(lru_cache()->num_elements(), lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
size_t num_elements = lru_cache()->num_elements();
ClearStats();
// Check things work when data is already cached, though the stats are
// messed up because we don't do any actual rewriting in that instance:
// num_blocks_rewritten_->Get() == 0 instead of 1
// total_bytes_saved_->Get() == 0 instead of negative something.
//
// We do not specify kNoClearFetcher, so the fetcher is cleared. Thus,
// content must be pulled from the cache. kNoOtherContexts because
// other contexts won't have this value cached.
ValidateRewriteExternalCss("flatten_from_cache_directly",
css_in, kFlattenedTopCssContents,
kExpectCached | kNoOtherContexts);
// Check that everything was read from the cache.
EXPECT_EQ(num_elements, lru_cache()->num_elements());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(2, lru_cache()->num_hits());
ClearStats();
num_elements = lru_cache()->num_elements();
// Access one of the cached ones directly.
//
// We do not specify kNoClearFetcher, so the fetcher is cleared. Thus,
// content must be pulled from the cache. kNoOtherContexts because
// other contexts won't have this value cached.
css_in = StrCat("@import url(http://test.com/", kTwoLevelsDownFile1, ");");
ValidateRewriteExternalCss("flatten_from_cache_directly_repeat",
css_in, kTwoLevelsDownContents1,
kExpectSuccess | kNoOtherContexts);
// The sequence in this case, for the new external link (_repeat on the end):
// MISS for the external link's partition key.
// MISS for the external link's URL.
// INSERT for the fetched external link.
// HIT for the already-cached kTwoLevelsDownFile1's partition key.
// INSERT for the rewritten external link's URL.
// INSERT for the rewritten external link's partition key.
// HIT for the rewritten external link's URL (from the fetch done by
// ValidateRewriteExternalCss with the kExpectSuccess flag).
// So, 3 new elements, 2 new misses, 2 new hits.
EXPECT_EQ(num_elements + 3, lru_cache()->num_elements());
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(2, lru_cache()->num_hits());
}
TEST_F(CssFlattenImportsTest, FlattenFromCacheIndirectly) {
// Prime the pumps by loading all the CSS files into the cache.
// Verifying that the resources fetched below _are_ cached is non-trivial
// because they are stored against their partition key and determining that
// from this level requires access to and reimplementation of the inner
// working of RewriteContext and various sub-classes. At the time of writing
// I verified in the debugger that they are cached.
GoogleString css_in = StrCat("@import url(http://test.com/",
kTopCssFile, ");");
ValidateRewriteExternalCss("flatten_from_cache_indirectly",
css_in, kFlattenedTopCssContents,
kExpectSuccess | kNoClearFetcher);
// Check cache activity: everything cached has been inserted, no reinserts,
// no deletes. Then note values we check against below.
EXPECT_EQ(lru_cache()->num_elements(), lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
size_t num_elements = lru_cache()->num_elements();
ClearStats();
// Access one of the cached ones from a different file (via @import).
const char filename[] = "alternative.css";
css_in = StrCat("@import url(http://test.com/", filename, ");");
GoogleString contents = StrCat("@import url(", kOneLevelDownFile1, ");");
SetResponseWithDefaultHeaders(filename, kContentTypeCss, contents, 100);
ValidateRewriteExternalCss("flatten_from_cache_indirectly_repeat",
css_in, kFlattenedOneLevelDownContents1,
kExpectSuccess | kNoClearFetcher);
// The sequence in this case, for the new external link (_repeat on the end):
// MISS for the external link's partition key.
// MISS for the external link's URL.
// INSERT for the fetched external link.
// MISS for alternative.css's partition key.
// MISS for alternative.css's URL.
// INSERT for the fetched alternative.css.
// HIT for the already-cached kOneLevelDownFile1's partition key.
// INSERT for the rewritten alternative.css's URL.
// INSERT for the rewritten alternative.css's partition key.
// INSERT for the rewritten external link's URL.
// INSERT for the rewritten external link's partition key.
// HIT for the rewritten external link's URL (from the fetch done by
// ValidateRewriteExternalCss with the kExpectSuccess flag).
// So, 6 new elements, 4 new misses, 2 new hits.
EXPECT_EQ(num_elements + 6, lru_cache()->num_elements());
EXPECT_EQ(4, lru_cache()->num_misses());
// TODO(matterbury): In 100 runs this was right 97 times but 3 times it
// was +4 not +2. I don't know why and don't especially care right now.
EXPECT_LE(2, lru_cache()->num_hits());
}
TEST_F(CssFlattenImportsTest, CacheExtendsAfterFlattening) {
// Check that we flatten -then- cache extend the PNG in the resulting CSS.
const char kCssFilename[] = "image.css";
const GoogleString css_before =
"body {\n"
" background-image: url(foo.png);\n"
"}\n";
const GoogleString css_after =
StrCat("body{background-image:url(",
Encode("", "ce", "0", "foo.png", "png"),
")}");
SetResponseWithDefaultHeaders(kCssFilename, kContentTypeCss, css_before, 100);
const char kFooPngFilename[] = "foo.png";
const char kImageData[] = "Invalid PNG but it does not matter for this test";
SetResponseWithDefaultHeaders(kFooPngFilename, kContentTypePng,
kImageData, 100);
ValidateRewriteExternalCss("flatten_then_cache_extend",
css_before, css_after,
kExpectSuccess | kNoClearFetcher);
// Test when everything is already cached.
ValidateRewriteExternalCss("flatten_then_cache_extend",
css_before, css_after,
kExpectCached | kNoClearFetcher);
}
TEST_F(CssFlattenImportsTest, CacheExtendsAfterFlatteningNestedAbsoluteUrls) {
TestCacheExtendsAfterFlatteningNested(false);
}
TEST_F(CssFlattenImportsTest, CacheExtendsAfterFlatteningNestedRelativeUrls) {
options()->ClearSignatureForTesting();
options()->EnableFilter(RewriteOptions::kLeftTrimUrls);
server_context()->ComputeSignature(options());
TestCacheExtendsAfterFlatteningNested(true);
}
TEST_F(CssFlattenImportsTest, FlattenRecursion) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"Recursive @import of http://test.com/recursive.css-->");
const char kFilename[] = "recursive.css";
const GoogleString css_in =
StrCat("@import url(http://test.com/", kFilename, ");");
SetResponseWithDefaultHeaders(kFilename, kContentTypeCss, css_in, 100);
ValidateRewriteExternalCss("flatten_recursive",
css_in, css_in,
kExpectSuccess | kNoClearFetcher |
kFlattenImportsRecursion);
}
TEST_F(CssFlattenImportsTest, FlattenSimpleMedia) {
const GoogleString css_in =
StrCat("@import url(http://test.com/", kSimpleCssFile, ") screen;");
const GoogleString css_out =
StrCat("@media screen{", kSimpleCss, "}");
SetResponseWithDefaultHeaders(kSimpleCssFile, kContentTypeCss, css_out, 100);
ValidateRewriteExternalCss("flatten_simple_media", css_in, css_out,
kExpectSuccess | kNoClearFetcher);
// Check things work when data is already cached.
// We do not specify kNoClearFetcher, so the fetcher is cleared. Thus,
// content must be pulled from the cache. kNoOtherContexts because
// other contexts won't have this value cached.
ValidateRewriteExternalCss("flatten_simple_media", css_in, css_out,
kExpectCached | kNoOtherContexts);
}
TEST_F(CssFlattenImportsTest, FlattenNestedMedia) {
const char kStylesFilename[] = "styles.css";
const GoogleString kStylesContents = StrCat(
"@import url(print.css) print;"
"@import url(screen.css) screen;"
"@media all{",
kSimpleCss,
"}");
SetResponseWithDefaultHeaders(kStylesFilename, kContentTypeCss,
kStylesContents, 100);
const char kPrintFilename[] = "print.css";
const char kPrintCss[] =
".background_cyan{background-color:#0ff}"
".foreground_pink{color:#ffc0cb}";
const char kPrintAllCss[] =
".background_green{background-color:#0f0}"
".foreground_rose{color:rose}";
const GoogleString kPrintContents = StrCat(
"@import url(screen.css) screen;", // discarded because print != screen
kPrintCss,
"@media all{", // subsetted to print
kPrintAllCss,
"}");
SetResponseWithDefaultHeaders(kPrintFilename, kContentTypeCss,
kPrintContents, 100);
const char kScreenFilename[] = "screen.css";
const char kScreenCss[] =
".background_blue{background-color:#00f}"
".foreground_gray{color:gray}";
const char kScreenAllCss[] =
".background_white{background-color:#fff}"
".foreground_black{color:#000}";
const GoogleString kScreenContents = StrCat(
"@import url(print.css) print;", // discarded because screen != print
kScreenCss,
"@media all{", // subsetted to screen
kScreenAllCss,
"}");
SetResponseWithDefaultHeaders(kScreenFilename, kContentTypeCss,
kScreenContents, 100);
const char css_in[] =
"@import url(http://test.com/styles.css);";
const GoogleString css_out = StrCat(
StrCat("@media print{",
kPrintCss,
kPrintAllCss,
"}"),
StrCat("@media screen{",
kScreenCss,
kScreenAllCss,
"}"),
kSimpleCss);
ValidateRewriteExternalCss("flatten_nested_media", css_in, css_out,
kExpectSuccess | kNoClearFetcher);
// Check things work when data is already cached.
ValidateRewriteExternalCss("flatten_nested_media",
css_in, css_out,
kExpectCached | kNoOtherContexts);
}
TEST_F(CssFlattenImportsTest, FlattenFontFace) {
const char kStylesFont[] = "@import url(font.css);";
SetResponseWithDefaultHeaders("font.css", kContentTypeCss,
"@font-face { font-family: 'cyborgo'; }", 100);
ValidateRewriteExternalCss("flatten_font_face", kStylesFont,
"@font-face{font-family:'cyborgo'}",
kExpectSuccess | kNoClearFetcher);
}
TEST_F(CssFlattenImportsTest, FlattenCacheDependsOnMedia) {
const GoogleString css_screen =
StrCat("@media screen{", kSimpleCss, "}");
const char css_print[] =
"@media print{"
".background_white{background-color:#fff}"
".foreground_black{color:#000}"
"}";
const char kFilename[] = "mixed.css";
const GoogleString css_contents = StrCat(css_screen, css_print);
SetResponseWithDefaultHeaders(kFilename, kContentTypeCss, css_contents, 100);
// When we @import with media screen we should cache the file in its
// entirety, and the screen-specific results, separately.
const GoogleString screen_in =
StrCat("@import url(http://test.com/", kFilename, ") screen;");
ValidateRewriteExternalCss("flatten_mixed_media_screen",
screen_in, css_screen,
kExpectSuccess | kNoClearFetcher);
// The sequence is:
// MISS for the external link's partition key.
// MISS for the external link's URL.
// INSERT for the fetched external link.
// MISS for mixed.css's partition key (for 'screen').
// MISS for mixed.css's URL.
// INSERT for the fetched mixed.css's URL.
// INSERT for the rewritten mixed.css's URL (for 'screen').
// INSERT for the fetched mixed.css's partition key (for 'screen').
// INSERT for the rewritten external link's URL.
// INSERT for the rewritten external link's partition key.
// HIT for the rewritten external link's URL (from the fetch done by
// ValidateRewriteExternalCss with the kExpectSuccess flag).
// So, 6 inserts, 4 misses, 1 hit.
EXPECT_EQ(6, lru_cache()->num_elements());
EXPECT_EQ(6, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
EXPECT_EQ(4, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_hits());
// When we @import with media print we should find the cached file but
// generate and cache the print-specific results.
const GoogleString print_in =
StrCat("@import url(http://test.com/", kFilename, ") print;");
ValidateRewriteExternalCss("flatten_mixed_media_print",
print_in, css_print,
kExpectSuccess | kNoClearFetcher);
// The sequence in this case, for the new external link (_repeat on the end):
// MISS for the external link's partition key.
// MISS for the external link's URL.
// INSERT for the fetched external link.
// MISS for mixed.css's partition key (for 'print').
// HIT for mixed.css's URL.
// DELETE for the rewritten mixed.css's URL (for 'screen').
// INSERT for the rewritten mixed.css's URL (for 'print').
// INSERT for the fetched mixed.css's partition key (for 'print').
// INSERT for the rewritten external link's URL.
// INSERT for the rewritten external link's partition key.
// HIT for the rewritten external link's URL (from the fetch done by
// ValidateRewriteExternalCss with the kExpectSuccess flag).
// So, 5 inserts, 1 delete, 3 misses, 2 hits.
EXPECT_EQ(10, lru_cache()->num_elements());
EXPECT_EQ(11, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_deletes());
EXPECT_EQ(7, lru_cache()->num_misses());
EXPECT_EQ(3, lru_cache()->num_hits());
// Now when we @import with media screen we should find cached data.
// Even though the cached data for mixed.css's URL is wrong for screen
// it doesn't matter because the data we use is accessed via its partition
// key which has the correct data for screen.
ValidateRewriteExternalCss("flatten_mixed_media_screen_repeat",
screen_in, css_screen,
kExpectSuccess | kNoClearFetcher);
// The sequence is:
// MISS for the external link's partition key.
// MISS for the external link's URL.
// INSERT for the fetched external link.
// HIT for mixed.css's partition key (for 'screen').
// INSERT for the rewritten external link's URL.
// INSERT for the rewritten external link's partition key.
// HIT for the rewritten external link's URL (from the fetch done by
// ValidateRewriteExternalCss with the kExpectSuccess flag).
// So, 3 inserts, 2 misses, 2 hit.
EXPECT_EQ(13, lru_cache()->num_elements());
EXPECT_EQ(14, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_deletes());
EXPECT_EQ(9, lru_cache()->num_misses());
EXPECT_EQ(5, lru_cache()->num_hits());
// Ditto for re-fetching print.
ValidateRewriteExternalCss("flatten_mixed_media_print_repeat",
print_in, css_print,
kExpectSuccess | kNoClearFetcher);
// The sequence is:
// MISS for the external link's partition key.
// MISS for the external link's URL.
// INSERT for the fetched external link.
// HIT for mixed.css's partition key (for 'print').
// INSERT for the rewritten external link's URL.
// INSERT for the rewritten external link's partition key.
// HIT for the rewritten external link's URL (from the fetch done by
// ValidateRewriteExternalCss with the kExpectSuccess flag).
// So, 3 inserts, 2 misses, 2 hit.
EXPECT_EQ(16, lru_cache()->num_elements());
EXPECT_EQ(17, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_deletes());
EXPECT_EQ(11, lru_cache()->num_misses());
EXPECT_EQ(7, lru_cache()->num_hits());
}
TEST_F(CssFlattenImportsTest, FlattenNestedCharsetsOk) {
// HTML = utf-8 (1st argument), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("utf-8", "", "", true);
}
TEST_F(CssFlattenImportsTest, FlattenNestedCharsetsMismatch) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"The charset of http://test.com/styles.css "
"(utf-8 from headers) is different from that of its parent "
"(inline): iso-8859-1 from unknown-->");
// HTML = iso-8859-1 (default), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("", "", "", false);
}
TEST_F(CssFlattenImportsTest, FlattenFailsIfLinkHasWrongCharset) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"The charset of the HTML (iso-8859-1, the default) "
"is different from the charset attribute "
"on the preceding element (utf-8)-->");
const char kStylesFilename[] = "styles.css";
SetResponseWithDefaultHeaders(kStylesFilename, kContentTypeCss,
kSimpleCss, 100);
const char css_in[] =
"@import url(http://test.com/styles.css);";
// TODO(sligocki): Why does this need kNoOtherContexts?
ValidateRewriteExternalCss("flatten_link_charset", css_in, css_in,
kExpectSuccess | kNoOtherContexts |
kNoClearFetcher | kLinkCharsetIsUTF8 |
kFlattenImportsCharsetMismatch);
}
TEST_F(CssFlattenImportsTest, FlattenRespectsMetaTagCharset) {
// HTML = utf-8 (2nd argument), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("", "utf-8", "", true);
}
TEST_F(CssFlattenImportsTest, FlattenRespectsHttpEquivCharset) {
// HTML = utf-8 (3rd argument), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("", "", "utf-8", true);
}
TEST_F(CssFlattenImportsTest, FlattenRespectsHttpEquivCharsetUnquoted) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"The charset of http://test.com/styles.css "
"(utf-8 from headers) is different from that of its parent "
"(inline): ISO-8859-1 from unknown-->");
// HTML = iso-8859-1 (3rd argument), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("", "", "iso-8859-1", false);
}
TEST_F(CssFlattenImportsTest, HeaderTakesPrecendenceOverMetaTag1) {
// HTML = utf-8 (1st argument), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("utf-8", "iso-8859-1", "", true);
}
TEST_F(CssFlattenImportsTest, HeaderTakesPrecendenceOverMetaTag2) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"The charset of http://test.com/styles.css "
"(utf-8 from headers) is different from that of its parent "
"(inline): iso-8859-1 from unknown-->");
// HTML = iso-8859-1 (1st argument), CSS = utf-8 (always).
TestFlattenWithHtmlCharset("iso-8859-1", "utf-8", "", false);
}
// Make sure we deal correctly with invalid URL in child.
TEST_F(CssFlattenImportsTest, InvalidGrandchildUrl) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"Invalid import URL //// in http://test.com/child.css-->");
// Invalid URL.
SetResponseWithDefaultHeaders("child.css", kContentTypeCss,
"@import url(////);", 100);
// TODO(sligocki): Why did this fail when run as ValidateRewrite()?
ValidateRewriteExternalCss("invalid_url",
"@import 'child.css';",
"@import url(child.css);",
kExpectSuccess | kNoClearFetcher |
kFlattenImportsInvalidUrl);
}
// Test that we do not flatten @imports that have complex media queries.
TEST_F(CssFlattenImportsTest, NoFlattenMediaQueries) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"Complex media queries in the @import of inline-->");
ValidateRewrite("media_queries",
// We do not flatten @imports with complex media queries.
"@import url(child.css) not screen;",
"@import url(child.css) not screen;",
kExpectSuccess | kFlattenImportsComplexQueries);
}
// Still don't flatten because child @import has complex media query.
TEST_F(CssFlattenImportsTest, NoFlattenMediaQueriesChild) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: "
"Complex media queries in the @import of inline-->");
SetResponseWithDefaultHeaders("child.css", kContentTypeCss,
"@import url(g.css) screen and (color);", 100);
// TODO(sligocki): Why did this fail when run as ValidateRewrite()?
ValidateRewriteExternalCss("invalid_url",
"@import 'child.css';",
"@import url(child.css);",
kExpectSuccess | kNoClearFetcher |
kFlattenImportsComplexQueries);
}
// See https://github.com/pagespeed/mod_pagespeed/issues/1092
TEST_F(CssFlattenImportsTest, FlattenTooComplexNested) {
GoogleString css_in = StrCat("@import url(http://test.com/",
kComplexCssFile, ");");
// First time should load the CSS files into cache.
ValidateRewriteExternalCss("flatten_too_complex_nested", css_in,
css_in, kExpectSuccess | kNoClearFetcher |
kFlattenImportsComplexQueries);
// Re-optimize. The CSS output should equal the input (and especially not be
// an empty string).
ValidateRewriteExternalCss("flatten_too_complex_nested_repeat",
css_in, css_in, kExpectSuccess |
kFlattenImportsComplexQueries | kNoClearFetcher);
}
// Test that we correctly deal with @import media types with possibly complex
// @media media queries in flattened CSS.
//
// Currently we just fail to flatten any CSS file with complex media queries.
// TODO(matterbury): Merge complex media queries for @media statements with
// simple media types allowed in @import statements.
// Note: This will require some thought and will almost certainly not be
// possible for some media queries (like those with "not"). We should only
// do this if there we think it is impacting our performance.
TEST_F(CssFlattenImportsTest, MergeMediaQueries) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: A media query "
"is too complex in http://test.com/child.css-->");
const char child_contents[] =
"@media screen and (color) { .a { color: red; } }\n"
"@media print and (max-width: 400px), only screen { .b { color: blue } }";
SetResponseWithDefaultHeaders("child.css", kContentTypeCss,
child_contents, 100);
// TODO(sligocki): Why did this fail when run as ValidateRewrite()?
ValidateRewriteExternalCss("invalid_url",
"@import url(child.css) screen;",
"@import url(child.css) screen;",
/* TODO(sligocki): This could be:
"@media screen and (color){.a{color: red}}"
"@media only screen{.b{color:#00f}}",
*/
kExpectSuccess | kNoClearFetcher |
kFlattenImportsComplexQueries);
}
// Intersections of media queries with "only" & "and" can be resolved relatively
// eaily, however it is almost impossible with "not". Ex: What is the
// intersection of "screen" and "not print and (max-width: 400px)"?
// "screen and (not-max-width: 400px)"?? We just give up with "not".
TEST_F(CssFlattenImportsTest, NoFlattenMediaQueriesAtMedia) {
// Turn on debug to get the flattening failure reason in an HTML comment.
DebugWithMessage("<!--Flattening failed: A media query "
"is too complex in http://test.com/child.css-->");
const char child_contents[] =
"@media screen and (color) { .a { color: red; } }\n"
"@media not print and (max-width: 400px) { .b { color: blue; } }\n";
SetResponseWithDefaultHeaders("child.css", kContentTypeCss,
child_contents, 100);
// TODO(sligocki): Why did this fail when run as ValidateRewrite()?
ValidateRewriteExternalCss("invalid_url",
"@import url(child.css) screen;",
"@import url(child.css) screen;",
kExpectSuccess | kNoClearFetcher |
kFlattenImportsComplexQueries);
}
TEST_F(CssFlattenImportsTest, FlattenInlineCssWithRelativeImage) {
// Proves that URLs are fixed when CSS is rewritten.
TestRelativeImageUrlInRelativeCssUrl(false, true);
}
class CssFlattenImportsOnlyTest : public CssFlattenImportsTest {
protected:
virtual void SetUpFilters() {
options()->SetRewriteLevel(RewriteOptions::kPassThrough);
options()->EnableFilter(RewriteOptions::kFlattenCssImports);
options()->set_always_rewrite_css(true);
rewrite_driver()->AddFilters();
}
};
TEST_F(CssFlattenImportsOnlyTest, FlattenInlineCssWithRelativeImage) {
// Proves that URLs are absolutified when CSS is flattened but not rewritten.
TestRelativeImageUrlInRelativeCssUrl(false, false);
TestFlattenNested(false, false);
}
TEST_F(CssFlattenImportsOnlyTest, FlattenAndTrimInlineCssWithRelativeImage) {
// Proves that URLs are fixed when CSS is flattened -and- -trimmed- but not
// rewritten.
options()->ClearSignatureForTesting();
options()->EnableFilter(RewriteOptions::kLeftTrimUrls);
server_context()->ComputeSignature(options());
TestRelativeImageUrlInRelativeCssUrl(true, false);
TestFlattenNested(true, false);
}
class CssFlattenImportsAndRewriteImagesTest : public CssFlattenImportsTest {
protected:
virtual void SetUpFilters() {
options()->SetRewriteLevel(RewriteOptions::kPassThrough);
options()->EnableFilter(RewriteOptions::kFlattenCssImports);
options()->EnableFilter(RewriteOptions::kRecompressPng);
options()->set_always_rewrite_css(true);
rewrite_driver()->AddFilters();
}
};
TEST_F(CssFlattenImportsAndRewriteImagesTest, UnauthorizedImageDomain) {
// Setup the image we refer to.
const char kFooPng[] = "http://unauth.com/images/foo.png";
const char kImageData[] = "Invalid PNG but does not matter for this test";
SetResponseWithDefaultHeaders(kFooPng, kContentTypePng, kImageData, 100);
// Setup the CSS that refers to it.
const char kSimpleCssTemplate[] =
".background_red{background-color:red}"
".foreground_yellow{color:#ff0}"
".body{background-image:url(%s)}";
// The input CSS refers to ../images/test.jpg from the file /a/b/simple.css,
// so the image's path is /a/images/test.jpg, which is what should be used
// when the CSS is flattened into the base document (with base of '/').
const GoogleString simple_css_path =
StrCat(kTestDomain, "a/b/", kSimpleCssFile);
const GoogleString simple_css_in = StringPrintf(kSimpleCssTemplate, kFooPng);
SetResponseWithDefaultHeaders(
simple_css_path, kContentTypeCss, simple_css_in, 100);
const GoogleString import_simple_css =
StrCat("@import url(", simple_css_path, ");");
DebugWithMessage(StrCat("<!--Cannot rewrite ", kFooPng,
" as it is on an unauthorized domain-->"));
ValidateRewriteExternalCss("unauthorized_image_domain",
import_simple_css, simple_css_in,
kExpectSuccess | kNoClearFetcher);
}
} // namespace
} // namespace net_instaweb