| /* |
| * 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 "net/instaweb/rewriter/public/css_hierarchy.h" |
| |
| #include <algorithm> |
| |
| #include "net/instaweb/rewriter/public/css_minify.h" |
| #include "net/instaweb/rewriter/public/data_url_input_resource.h" |
| #include "net/instaweb/rewriter/public/resource.h" |
| #include "net/instaweb/rewriter/public/rewrite_test_base.h" |
| #include "pagespeed/kernel/base/gtest.h" |
| #include "pagespeed/kernel/base/message_handler.h" |
| #include "pagespeed/kernel/base/mock_message_handler.h" |
| #include "pagespeed/kernel/base/null_mutex.h" |
| #include "pagespeed/kernel/base/ref_counted_ptr.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/base/string_writer.h" |
| #include "pagespeed/kernel/html/html_parse_test_base.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| #include "webutil/css/parser.h" |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| static const char kTestDomain[] = "http://test.com/"; |
| |
| // The @import hierarchy is: |
| // Top |
| // +- TopChild1 |
| // +- TopChild1Child1 |
| // +- TopChild2 |
| // +- TopChild2Child1 |
| static const char kTopCss[] = |
| ".background_red{background-color:red}" |
| "@foobar { font-family: 'Magellan'; font-style: normal }" |
| ".foreground_yellow{color:#ff0}"; |
| static const char kTopChild1Css[] = |
| ".background_blue{background-color:#00f}" |
| ".foreground_gray{color:gray}"; |
| static const char kTopChild1Child1Css[] = |
| ".background_cyan{background-color:#0ff}" |
| ".foreground_pink{color:#ffc0cb}"; |
| static const char kTopChild2Css[] = |
| ".background_white{background-color:#fff}" |
| "@foobar { font-family: 'Cook'; font-style: normal }" |
| ".foreground_black{color:#000}"; |
| static const char kTopChild2Child1Css[] = |
| ".background_green{background-color:#0f0}" |
| ".foreground_rose{color:rose}"; |
| |
| } // namespace |
| |
| class CssHierarchyTest : public RewriteTestBase { |
| protected: |
| CssHierarchyTest() |
| : handler_(new NullMutex), |
| top_url_(kTestDomain), |
| top_child1_url_(top_url_, "nested1.css"), |
| top_child2_url_(top_url_, "nested2.css"), |
| top_child1_child1_url_(top_url_, "nested/nested1.css"), |
| top_child2_child1_url_(top_url_, "nested/nested2.css") { |
| } |
| |
| // Initialize our CSS contents with the given, optional, media. |
| void InitializeCss(const StringPiece top_media, |
| const StringPiece child_media); |
| |
| // Initialize a flat root - top-level CSS with no @imports. |
| void InitializeFlatRoot(CssHierarchy* top) { |
| InitializeCss("", ""); |
| top->InitializeRoot(top_url_, top_url_, flat_top_css_, |
| false /* has_unparseables */, |
| 0 /* flattened_result_limit */, NULL /* stylesheet */, |
| message_handler()); |
| } |
| |
| // Initialize a nested root - top-level CSS with @imports. |
| void InitializeNestedRoot(CssHierarchy* top) { |
| InitializeCss("", ""); |
| top->InitializeRoot(top_url_, top_url_, nested_top_css_, |
| false /* has_unparseables */, |
| 0 /* flattened_result_limit */, NULL /* stylesheet */, |
| message_handler()); |
| } |
| |
| // Initialize a nested root with the given media. |
| void InitializeNestedRootWithMedia(CssHierarchy* top, |
| const StringPiece top_media, |
| const StringPiece child_media) { |
| InitializeCss(top_media, child_media); |
| top->InitializeRoot(top_url_, top_url_, nested_top_css_, |
| false /* has_unparseables */, |
| 0 /* flattened_result_limit */, NULL /* stylesheet */, |
| message_handler()); |
| } |
| |
| // Expand the hierarchy using ExpandChildren. Expands the top then adds |
| // each child's contents and expands it, and so on for entire hierarchy. |
| void ExpandHierarchy(CssHierarchy* top); |
| |
| // Create the given number of children under the given hierarchy. |
| void ResizeChildren(CssHierarchy* top, int n) { |
| top->children().resize(n); |
| for (int i = 0; i < n; ++i) { |
| top->children()[i] = new CssHierarchy(NULL); |
| } |
| } |
| |
| // This version populates the hierarchy manually, deliberately NOT using |
| // ExpandChildren to ensure it ends up as we expect so that we can then |
| // compare against and so test ExpandChildren. |
| void PopulateHierarchy(CssHierarchy* top); |
| |
| // Are these two instances equivalent? Shallow comparison only: does not |
| // check parent and only checks that they have the same number of children. |
| bool AreEquivalent(const CssHierarchy& one, const CssHierarchy& two); |
| |
| GoogleString MakeAtImport(StringPiece url, StringPiece media) { |
| if (media.empty()) { |
| return StrCat("@import url(", url, ");"); |
| } else { |
| return StrCat("@import url(", url, ") ", media, ";"); |
| } |
| } |
| |
| MessageHandler* message_handler() { return &handler_; } |
| |
| const GoogleUrl& top_url() const { return top_url_; } |
| const GoogleString& flat_top_css() const { return flat_top_css_; } |
| const GoogleString& nested_top_css() const { return nested_top_css_; } |
| const GoogleString& nested_child1_css() const { return nested_child1_css_; } |
| const GoogleString& nested_child2_css() const { return nested_child2_css_; } |
| const GoogleString& flattened_css() const { return flattened_css_; } |
| |
| private: |
| MockMessageHandler handler_; |
| GoogleUrl top_url_; |
| GoogleUrl top_child1_url_; |
| GoogleUrl top_child2_url_; |
| GoogleUrl top_child1_child1_url_; |
| GoogleUrl top_child2_child1_url_; |
| GoogleString flat_top_css_; // top-level without any @imports. |
| GoogleString nested_top_css_; // top-level with @imports. |
| GoogleString nested_child1_css_; |
| GoogleString nested_child2_css_; |
| GoogleString flattened_css_; // Flattened version of the entire hierarchy. |
| |
| DISALLOW_COPY_AND_ASSIGN(CssHierarchyTest); |
| }; |
| |
| void CssHierarchyTest::InitializeCss(const StringPiece top_media, |
| const StringPiece child_media) { |
| if (flat_top_css_.empty()) { |
| flat_top_css_ = kTopCss; |
| nested_top_css_ = StrCat( |
| MakeAtImport(top_child1_url_.Spec(), top_media), |
| MakeAtImport(top_child2_url_.Spec(), top_media), |
| kTopCss); |
| nested_child1_css_ = StrCat( |
| MakeAtImport(top_child1_child1_url_.Spec(), child_media), |
| kTopChild1Css); |
| nested_child2_css_ = StrCat( |
| MakeAtImport(top_child2_child1_url_.Spec(), child_media), |
| kTopChild2Css); |
| flattened_css_ = StrCat(kTopChild1Child1Css, kTopChild1Css, |
| kTopChild2Child1Css, kTopChild2Css, |
| kTopCss); |
| } |
| } |
| |
| |
| void CssHierarchyTest::ExpandHierarchy(CssHierarchy* top) { |
| EXPECT_TRUE(top->Parse()); |
| EXPECT_TRUE(top->ExpandChildren()); |
| |
| GoogleString child_contents[] = { |
| nested_child1_css_, |
| nested_child2_css_ |
| }; |
| GoogleString grandchild_contents[] = { |
| kTopChild1Child1Css, |
| kTopChild2Child1Css |
| }; |
| |
| for (int i = 0, n = top->children().size(); i < n && i < 2; ++i) { |
| CssHierarchy* child = top->children()[i]; |
| if (child->NeedsRewriting()) { |
| child->set_input_contents(child_contents[i]); |
| EXPECT_TRUE(child->Parse()); |
| child->ExpandChildren(); |
| |
| if (child->children().size() > 0 && |
| child->children()[0]->NeedsRewriting()) { |
| CssHierarchy* grandchild = child->children()[0]; |
| grandchild->set_input_contents(grandchild_contents[i]); |
| EXPECT_TRUE(grandchild->Parse()); |
| EXPECT_FALSE(grandchild->ExpandChildren()); |
| } |
| } |
| } |
| } |
| |
| void CssHierarchyTest::PopulateHierarchy(CssHierarchy* top) { |
| ResizeChildren(top, 2); |
| |
| CssHierarchy* top_child1 = top->children()[0]; |
| top_child1->InitializeNested(*top, top_child1_url_); |
| top_child1->set_input_contents(nested_child1_css_); |
| ResizeChildren(top_child1, 1); |
| |
| CssHierarchy* top_child2 = top->children()[1]; |
| top_child2->InitializeNested(*top, top_child2_url_); |
| top_child2->set_input_contents(nested_child2_css_); |
| ResizeChildren(top_child2, 1); |
| |
| CssHierarchy* top_child1_child1 = top_child1->children()[0]; |
| top_child1_child1->InitializeNested(*top_child1, top_child1_child1_url_); |
| top_child1_child1->set_input_contents(kTopChild1Child1Css); |
| |
| CssHierarchy* top_child2_child1 = top_child2->children()[0]; |
| top_child2_child1->InitializeNested(*top_child2, top_child2_child1_url_); |
| top_child2_child1->set_input_contents(kTopChild2Child1Css); |
| } |
| |
| bool CssHierarchyTest::AreEquivalent(const CssHierarchy& one, |
| const CssHierarchy& two) { |
| if (one.url() != two.url()) { |
| return false; |
| } |
| if (one.css_base_url() != two.css_base_url()) { |
| return false; |
| } |
| if (one.css_trim_url() != two.css_trim_url()) { |
| return false; |
| } |
| if (one.children().size() != two.children().size()) { |
| return false; |
| } |
| if (one.input_contents() != two.input_contents()) { |
| return false; |
| } |
| if (one.minified_contents() != two.minified_contents()) { |
| return false; |
| } |
| if (one.charset() != two.charset()) { |
| return false; |
| } |
| if (one.flattening_succeeded() != two.flattening_succeeded()) { |
| return false; |
| } |
| // It would be nice to check parent_ but it's private so skip it. |
| |
| // Sigh. We need to check the stylesheet data manually. |
| const Css::Stylesheet* stylesheet_one = one.stylesheet(); |
| const Css::Stylesheet* stylesheet_two = two.stylesheet(); |
| if ((stylesheet_one == NULL && stylesheet_two != NULL) || |
| (stylesheet_one != NULL && stylesheet_two == NULL)) { |
| return false; |
| } |
| if (stylesheet_one != NULL && stylesheet_two != NULL) { |
| // The easiest way to compare two stylesheets is to textify them and |
| // compare the texts. Not inefficient but simple and effective. If either |
| // textification fails though we give up and treat the as different. |
| GoogleString text_one; |
| StringWriter writer_one(&text_one); |
| if (!CssMinify::Stylesheet(*stylesheet_one, &writer_one, &handler_)) { |
| return false; |
| } |
| GoogleString text_two; |
| StringWriter writer_two(&text_two); |
| if (!CssMinify::Stylesheet(*stylesheet_two, &writer_two, &handler_)) { |
| return false; |
| } |
| if (text_one != text_two) { |
| return false; |
| } |
| } |
| // And the same for the media though it's much easier. |
| const StringVector& media_one = one.media(); |
| const StringVector& media_two = two.media(); |
| if (media_one.size() != media_two.size() || |
| !std::equal(media_one.begin(), media_one.end(), media_two.begin())) { |
| return false; |
| } |
| return true; |
| } |
| |
| TEST_F(CssHierarchyTest, ParseFlat) { |
| CssHierarchy top(NULL); |
| |
| InitializeFlatRoot(&top); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| EXPECT_TRUE(top.Parse()); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| } |
| |
| TEST_F(CssHierarchyTest, ExpandFlat) { |
| CssHierarchy top(NULL); |
| |
| InitializeFlatRoot(&top); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| EXPECT_TRUE(top.Parse()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| EXPECT_TRUE(top.children().empty()); |
| |
| // No imports to expand => no change in these checks. |
| EXPECT_FALSE(top.ExpandChildren()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| EXPECT_TRUE(top.children().empty()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpContentsFlat) { |
| CssHierarchy top(NULL); |
| |
| InitializeFlatRoot(&top); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| top.RollUpContents(); |
| EXPECT_EQ(flat_top_css(), top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpStylesheetsFlat) { |
| CssHierarchy top(NULL); |
| |
| InitializeFlatRoot(&top); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| top.RollUpStylesheets(); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| |
| // Re-serialize stylesheet and check it matches. |
| GoogleString out_text; |
| StringWriter writer(&out_text); |
| CssMinify::Stylesheet(*top.stylesheet(), &writer, message_handler()); |
| EXPECT_EQ(flat_top_css(), out_text); |
| } |
| |
| TEST_F(CssHierarchyTest, ParseNested) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_EQ(2, top.stylesheet()->imports().size()); |
| } |
| |
| TEST_F(CssHierarchyTest, ExpandNested) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_EQ(2, top.stylesheet()->imports().size()); |
| EXPECT_EQ(2, top.children().size()); |
| |
| for (int i = 0, n = top.children().size(); i < n; ++i) { |
| CssHierarchy* child = top.children()[i]; |
| EXPECT_TRUE(NULL != child->stylesheet()); |
| EXPECT_EQ(1, child->stylesheet()->imports().size()); |
| EXPECT_EQ(1, child->children().size()); |
| |
| CssHierarchy* grandchild = child->children()[0]; |
| EXPECT_TRUE(NULL != grandchild->stylesheet()); |
| EXPECT_TRUE(grandchild->stylesheet()->imports().empty()); |
| EXPECT_TRUE(grandchild->children().empty()); |
| } |
| } |
| |
| TEST_F(CssHierarchyTest, ExpandEqualsPopulate) { |
| CssHierarchy top1(NULL); |
| CssHierarchy top2(NULL); |
| |
| InitializeNestedRoot(&top1); |
| ExpandHierarchy(&top1); |
| |
| InitializeNestedRoot(&top2); |
| PopulateHierarchy(&top2); |
| |
| // Since PopulateHierarchy doesn't parse the stylesheets, do it here so |
| // that the comparisons are fair. |
| EXPECT_TRUE(top2.Parse()); |
| EXPECT_TRUE(top2.children()[0]->Parse()); |
| EXPECT_TRUE(top2.children()[1]->Parse()); |
| EXPECT_TRUE(top2.children()[0]->children()[0]->Parse()); |
| EXPECT_TRUE(top2.children()[1]->children()[0]->Parse()); |
| |
| EXPECT_TRUE(AreEquivalent(top1, top2)); |
| } |
| |
| TEST_F(CssHierarchyTest, FailOnDirectRecursion) { |
| InitializeCss("", ""); // to initialize top_url(). |
| |
| CssHierarchy top(NULL); |
| GoogleString recursive_import = StrCat("@import '", top_url().Spec(), "' ;"); |
| top.InitializeRoot(top_url(), top_url(), recursive_import, |
| false /* has_unparseables */, |
| 0 /* flattened_result_limit */, NULL /* stylesheet */, |
| message_handler()); |
| |
| // The top-level normally doesn't have an URL so we won't catch it recursing |
| // until the grandchild level, but we -do- catch it, eventually. |
| EXPECT_TRUE(top.Parse()); |
| EXPECT_TRUE(top.ExpandChildren()); |
| EXPECT_TRUE(top.flattening_succeeded()); |
| EXPECT_FALSE(top.unparseable_detected()); |
| EXPECT_EQ(1, top.children().size()); |
| |
| CssHierarchy* child = top.children()[0]; |
| child->set_input_contents(recursive_import); |
| EXPECT_TRUE(child->NeedsRewriting()); |
| EXPECT_TRUE(child->Parse()); |
| EXPECT_FALSE(child->ExpandChildren()); |
| EXPECT_TRUE(child->flattening_succeeded()); |
| EXPECT_EQ(1, child->children().size()); |
| |
| // THIS is the one who's flattening has failed, at last. |
| CssHierarchy* grandchild = child->children()[0]; |
| EXPECT_FALSE(grandchild->flattening_succeeded()); |
| } |
| |
| TEST_F(CssHierarchyTest, FailOnIndirectRecursion) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| |
| // Manually expand the hierarchy so we can introduce recursion. |
| EXPECT_TRUE(top.Parse()); |
| EXPECT_TRUE(top.ExpandChildren()); |
| EXPECT_TRUE(top.flattening_succeeded()); |
| EXPECT_TRUE(top.unparseable_detected()); |
| |
| CssHierarchy* child1 = top.children()[0]; |
| child1->set_input_contents(nested_child1_css()); |
| EXPECT_TRUE(child1->Parse()); |
| EXPECT_TRUE(child1->ExpandChildren()); |
| EXPECT_TRUE(child1->flattening_succeeded()); |
| |
| CssHierarchy* child2 = top.children()[1]; |
| child2->set_input_contents(nested_child2_css()); |
| EXPECT_TRUE(child2->Parse()); |
| EXPECT_TRUE(child2->ExpandChildren()); |
| EXPECT_TRUE(child2->flattening_succeeded()); |
| |
| CssHierarchy* grandchild1 = child1->children()[0]; |
| grandchild1->set_input_contents(kTopChild1Child1Css); |
| EXPECT_TRUE(grandchild1->Parse()); |
| EXPECT_FALSE(grandchild1->ExpandChildren()); |
| EXPECT_TRUE(grandchild1->flattening_succeeded()); |
| |
| CssHierarchy* grandchild2 = child2->children()[0]; |
| grandchild2->set_input_contents(nested_top_css()); // Same as root so ... |
| EXPECT_TRUE(grandchild2->Parse()); |
| EXPECT_TRUE(grandchild2->ExpandChildren()); |
| EXPECT_EQ(2, grandchild2->children().size()); |
| CssHierarchy* greatgrandchild2 = grandchild2->children()[1]; |
| EXPECT_FALSE(greatgrandchild2->flattening_succeeded()); // ... should fail. |
| } |
| |
| TEST_F(CssHierarchyTest, UnparseableSection) { |
| InitializeCss("", ""); // to initialize top_url(). |
| |
| GoogleString unparseable_css = StrCat("@foobar { background: " |
| "url(", top_url().Spec(), "), ", |
| "url(", top_url().Spec(), ") }"); |
| CssHierarchy top(NULL); |
| top.InitializeRoot(top_url(), top_url(), unparseable_css, |
| false /* has_unparseables */, |
| 0 /* flattened_result_limit */, NULL /* stylesheet */, |
| message_handler()); |
| |
| // The top-level normally doesn't have an URL so we won't catch it recursing |
| // until the grandchild level, but we -do- catch it, eventually. |
| EXPECT_TRUE(top.Parse()); |
| EXPECT_TRUE(top.flattening_succeeded()); |
| EXPECT_TRUE(top.unparseable_detected()); |
| } |
| |
| TEST_F(CssHierarchyTest, ExpandElidesImportsWithNoMedia) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRootWithMedia(&top, "screen", "print"); |
| ExpandHierarchy(&top); |
| |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_EQ(2, top.stylesheet()->imports().size()); |
| EXPECT_EQ(2, top.children().size()); |
| |
| for (int i = 0, n = top.children().size(); i < n; ++i) { |
| CssHierarchy* child = top.children()[i]; |
| EXPECT_TRUE(NULL != child->stylesheet()); |
| EXPECT_EQ(1, child->stylesheet()->imports().size()); |
| EXPECT_EQ(1, child->children().size()); |
| |
| CssHierarchy* grandchild = child->children()[0]; |
| EXPECT_TRUE(NULL == grandchild->stylesheet()); |
| EXPECT_TRUE(grandchild->children().empty()); |
| EXPECT_FALSE(grandchild->NeedsRewriting()); |
| } |
| |
| top.RollUpContents(); |
| GoogleString flattened_css = StrCat( |
| StrCat("@media screen{", kTopChild1Css, "}"), |
| StrCat("@media screen{", kTopChild2Css, "}"), |
| kTopCss); |
| EXPECT_EQ(flattened_css, top.minified_contents()); |
| } |
| |
| TEST_F(CssHierarchyTest, CompatibleCharset) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| |
| // Construct a resource without a charset. |
| ResourcePtr resource( |
| DataUrlInputResource::Make("data:text/css,test", rewrite_driver())); |
| ResponseHeaders* response_headers = resource->response_headers(); |
| |
| // First check that with no charsets anywhere we match. |
| CssHierarchy* child = top.children()[0]; |
| GoogleString failure_reason; |
| EXPECT_TRUE(child->CheckCharsetOk(resource, &failure_reason)); |
| EXPECT_TRUE(failure_reason.empty()); |
| |
| // Now set both the charsets to something compatible. |
| StringPiece charset("iso-8859-1"); |
| response_headers->MergeContentType(StrCat(kContentTypeCss.mime_type(), |
| "; charset=", charset)); |
| charset.CopyToString(top.mutable_charset()); |
| EXPECT_TRUE(child->CheckCharsetOk(resource, &failure_reason)); |
| EXPECT_EQ(charset, child->charset()); |
| EXPECT_TRUE(failure_reason.empty()); |
| } |
| |
| TEST_F(CssHierarchyTest, IncompatibleCharset) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| |
| // Construct a resource with an incompatible charset. |
| ResourcePtr resource( |
| DataUrlInputResource::Make("data:text/css,test", rewrite_driver())); |
| ResponseHeaders* response_headers = resource->response_headers(); |
| response_headers->MergeContentType(StrCat(kContentTypeCss.mime_type(), |
| "; charset=utf-8")); |
| |
| StringPiece charset("iso-8859-1"); |
| charset.CopyToString(top.mutable_charset()); |
| CssHierarchy* child = top.children()[0]; |
| GoogleString failure_reason; |
| EXPECT_FALSE(child->CheckCharsetOk(resource, &failure_reason)); |
| EXPECT_EQ("utf-8", child->charset()); |
| EXPECT_EQ("The charset of http://test.com/nested1.css (utf-8 from headers) " |
| "is different from that of its parent (inline): " |
| "iso-8859-1 from unknown", failure_reason); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpContentsNested) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| |
| top.RollUpContents(); |
| EXPECT_EQ(flattened_css(), top.minified_contents()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpContentsNestedUnderLimit) { |
| // The flattening limit is so big flattening succeeds just fine. |
| CssHierarchy top(NULL); |
| InitializeNestedRoot(&top); |
| top.set_flattened_result_limit(2048L); |
| ExpandHierarchy(&top); |
| top.RollUpContents(); |
| EXPECT_TRUE(top.flattening_succeeded()); |
| EXPECT_EQ(flattened_css(), top.minified_contents()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpContentsNestedAtLimit) { |
| // The flattening limit is exactly the flattened result size, so flattening |
| // fails, and the result is the unflattened/original input. |
| CssHierarchy top(NULL); |
| InitializeNestedRoot(&top); |
| top.set_flattened_result_limit(flattened_css().size()); |
| ExpandHierarchy(&top); |
| top.RollUpContents(); |
| EXPECT_FALSE(top.flattening_succeeded()); |
| EXPECT_EQ(nested_top_css(), top.minified_contents()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpContentsNestedOverLimit) { |
| // The flattening limit is tiny so flattening fails, and the result is the |
| // unflattened/original input. |
| CssHierarchy top(NULL); |
| InitializeNestedRoot(&top); |
| top.set_flattened_result_limit(10L); |
| ExpandHierarchy(&top); |
| top.RollUpContents(); |
| EXPECT_FALSE(top.flattening_succeeded()); |
| EXPECT_EQ(nested_top_css(), top.minified_contents()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpStylesheetsNested) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| |
| top.RollUpStylesheets(); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| |
| // Re-serialize stylesheet and check it matches. |
| GoogleString out_text; |
| StringWriter writer(&out_text); |
| CssMinify::Stylesheet(*top.stylesheet(), &writer, message_handler()); |
| EXPECT_EQ(flattened_css(), out_text); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpStylesheetsNestedWithoutRollUpContents) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| PopulateHierarchy(&top); // ExpandHierarchy does too much. |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| top.RollUpStylesheets(); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_EQ(2, top.stylesheet()->imports().size()); // 2 => unflattened => bad. |
| |
| // Re-serialize stylesheet and check it matches. |
| GoogleString out_text; |
| StringWriter writer(&out_text); |
| CssMinify::Stylesheet(*top.stylesheet(), &writer, message_handler()); |
| EXPECT_EQ(nested_top_css(), out_text); // unchanged => unflattened => bad |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpStylesheetsNestedWithChildrenRollUpContents) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| PopulateHierarchy(&top); // ExpandHierarchy does too much. |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| // Per the contract, make sure our CSS is already parsed. |
| EXPECT_TRUE(top.Parse()); |
| |
| // Roll up all the children's contents manually. This is the contract so |
| // we test that here. Later we roll up our own contents and test that case. |
| for (int i = 0, n = top.children().size(); i < n; ++i) { |
| top.children()[i]->RollUpContents(); |
| } |
| |
| top.RollUpStylesheets(); |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| |
| // Re-serialize stylesheet and check it matches. |
| GoogleString out_text; |
| StringWriter writer(&out_text); |
| CssMinify::Stylesheet(*top.stylesheet(), &writer, message_handler()); |
| EXPECT_EQ(flattened_css(), out_text); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpStylesheetsNestedAfterRollUpContents) { |
| CssHierarchy top(NULL); |
| |
| InitializeNestedRoot(&top); |
| PopulateHierarchy(&top); // ExpandHierarchy does too much. |
| EXPECT_EQ("", top.minified_contents()); |
| EXPECT_TRUE(NULL == top.stylesheet()); |
| |
| // Roll up our own contents which should manually roll-up all our children's |
| // thereby meeting the contract for RollUpStylesheets(). This implicitly |
| // parses our CSS so no need to do it explicitly. |
| top.RollUpContents(); |
| EXPECT_EQ(flattened_css(), top.minified_contents()); |
| |
| top.RollUpStylesheets(); |
| EXPECT_TRUE(NULL != top.stylesheet()); |
| EXPECT_TRUE(top.stylesheet()->imports().empty()); |
| |
| // Re-serialize stylesheet and check it matches. |
| GoogleString out_text; |
| StringWriter writer(&out_text); |
| CssMinify::Stylesheet(*top.stylesheet(), &writer, message_handler()); |
| EXPECT_EQ(flattened_css(), out_text); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpContentsKeepsDebugMessages) { |
| CssHierarchy top(NULL); |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| |
| // Inject a log message into one of the to-be-rolled-up descendents. |
| CssHierarchy* grandchild = top.children()[0]->children()[0]; |
| grandchild->AddFlatteningFailureReason("Nothing to see here!"); |
| |
| // Take this opportunity to also test that we don't add a new reason if |
| // its text is already in the failure reason. |
| grandchild->AddFlatteningFailureReason("But there is here!"); // Added. |
| grandchild->AddFlatteningFailureReason("Nothing to see here!"); // Ignored. |
| grandchild->AddFlatteningFailureReason("But there is here!"); // Ignored. |
| grandchild->AddFlatteningFailureReason("Nothing"); // Ignored. |
| grandchild->AddFlatteningFailureReason("here!"); // Ignored. |
| |
| top.RollUpContents(); |
| EXPECT_TRUE(top.flattening_succeeded()); |
| EXPECT_STREQ("Nothing to see here! AND But there is here!", |
| top.flattening_failure_reason()); |
| } |
| |
| TEST_F(CssHierarchyTest, RollUpStylesheetsKeepsDebugMessages) { |
| CssHierarchy top(NULL); |
| InitializeNestedRoot(&top); |
| ExpandHierarchy(&top); |
| |
| // Inject a log message into one of the to-be-rolled-up descendents. |
| CssHierarchy* grandchild = top.children()[0]->children()[0]; |
| grandchild->AddFlatteningFailureReason("Nothing to see here!"); |
| |
| top.RollUpStylesheets(); |
| EXPECT_TRUE(top.flattening_succeeded()); |
| EXPECT_STREQ("Nothing to see here!", top.flattening_failure_reason()); |
| } |
| |
| } // namespace net_instaweb |