| /* |
| * 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: jmarantz@google.com (Joshua Marantz) |
| // and sligocki@google.com (Shawn Ligocki) |
| |
| #include "net/instaweb/rewriter/public/resource_manager_test_base.h" |
| |
| #include "net/instaweb/htmlparse/public/empty_html_filter.h" |
| #include "net/instaweb/rewriter/public/css_tag_scanner.h" |
| #include "net/instaweb/util/public/url_multipart_encoder.h" |
| #include "net/instaweb/util/public/url_escaper.h" |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| const char kDomain[] = "http://combine_css.test/"; |
| const char kYellow[] = ".yellow {background-color: yellow;}"; |
| const char kBlue[] = ".blue {color: blue;}\n"; |
| |
| class CssCombineFilterTest : public ResourceManagerTestBase { |
| protected: |
| // Test spriting CSS with options to write headers and use a hasher. |
| void CombineCss(const StringPiece& id, Hasher* hasher, |
| const char* barrier_text, bool is_barrier) { |
| CombineCssWithNames(id, hasher, barrier_text, is_barrier, "a.css", "b.css"); |
| } |
| |
| void CombineCssWithNames(const StringPiece& id, Hasher* hasher, |
| const char* barrier_text, bool is_barrier, |
| const char* a_css_name, |
| const char* b_css_name) { |
| resource_manager_->set_hasher(hasher); |
| other_resource_manager_.set_hasher(hasher); |
| |
| AddFilter(RewriteOptions::kCombineCss); |
| AddOtherFilter(RewriteOptions::kCombineCss); |
| |
| // URLs and content for HTML document and resources. |
| CHECK_EQ(StringPiece::npos, id.find("/")); |
| std::string html_url = StrCat(kDomain, id, ".html"); |
| std::string a_css_url = StrCat(kDomain, a_css_name); |
| std::string b_css_url = StrCat(kDomain, b_css_name); |
| std::string c_css_url = StrCat(kDomain, "c.css"); |
| |
| static const char html_input_format[] = |
| "<head>\n" |
| " <link rel='stylesheet' href='%s' type='text/css'>\n" |
| " <link rel='stylesheet' href='%s' type='text/css'>\n" |
| " <title>Hello, Instaweb</title>\n" |
| "%s" |
| "</head>\n" |
| "<body>\n" |
| " <div class=\"c1\">\n" |
| " <div class=\"c2\">\n" |
| " Yellow on Blue\n" |
| " </div>\n" |
| " </div>\n" |
| " <link rel='stylesheet' href='c.css' type='text/css'>\n" |
| "</body>\n"; |
| std::string html_input = |
| StringPrintf(html_input_format, a_css_name, b_css_name, barrier_text); |
| const char a_css_body[] = ".c1 {\n background-color: blue;\n}\n"; |
| const char b_css_body[] = ".c2 {\n color: yellow;\n}\n"; |
| const char c_css_body[] = ".c3 {\n font-weight: bold;\n}\n"; |
| |
| // Put original CSS files into our fetcher. |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(a_css_url, default_css_header, a_css_body); |
| mock_url_fetcher_.SetResponse(b_css_url, default_css_header, b_css_body); |
| mock_url_fetcher_.SetResponse(c_css_url, default_css_header, c_css_body); |
| |
| ParseUrl(html_url, html_input); |
| |
| std::string headers; |
| AppendDefaultHeaders(kContentTypeCss, resource_manager_, &headers); |
| |
| // Check for CSS files in the rewritten page. |
| StringVector css_urls; |
| CollectCssLinks(id, output_buffer_, &css_urls); |
| EXPECT_LE(1UL, css_urls.size()); |
| const std::string& combine_url = css_urls[0]; |
| |
| GURL gurl(combine_url); |
| std::string path = GoogleUrl::PathAndLeaf(gurl); |
| |
| std::string combine_filename; |
| filename_encoder_.Encode(file_prefix_, combine_url, &combine_filename); |
| |
| // Expected CSS combination. |
| // This syntax must match that in css_combine_filter |
| std::string expected_combination = StrCat(a_css_body, b_css_body); |
| if (!is_barrier) { |
| expected_combination.append(c_css_body); |
| } |
| |
| static const char expected_output_format[] = |
| "<head>\n" |
| " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">\n" |
| " \n" // The whitespace from the original link is preserved here ... |
| " <title>Hello, Instaweb</title>\n" |
| "%s" |
| "</head>\n" |
| "<body>\n" |
| " <div class=\"c1\">\n" |
| " <div class=\"c2\">\n" |
| " Yellow on Blue\n" |
| " </div>\n" |
| " </div>\n" |
| " %s\n" |
| "</body>\n"; |
| std::string expected_output = StringPrintf( |
| expected_output_format, combine_url.c_str(), barrier_text, |
| is_barrier ? "<link rel='stylesheet' href='c.css' type='text/css'>" |
| : ""); |
| EXPECT_EQ(AddHtmlBody(expected_output), output_buffer_); |
| |
| std::string actual_combination; |
| ASSERT_TRUE(file_system_.ReadFile(combine_filename.c_str(), |
| &actual_combination, &message_handler_)); |
| EXPECT_EQ(headers + expected_combination, actual_combination); |
| |
| // Fetch the combination to make sure we can serve the result from above. |
| SimpleMetaData request_headers, response_headers; |
| std::string fetched_resource_content; |
| StringWriter writer(&fetched_resource_content); |
| DummyCallback dummy_callback(true); |
| rewrite_driver_.FetchResource(combine_url, request_headers, |
| &response_headers, &writer, |
| &message_handler_, &dummy_callback); |
| EXPECT_EQ(HttpStatus::kOK, response_headers.status_code()) << combine_url; |
| EXPECT_EQ(expected_combination, fetched_resource_content); |
| |
| // Now try to fetch from another server (other_rewrite_driver_) that |
| // does not already have the combination cached. |
| // TODO(sligocki): This has too much shared state with the first server. |
| // See RewriteImage for details. |
| SimpleMetaData other_response_headers; |
| fetched_resource_content.clear(); |
| message_handler_.Message(kInfo, "Now with serving."); |
| file_system_.Enable(); |
| dummy_callback.Reset(); |
| other_rewrite_driver_.FetchResource(combine_url, request_headers, |
| &other_response_headers, &writer, |
| &message_handler_, &dummy_callback); |
| EXPECT_EQ(HttpStatus::kOK, other_response_headers.status_code()); |
| EXPECT_EQ(expected_combination, fetched_resource_content); |
| |
| // Try to fetch from an independent server. |
| ServeResourceFromManyContexts(combine_url, RewriteOptions::kCombineCss, |
| hasher, fetched_resource_content); |
| } |
| |
| // Test what happens when CSS combine can't find a previously-rewritten |
| // resource during a subsequent resource fetch. This used to segfault. |
| void CssCombineMissingResource() { |
| std::string a_css_url = StrCat(kDomain, "a.css"); |
| std::string c_css_url = StrCat(kDomain, "c.css"); |
| |
| const char a_css_body[] = ".c1 {\n background-color: blue;\n}\n"; |
| const char c_css_body[] = ".c3 {\n font-weight: bold;\n}\n"; |
| std::string expected_combination = StrCat(a_css_body, c_css_body); |
| |
| AddFilter(RewriteOptions::kCombineCss); |
| |
| // Put original CSS files into our fetcher. |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(a_css_url, default_css_header, a_css_body); |
| mock_url_fetcher_.SetResponse(c_css_url, default_css_header, c_css_body); |
| |
| // First make sure we can serve the combination of a & c. This is to avoid |
| // spurious test successes. |
| |
| std::string kACUrl = Encode(kDomain, "cc", "0", "a.css+c.css", "css"); |
| std::string kABCUrl = Encode(kDomain, "cc", "0", "a.css+bbb.css+c.css", |
| "css"); |
| SimpleMetaData request_headers, response_headers; |
| std::string fetched_resource_content; |
| StringWriter writer(&fetched_resource_content); |
| DummyCallback dummy_callback(true); |
| |
| // NOTE: This first fetch used to return status 0 because response_headers |
| // weren't initialized by the first resource fetch (but were cached |
| // correctly). Content was correct. |
| EXPECT_TRUE( |
| rewrite_driver_.FetchResource(kACUrl, request_headers, |
| &response_headers, &writer, |
| &message_handler_, &dummy_callback)); |
| EXPECT_EQ(HttpStatus::kOK, response_headers.status_code()); |
| EXPECT_EQ(expected_combination, fetched_resource_content); |
| |
| // We repeat the fetch to prove that it succeeds from cache: |
| fetched_resource_content.clear(); |
| dummy_callback.Reset(); |
| response_headers.Clear(); |
| EXPECT_TRUE( |
| rewrite_driver_.FetchResource(kACUrl, request_headers, |
| &response_headers, &writer, |
| &message_handler_, &dummy_callback)); |
| EXPECT_EQ(HttpStatus::kOK, response_headers.status_code()); |
| EXPECT_EQ(expected_combination, fetched_resource_content); |
| |
| // Now let's try fetching the url that references a missing resource |
| // (bbb.css) in addition to the two that do exist, a.css and c.css. Using |
| // an entirely non-existent resource appears to test a strict superset of |
| // filter code paths when compared with returning a 404 for the resource. |
| mock_url_fetcher_.set_fail_on_unexpected(false); |
| DummyCallback fail_callback(false); |
| fetched_resource_content.clear(); |
| response_headers.Clear(); |
| EXPECT_TRUE( |
| rewrite_driver_.FetchResource(kABCUrl, request_headers, |
| &response_headers, &writer, |
| &message_handler_, &fail_callback)); |
| EXPECT_EQ(HttpStatus::kNotFound, response_headers.status_code()); |
| EXPECT_EQ("", fetched_resource_content); |
| } |
| |
| // Representation for a CSS tag. |
| class CssLink { |
| public: |
| CssLink(const StringPiece& url, const StringPiece& content, |
| const StringPiece& media, bool supply_mock) |
| : url_(url.data(), url.size()), |
| content_(content.data(), content.size()), |
| media_(media.data(), media.size()), |
| supply_mock_(supply_mock) { |
| } |
| |
| // A vector of CssLink* should know how to accumulate and add. |
| class Vector : public std::vector<CssLink*> { |
| public: |
| ~Vector() { |
| STLDeleteElements(this); |
| } |
| |
| void Add(const StringPiece& url, const StringPiece& content, |
| const StringPiece& media, bool supply_mock) { |
| push_back(new CssLink(url, content, media, supply_mock)); |
| } |
| }; |
| |
| // Parses a combined CSS elementand provides the segments from which |
| // it came. |
| bool DecomposeCombinedUrl(std::string* base, StringVector* segments, |
| MessageHandler* handler) { |
| GURL gurl = GoogleUrl::Create(url_); |
| bool ret = false; |
| if (gurl.is_valid()) { |
| *base = GoogleUrl::AllExceptLeaf(gurl); |
| ResourceNamer namer; |
| if (namer.Decode(GoogleUrl::Leaf(gurl)) && |
| (namer.id() == "cc")) { // TODO(jmarantz): Share this literal |
| UrlEscaper escaper; |
| UrlMultipartEncoder multipart_encoder; |
| std::string segment; |
| if (escaper.DecodeFromUrlSegment(namer.name(), &segment) && |
| multipart_encoder.Decode(segment, handler)) { |
| ret = true; |
| for (int i = 0; i < multipart_encoder.num_urls(); ++i) { |
| segments->push_back(multipart_encoder.url(i)); |
| } |
| } |
| } |
| } |
| return ret; |
| } |
| |
| std::string url_; |
| std::string content_; |
| std::string media_; |
| bool supply_mock_; |
| }; |
| |
| // Helper class to collect CSS hrefs. |
| class CssCollector : public EmptyHtmlFilter { |
| public: |
| CssCollector(HtmlParse* html_parse, CssLink::Vector* css_links) |
| : css_links_(css_links), |
| css_tag_scanner_(html_parse) { |
| } |
| |
| virtual void EndElement(HtmlElement* element) { |
| HtmlElement::Attribute* href; |
| const char* media; |
| if (css_tag_scanner_.ParseCssElement(element, &href, &media)) { |
| // TODO(jmarantz): collect content of the CSS files, before and |
| // after combination, so we can diff. |
| const char* content = ""; |
| css_links_->Add(href->value(), content, media, false); |
| } |
| } |
| |
| virtual const char* Name() const { return "CssCollector"; } |
| |
| private: |
| CssLink::Vector* css_links_; |
| CssTagScanner css_tag_scanner_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CssCollector); |
| }; |
| |
| // Collects just the hrefs from CSS links into a string vector. |
| void CollectCssLinks(const StringPiece& id, const StringPiece& html, |
| StringVector* css_links) { |
| CssLink::Vector v; |
| CollectCssLinks(id, html, &v); |
| for (int i = 0, n = v.size(); i < n; ++i) { |
| css_links->push_back(v[i]->url_); |
| } |
| } |
| |
| // Collects all information about CSS links into a CssLink::Vector. |
| void CollectCssLinks(const StringPiece& id, const StringPiece& html, |
| CssLink::Vector* css_links) { |
| HtmlParse html_parse(&message_handler_); |
| CssCollector collector(&html_parse, css_links); |
| html_parse.AddFilter(&collector); |
| std::string dummy_url = StrCat("http://collect.css.links/", id, ".html"); |
| html_parse.StartParse(dummy_url); |
| html_parse.ParseText(html.data(), html.size()); |
| html_parse.FinishParse(); |
| } |
| |
| // Common framework for testing barriers. A null-terminated set of css |
| // names is specified, with optional media tags. E.g. |
| // static const char* link[] { |
| // "a.css", |
| // "styles/b.css", |
| // "print.css media=print", |
| // } |
| // |
| // The output of this function is the collected CSS links after rewrite. |
| void BarrierTestHelper( |
| const StringPiece& id, |
| const CssLink::Vector& input_css_links, |
| CssLink::Vector* output_css_links) { |
| // TODO(sligocki): Allow other domains (this is constrained right now b/c |
| // of InitMetaData. |
| std::string html_url = StrCat("http://test.com/", id, ".html"); |
| std::string html_input("<head>\n"); |
| for (int i = 0, n = input_css_links.size(); i < n; ++i) { |
| const CssLink* link = input_css_links[i]; |
| if (!link->url_.empty()) { |
| if (link->supply_mock_) { |
| // If the css-vector contains a 'true' for this, then we supply the |
| // mock fetcher with headers and content for the CSS file. |
| InitMetaData(link->url_, kContentTypeCss, link->content_, 600); |
| } |
| std::string style(" <"); |
| style += StringPrintf("link rel='stylesheet' type='text/css' href='%s'", |
| link->url_.c_str()); |
| if (!link->media_.empty()) { |
| style += StrCat(" media='", link->media_, "'"); |
| } |
| style += ">\n"; |
| html_input += style; |
| } else { |
| html_input += link->content_; |
| } |
| } |
| html_input += "</head>\n<body>\n <div class='yellow'>\n"; |
| html_input += " Hello, mod_pagespeed!\n </div>\n</body>\n"; |
| |
| AddFilter(RewriteOptions::kCombineCss); |
| ParseUrl(html_url, html_input); |
| CollectCssLinks("combine_css_missing_files", output_buffer_, |
| output_css_links); |
| |
| // TODO(jmarantz): fetch all content and provide output as text. |
| } |
| }; |
| |
| TEST_F(CssCombineFilterTest, CombineCss) { |
| CombineCss("combine_css_no_hash", &mock_hasher_, "", false); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssMD5) { |
| CombineCss("combine_css_md5", &md5_hasher_, "", false); |
| } |
| |
| |
| // http://code.google.com/p/modpagespeed/issues/detail?q=css&id=39 |
| TEST_F(CssCombineFilterTest, DealWithParams) { |
| CombineCssWithNames("deal_with_params", &mock_hasher_, "", false, |
| "a.css?U", "b.css?rev=138"); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithIEDirective) { |
| const char ie_directive_barrier[] = |
| "<!--[if IE]>\n" |
| "<link rel=\"stylesheet\" type=\"text/css\" " |
| "href=\"http://graphics8.nytimes.com/css/" |
| "0.1/screen/build/homepage/ie.css\">\n" |
| "<![endif]-->"; |
| CombineCss("combine_css_ie", &md5_hasher_, ie_directive_barrier, true); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithStyle) { |
| const char style_barrier[] = "<style>a { color: red }</style>\n"; |
| CombineCss("combine_css_style", &md5_hasher_, style_barrier, true); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithBogusLink) { |
| const char bogus_barrier[] = "<link rel='stylesheet' type='text/css' " |
| "href='crazee://big/blue/fake'>\n"; |
| CombineCss("combine_css_bogus_link", &md5_hasher_, bogus_barrier, true); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithImportInFirst) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("1.css", "@Import \"1a.css\"", "", true); |
| css_in.Add("2.css", kYellow, "", true); |
| css_in.Add("3.css", kYellow, "", true); |
| BarrierTestHelper("combine_css_with_import1", css_in, &css_out); |
| EXPECT_EQ(1, css_out.size()); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithImportInSecond) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("1.css", kYellow, "", true); |
| css_in.Add("2.css", "@Import \"2a.css\"", "", true); |
| css_in.Add("3.css", kYellow, "", true); |
| BarrierTestHelper("combine_css_with_import1", css_in, &css_out); |
| EXPECT_EQ("1.css", css_out[0]->url_); |
| EXPECT_EQ(2, css_out.size()); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithNoscriptBarrier) { |
| const char noscript_barrier[] = |
| "<noscript>\n" |
| " <link rel='stylesheet' type='text/css' href='d.css'>\n" |
| "</noscript>\n"; |
| |
| // Put this in the Test class to remove repetition here and below. |
| std::string d_css_url = StrCat(kDomain, "d.css"); |
| const char d_css_body[] = ".c4 {\n color: green;\n}\n"; |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(d_css_url, default_css_header, d_css_body); |
| |
| CombineCss("combine_css_noscript", &md5_hasher_, noscript_barrier, true); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithFakeNoscriptBarrier) { |
| const char non_barrier[] = |
| "<noscript>\n" |
| " <p>You have no scripts installed</p>\n" |
| "</noscript>\n"; |
| CombineCss("combine_css_fake_noscript", &md5_hasher_, non_barrier, false); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithMediaBarrier) { |
| const char media_barrier[] = |
| "<link rel='stylesheet' type='text/css' href='d.css' media='print'>\n"; |
| |
| std::string d_css_url = StrCat(kDomain, "d.css"); |
| const char d_css_body[] = ".c4 {\n color: green;\n}\n"; |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(d_css_url, default_css_header, d_css_body); |
| |
| CombineCss("combine_css_media", &md5_hasher_, media_barrier, true); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssWithNonMediaBarrier) { |
| // Put original CSS files into our fetcher. |
| std::string html_url = StrCat(kDomain, "no_media_barrier.html"); |
| std::string a_css_url = StrCat(kDomain, "a.css"); |
| std::string b_css_url = StrCat(kDomain, "b.css"); |
| std::string c_css_url = StrCat(kDomain, "c.css"); |
| std::string d_css_url = StrCat(kDomain, "d.css"); |
| |
| const char a_css_body[] = ".c1 {\n background-color: blue;\n}\n"; |
| const char b_css_body[] = ".c2 {\n color: yellow;\n}\n"; |
| const char c_css_body[] = ".c3 {\n font-weight: bold;\n}\n"; |
| const char d_css_body[] = ".c4 {\n color: green;\n}\n"; |
| |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(a_css_url, default_css_header, a_css_body); |
| mock_url_fetcher_.SetResponse(b_css_url, default_css_header, b_css_body); |
| mock_url_fetcher_.SetResponse(c_css_url, default_css_header, c_css_body); |
| mock_url_fetcher_.SetResponse(d_css_url, default_css_header, d_css_body); |
| |
| // Only the first two CSS files should be combined. |
| const char html_input[] = |
| "<head>\n" |
| " <link rel='stylesheet' type='text/css' href='a.css' media='print'>\n" |
| " <link rel='stylesheet' type='text/css' href='b.css' media='print'>\n" |
| " <link rel='stylesheet' type='text/css' href='c.css'>\n" |
| " <link rel='stylesheet' type='text/css' href='d.css' media='print'>\n" |
| "</head>"; |
| |
| // Rewrite |
| AddFilter(RewriteOptions::kCombineCss); |
| ParseUrl(html_url, html_input); |
| |
| // Check for CSS files in the rewritten page. |
| StringVector css_urls; |
| CollectCssLinks("combine_css_no_media-links", output_buffer_, &css_urls); |
| EXPECT_EQ(3UL, css_urls.size()); |
| const std::string& combine_url = css_urls[0]; |
| |
| const char expected_output_format[] = |
| "<head>\n" |
| " <link rel=\"stylesheet\" type=\"text/css\" media=\"print\" " |
| "href=\"%s\">\n" |
| " \n" |
| " <link rel='stylesheet' type='text/css' href='c.css'>\n" |
| " <link rel='stylesheet' type='text/css' href='d.css' media='print'>\n" |
| "</head>"; |
| std::string expected_output = StringPrintf(expected_output_format, |
| combine_url.c_str()); |
| |
| EXPECT_EQ(AddHtmlBody(expected_output), output_buffer_); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssBaseUrl) { |
| // Put original CSS files into our fetcher. |
| std::string html_url = StrCat(kDomain, "base_url.html"); |
| std::string a_css_url = StrCat(kDomain, "a.css"); |
| const char b_css_url[] = "http://other_domain.test/foo/b.css"; |
| |
| const char a_css_body[] = ".c1 {\n background-color: blue;\n}\n"; |
| const char b_css_body[] = ".c2 {\n color: yellow;\n}\n"; |
| |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(a_css_url, default_css_header, a_css_body); |
| mock_url_fetcher_.SetResponse(b_css_url, default_css_header, b_css_body); |
| |
| // Second stylesheet is on other domain. |
| const char html_input[] = |
| "<head>\n" |
| " <link rel='stylesheet' type='text/css' href='a.css'>\n" |
| " <base href='http://other_domain.test/foo/'>\n" |
| " <link rel='stylesheet' type='text/css' href='b.css'>\n" |
| "</head>\n"; |
| |
| // Rewrite |
| AddFilter(RewriteOptions::kCombineCss); |
| ParseUrl(html_url, html_input); |
| |
| // Check for CSS files in the rewritten page. |
| StringVector css_urls; |
| CollectCssLinks("combine_css_no_media-links", output_buffer_, &css_urls); |
| EXPECT_EQ(1UL, css_urls.size()); |
| const std::string& combine_url = css_urls[0]; |
| |
| const char expected_output_format[] = |
| "<head>\n" |
| " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">\n" |
| " <base href='http://other_domain.test/foo/'>\n" |
| " \n" |
| "</head>\n"; |
| std::string expected_output = StringPrintf(expected_output_format, |
| combine_url.c_str()); |
| |
| EXPECT_EQ(AddHtmlBody(expected_output), output_buffer_); |
| |
| EXPECT_TRUE(GURL(combine_url.c_str()).is_valid()); |
| } |
| |
| // TODO(jmaessen): Re-write for new sharding story when it exists. |
| // TEST_F(CssCombineFilterTest, CombineCssShards) { |
| // num_shards_ = 10; |
| // url_prefix_ = "http://mysite%d/"; |
| // CombineCss("combine_css_sha1", &mock_hasher_, "", false); |
| // } |
| |
| TEST_F(CssCombineFilterTest, CombineCssNoInput) { |
| // TODO(sligocki): This is probably not working correctly, we need to put |
| // a_broken.css and b.css in mock_fetcher_ ... not sure why this isn't |
| // crashing right now. |
| static const char html_input[] = |
| "<head>\n" |
| " <link rel='stylesheet' href='a_broken.css' type='text/css'>\n" |
| " <link rel='stylesheet' href='b.css' type='text/css'>\n" |
| "</head>\n" |
| "<body><div class=\"c1\"><div class=\"c2\"><p>\n" |
| " Yellow on Blue</p></div></div></body>"; |
| ValidateNoChanges("combine_css_missing_input", html_input); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssMissingResource) { |
| CssCombineMissingResource(); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssManyFiles) { |
| // Prepare an HTML fragment with too many CSS files to combine, |
| // exceeding the 250 char limit currently the default in rewrite_options.cc. |
| // |
| // It looks like we can fit a limited number of encodings of |
| // "yellow%d.css" in the buffer. It might be more general to base |
| // this on the constant declared in RewriteOptions but I think it's |
| // easier to understand leaving these exposed as constants; we can |
| // abstract them later. |
| const int kNumCssLinks = 31; |
| const int kNumCssInCombination = 18; // based on how we encode "yellow%d.css" |
| CssLink::Vector css_in, css_out; |
| for (int i = 0; i < kNumCssLinks; ++i) { |
| css_in.Add(StringPrintf("styles/yellow%d.css", i), |
| kYellow, "", true); |
| } |
| BarrierTestHelper("combine_css_many_files", css_in, &css_out); |
| ASSERT_EQ(2, css_out.size()); |
| |
| // Check that the first element is really a combination. |
| std::string base; |
| StringVector segments; |
| ASSERT_TRUE(css_out[0]->DecomposeCombinedUrl(&base, &segments, |
| &message_handler_)); |
| EXPECT_EQ("http://test.com/styles/", base); |
| EXPECT_EQ(kNumCssInCombination, segments.size()); |
| |
| segments.clear(); |
| ASSERT_TRUE(css_out[1]->DecomposeCombinedUrl(&base, &segments, |
| &message_handler_)); |
| EXPECT_EQ("http://test.com/styles/", base); |
| EXPECT_EQ(kNumCssLinks - kNumCssInCombination, segments.size()); |
| } |
| |
| TEST_F(CssCombineFilterTest, CombineCssManyFilesOneOrphan) { |
| // This test differs from the previous test in we have exactly one CSS file |
| // that stays on its own. |
| const int kNumCssInCombination = 18; // based on how we encode "yellow%d.css" |
| const int kNumCssLinks = kNumCssInCombination + 1; |
| CssLink::Vector css_in, css_out; |
| for (int i = 0; i < kNumCssLinks - 1; ++i) { |
| css_in.Add(StringPrintf("styles/yellow%d.css", i), |
| kYellow, "", true); |
| } |
| css_in.Add("styles/last_one.css", |
| kYellow, "", true); |
| BarrierTestHelper("combine_css_many_files", css_in, &css_out); |
| ASSERT_EQ(2, css_out.size()); |
| |
| // Check that the first element is really a combination. |
| std::string base; |
| StringVector segments; |
| ASSERT_TRUE(css_out[0]->DecomposeCombinedUrl(&base, &segments, |
| &message_handler_)); |
| EXPECT_EQ("http://test.com/styles/", base); |
| EXPECT_EQ(kNumCssInCombination, segments.size()); |
| EXPECT_EQ("styles/last_one.css", css_out[1]->url_); |
| } |
| |
| // Note -- this test is redundant with CombineCssMissingResource -- this |
| // is a taste test. This new mechanism is more code per test but I think |
| // the failures are more obvious and the expect/assert tests are in the |
| // top level of the test which might make it easier to debug. |
| TEST_F(CssCombineFilterTest, CombineCssNotCached) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("1.css", kYellow, "", true); |
| css_in.Add("2.css", kYellow, "", true); |
| css_in.Add("3.css", kYellow, "", false); |
| css_in.Add("4.css", kYellow, "", true); |
| mock_url_fetcher_.set_fail_on_unexpected(false); |
| BarrierTestHelper("combine_css_not_cached", css_in, &css_out); |
| EXPECT_EQ(3, css_out.size()); |
| std::string base; |
| StringVector segments; |
| ASSERT_TRUE(css_out[0]->DecomposeCombinedUrl(&base, &segments, |
| &message_handler_)); |
| EXPECT_EQ(2, segments.size()); |
| EXPECT_EQ("1.css", segments[0]); |
| EXPECT_EQ("2.css", segments[1]); |
| EXPECT_EQ("3.css", css_out[1]->url_); |
| EXPECT_EQ("4.css", css_out[2]->url_); |
| } |
| |
| // Note -- this test is redundant with CombineCssWithIEDirective -- this |
| // is a taste test. |
| TEST_F(CssCombineFilterTest, CombineStyleTag) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("1.css", kYellow, "", true); |
| css_in.Add("2.css", kYellow, "", true); |
| css_in.Add("", "<style>a { color: red }</style>\n", "", false); |
| css_in.Add("4.css", kYellow, "", true); |
| BarrierTestHelper("combine_css_with_style", css_in, &css_out); |
| EXPECT_EQ(2, css_out.size()); |
| std::string base; |
| StringVector segments; |
| ASSERT_TRUE(css_out[0]->DecomposeCombinedUrl(&base, &segments, |
| &message_handler_)); |
| EXPECT_EQ(2, segments.size()); |
| EXPECT_EQ("1.css", segments[0]); |
| EXPECT_EQ("2.css", segments[1]); |
| EXPECT_EQ("4.css", css_out[1]->url_); |
| } |
| |
| TEST_F(CssCombineFilterTest, NoAbsolutifySameDir) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("1.css", ".yellow {background-image: url('1.png');}\n", "", true); |
| css_in.Add("2.css", ".yellow {background-image: url('2.png');}\n", "", true); |
| BarrierTestHelper("combine_css_with_style", css_in, &css_out); |
| EXPECT_EQ(1, css_out.size()); |
| |
| // Note: the urls are not absolutified. |
| std::string expected_combination = |
| ".yellow {background-image: url('1.png');}\n" |
| ".yellow {background-image: url('2.png');}\n"; |
| |
| // Check fetched resource. |
| std::string actual_combination; |
| EXPECT_TRUE(ServeResourceUrl(css_out[0]->url_, &actual_combination)); |
| // TODO(sligocki): Check headers? |
| EXPECT_EQ(expected_combination, actual_combination); |
| } |
| |
| TEST_F(CssCombineFilterTest, DoAbsolutifyDifferentDir) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("1.css", ".yellow {background-image: url('1.png');}\n", "", true); |
| css_in.Add("foo/2.css", ".yellow {background-image: url('2.png');}\n", |
| "", true); |
| BarrierTestHelper("combine_css_with_style", css_in, &css_out); |
| EXPECT_EQ(1, css_out.size()); |
| |
| std::string expected_combination = |
| ".yellow {background-image: url('1.png');}\n" |
| ".yellow {background-image: url('http://test.com/foo/2.png');}\n"; |
| |
| // Check fetched resource. |
| std::string actual_combination; |
| EXPECT_TRUE(ServeResourceUrl(css_out[0]->url_, &actual_combination)); |
| // TODO(sligocki): Check headers? |
| EXPECT_EQ(expected_combination, actual_combination); |
| } |
| |
| // Verifies that when we combine across paths in a certain pattern we get |
| // the correct results. |
| TEST_F(CssCombineFilterTest, CrossAcrossPathsExceedingUrlSize) { |
| CssLink::Vector css_in, css_out; |
| css_in.Add("modules/acquia/fivestar/css/fivestar.css?3", "a", "", true); |
| css_in.Add("modules/node/node.css?3", "b", "", true); |
| css_in.Add("modules/poll/poll.css?3", "c", "", true); |
| css_in.Add("modules/system/defaults.css?3", "d", "", true); |
| css_in.Add("modules/system/system.css?3", "e", "", true); |
| css_in.Add("modules/system/system-menus.css?3", "f", "", true); |
| css_in.Add("modules/user/user.css?3", "g", "", true); |
| |
| // This last 'Add' causes the resolved path to change from "/modules/" to "/". |
| css_in.Add("sites/all/modules/ckeditor/ckeditor.css?3", "h", "", true); |
| BarrierTestHelper("cross_paths", css_in, &css_out); |
| EXPECT_EQ(2, css_out.size()); |
| std::string actual_combination; |
| EXPECT_TRUE(ServeResourceUrl(css_out[0]->url_, &actual_combination)); |
| GURL gurl = GoogleUrl::Create(css_out[0]->url_); |
| EXPECT_EQ("/modules/", GoogleUrl::PathSansLeaf(gurl)); |
| ResourceNamer namer; |
| ASSERT_TRUE(namer.Decode(GoogleUrl::Leaf(gurl))); |
| EXPECT_EQ("acquia,_fivestar,_css,_fivestar.css,q3+" |
| "node,_node.css,q3+" |
| "poll,_poll.css,q3+" |
| "system,_defaults.css,q3+" |
| "system,_system.css,q3+" |
| "system,_system-menus.css,q3+" |
| "user,_user.css,q3", |
| namer.name()); |
| EXPECT_EQ("abcdefg", actual_combination); |
| } |
| |
| TEST_F(CssCombineFilterTest, CrossMappedDomain) { |
| CssLink::Vector css_in, css_out; |
| DomainLawyer* laywer = options_.domain_lawyer(); |
| laywer->AddRewriteDomainMapping("a.com", "b.com", &message_handler_); |
| bool supply_mock = false; |
| css_in.Add("http://a.com/1.css", kYellow, "", supply_mock); |
| css_in.Add("http://b.com/2.css", kBlue, "", supply_mock); |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse("http://a.com/1.css", default_css_header, |
| kYellow); |
| mock_url_fetcher_.SetResponse("http://a.com/2.css", default_css_header, |
| kBlue); |
| BarrierTestHelper("combine_css_with_style", css_in, &css_out); |
| EXPECT_EQ(1, css_out.size()); |
| std::string actual_combination; |
| EXPECT_TRUE(ServeResourceUrl(css_out[0]->url_, &actual_combination)); |
| EXPECT_EQ(StrCat(kYellow, kBlue), actual_combination); |
| } |
| |
| // Verifies that we cannot do the same cross-domain combo when we lack |
| // the domain mapping. |
| TEST_F(CssCombineFilterTest, CrossUnmappedDomain) { |
| CssLink::Vector css_in, css_out; |
| DomainLawyer* laywer = options_.domain_lawyer(); |
| laywer->AddDomain("a.com", &message_handler_); |
| laywer->AddDomain("b.com", &message_handler_); |
| bool supply_mock = false; |
| const char kUrl1[] = "http://a.com/1.css"; |
| const char kUrl2[] = "http://b.com/2.css"; |
| css_in.Add(kUrl1, kYellow, "", supply_mock); |
| css_in.Add(kUrl2, kBlue, "", supply_mock); |
| SimpleMetaData default_css_header; |
| resource_manager_->SetDefaultHeaders(&kContentTypeCss, &default_css_header); |
| mock_url_fetcher_.SetResponse(kUrl1, default_css_header, kYellow); |
| mock_url_fetcher_.SetResponse(kUrl2, default_css_header, kBlue); |
| BarrierTestHelper("combine_css_with_style", css_in, &css_out); |
| EXPECT_EQ(2, css_out.size()); |
| std::string actual_combination; |
| EXPECT_EQ(kUrl1, css_out[0]->url_); |
| EXPECT_EQ(kUrl2, css_out[1]->url_); |
| } |
| |
| /* |
| TODO(jmarantz): cover intervening FLUSH |
| TODO(jmarantz): consider converting some of the existing tests to this |
| format, covering |
| IE Directive |
| @Import in any css element except the first |
| link in noscript tag |
| change in 'media' |
| incompatible domain |
| intervening inline style tag (TODO: outline first?) |
| */ |
| |
| } // namespace |
| |
| } // namespace net_instaweb |