text: /*
   * Copyright 2013 Google Inc.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  // Author: mmohabey@google.com (Megha Mohabey)
  //         pulkitg@google.com (Pulkit Goyal)
  
  // Unit-tests for CacheHtmlFlow.
  
  #include "pagespeed/automatic/proxy_interface_test_base.h"
  
  #include "base/logging.h"
  #include "net/instaweb/http/public/log_record.h"
  #include "net/instaweb/http/public/logging_proto_impl.h"
  #include "net/instaweb/http/public/mock_callback.h"
  #include "net/instaweb/http/public/request_context.h"
  #include "net/instaweb/public/global_constants.h"
  #include "net/instaweb/rewriter/public/blink_util.h"
  #include "net/instaweb/rewriter/public/cache_html_info_finder.h"
  #include "net/instaweb/rewriter/public/critical_css_filter.h"
  #include "net/instaweb/rewriter/public/critical_selector_finder.h"
  #include "net/instaweb/rewriter/public/delay_images_filter.h"
  #include "net/instaweb/rewriter/public/flush_early_info_finder_test_base.h"
  #include "net/instaweb/rewriter/public/js_disable_filter.h"
  #include "net/instaweb/rewriter/public/mock_critical_css_finder.h"
  #include "net/instaweb/rewriter/public/rewrite_driver.h"
  #include "net/instaweb/rewriter/public/rewrite_options.h"
  #include "net/instaweb/rewriter/public/server_context.h"
  #include "net/instaweb/rewriter/public/static_asset_manager.h"
  #include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
  #include "net/instaweb/rewriter/public/url_namer.h"
  #include "net/instaweb/util/public/cache_property_store.h"
  #include "net/instaweb/util/public/mock_property_page.h"
  #include "net/instaweb/util/public/property_cache.h"
  #include "pagespeed/automatic/cache_html_flow.h"
  #include "pagespeed/automatic/proxy_fetch.h"
  #include "pagespeed/automatic/proxy_interface.h"
  #include "pagespeed/kernel/base/basictypes.h"
  #include "pagespeed/kernel/base/dynamic_annotations.h"
  #include "pagespeed/kernel/base/gtest.h"
  #include "pagespeed/kernel/base/mock_hasher.h"
  #include "pagespeed/kernel/base/mock_message_handler.h"
  #include "pagespeed/kernel/base/mock_timer.h"
  #include "pagespeed/kernel/base/null_message_handler.h"
  #include "pagespeed/kernel/base/ref_counted_ptr.h"
  #include "pagespeed/kernel/base/scoped_ptr.h"
  #include "pagespeed/kernel/base/string.h"
  #include "pagespeed/kernel/base/string_util.h"
  #include "pagespeed/kernel/base/thread_system.h"
  #include "pagespeed/kernel/base/time_util.h"
  #include "pagespeed/kernel/base/timer.h"
  #include "pagespeed/kernel/base/wildcard.h"
  #include "pagespeed/kernel/cache/delay_cache.h"
  #include "pagespeed/kernel/cache/lru_cache.h"
  #include "pagespeed/kernel/html/html_parse_test_base.h"
  #include "pagespeed/kernel/http/content_type.h"
  #include "pagespeed/kernel/http/google_url.h"
  #include "pagespeed/kernel/http/http_names.h"
  #include "pagespeed/kernel/http/http_options.h"
  #include "pagespeed/kernel/http/request_headers.h"
  #include "pagespeed/kernel/http/response_headers.h"
  #include "pagespeed/kernel/http/user_agent_matcher.h"
  #include "pagespeed/kernel/http/user_agent_matcher_test_base.h"
  #include "pagespeed/kernel/thread/mock_scheduler.h"
  #include "pagespeed/kernel/thread/thread_synchronizer.h"
  #include "pagespeed/kernel/thread/worker_test_base.h"
  
  namespace net_instaweb {
  
  class AbstractMutex;
  class AsyncFetch;
  class Statistics;
  
  namespace {
  
  const char kTestUrl[] = "http://test.com/text.html";
  
  const char kMockHashValue[] = "MDAwMD";
  
  const char kCssContent[] = "* { display: none; }";
  
  const char kSampleJpgFile[] = "Sample.jpg";
  
  const char kChromeLinuxUserAgent[] =
      "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
      "(KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5";
  
  const char kWindowsUserAgent[] =
      "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 "
      "Firefox/15.0a1";
  
  const char kBlackListUserAgent[] =
      "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/2.0a1";
  
  const char kWhitespace[] = "                  ";
  
  const char kHtmlInput[] =
      "<html>"
      "<head>"
      "</head>"
      "<body>\n"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>"
      "</body></html>";
  
  const char kHtmlInputWithMinifiableJs[] =
      "<html>"
      "<head>"
      "<script type=\"text/javascript\">var a = \"hello\"; </script>"
      "</head>"
      "<body>\n"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>"
      "</body></html>";
  
  const char kHtmlInputWithMinifiedJs[] =
      "<html>"
      "<head>"
      "<script data-pagespeed-orig-type=\"text/javascript\" "
      "type=\"text/psajs\" orig_index=\"0\">var a=\"hello\";</script>"
      "</head>"
      "<body>\n"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>"
      "%s<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\"></script>"
      "</body></html>";
  
  const char kHtmlInputWithExtraCommentAndNonCacheable[] =
      "<html>"
      "<head>"
      "</head>"
      "<body>\n"
      "<!-- Hello -->"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>"
      "</body></html>";
  
  const char kHtmlInputWithExtraAttribute[] =
      "<html>"
      "<head>"
      "</head>"
      "<body>\n"
      "<div id=\"header\" align=\"center\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>"
      "</body></html>";
  
  const char kHtmlInputWithEmptyVisiblePortions[] =
      "<html><body></body></html>";
  
  const char kSmallHtmlInput[] =
      "<html><head></head><body>A small test html.</body></html>";
  const char kHtmlInputForNoBlink[] =
      "<html><head></head><body></body></html>";
  
  const char kBlinkOutputCommon[] =
      "<html><head></head><body>"
      "<noscript><meta HTTP-EQUIV=\"refresh\" content=\"0;"
      "url='%s?PageSpeed=noscript'\" />"
      "<style><!--table,div,span,font,p{display:none} --></style>"
      "<div style=\"display:block\">Please click "
      "<a href=\"%s?PageSpeed=noscript\">here</a> "
      "if you are not redirected within a few seconds.</div></noscript>"
      "\n<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
      "<!--GooglePanel begin panel-id-1.0-->"
      "<!--GooglePanel end panel-id-1.0-->"
      "<!--GooglePanel begin panel-id-0.0-->"
      "<!--GooglePanel end panel-id-0.0-->"
      "<!--GooglePanel begin panel-id-0.1-->"
      "<!--GooglePanel end panel-id-0.1-->"
      "</div>"
      "</body></html>"
      "%s<script type=\"text/javascript\" src=\"/psajs/blink.0.js\"></script>"
      "<script type=\"text/javascript\">"
      "pagespeed.panelLoaderInit();</script>\n"
      "<script type=\"text/javascript\">"
      "pagespeed.panelLoader.setRequestFromInternalIp();</script>\n";
  
  const char kCookieScript[] =
      "<script>pagespeed.panelLoader.loadCookies([\"helo=world; path=/\"]);"
      "</script>";
  
  const char kBlinkOutputSuffix[] =
      "<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-1.0\":{\"instance_html\":\"<h2 id=\\\"beforeItems\\\"> This is before Items </h2>\",\"xpath\":\"//div[@id=\\\"container\\\"]/h2[1]\"}}\n);</script>"  // NOLINT
      "<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-0.0\":{\"instance_html\":\"<div class=\\\"item\\\"><img src=\\\"%s\\\"><img src=\\\"image2\\\"></div>\",\"xpath\":\"//div[@id=\\\"container\\\"]/div[2]\"}}\n);</script>"  // NOLINT
      "<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-0.1\":{\"instance_html\":\"<div class=\\\"item\\\"><img src=\\\"image3\\\"><div class=\\\"item\\\"><img src=\\\"image4\\\"></div></div>\",\"xpath\":\"//div[@id=\\\"container\\\"]/div[3]\"}}\n);</script>"  // NOLINT
      "<script>pagespeed.panelLoader.bufferNonCriticalData({});</script>";  // NOLINT
  
  const char kBlinkOutputWithCacheablePanelsNoCookiesSuffix[] =
      "<script>pagespeed.panelLoader.bufferNonCriticalData();</script>\n"
      "</body></html>\n";
  
  const char kBlinkOutputWithCacheablePanelsCookiesSuffix[] =
      "</body></html>\n";
  
  const char kFakePngInput[] = "FakePng";
  
  const char kFlushSubresourcesHtmlInput[] =
      "<html>"
      "<head>"
      "<link rel=\"stylesheet\" type=\"text/css\" href=\"1.css\">"
      "</head>"
      "<body>\n"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</body></html>";
  
  const char kNoBlinkUrl[] =
      "http://test.com/noblink_text.html?PageSpeed=noscript";
  
  const char kNoScriptTextUrl[] = "http://test.com/text.html?PageSpeed=noscript";
  
  // Like ExpectStringAsyncFetch but for asynchronous invocation -- it lets
  // one specify a WorkerTestBase::SyncPoint to help block until completion.
  class AsyncExpectStringAsyncFetch : public ExpectStringAsyncFetch {
   public:
    AsyncExpectStringAsyncFetch(bool expect_success,
                                WorkerTestBase::SyncPoint* notify,
                                const RequestContextPtr& request_context)
        : ExpectStringAsyncFetch(expect_success, request_context),
          notify_(notify) {
    }
  
    virtual ~AsyncExpectStringAsyncFetch() {}
  
    virtual void HandleDone(bool success) {
      ExpectStringAsyncFetch::HandleDone(success);
      notify_->Notify();
    }
  
   private:
    WorkerTestBase::SyncPoint* notify_;
    DISALLOW_COPY_AND_ASSIGN(AsyncExpectStringAsyncFetch);
  };
  
  class ProxyInterfaceWithDelayCache : public ProxyInterface {
   public:
    ProxyInterfaceWithDelayCache(const StringPiece& hostname, int port,
                                 ServerContext* manager, Statistics* stats,
                                 DelayCache* delay_cache,
                                 TestRewriteDriverFactory* factory)
        : ProxyInterface(hostname, port, manager, stats),
          manager_(manager),
          delay_cache_(delay_cache),
          key_(""),
          factory_(factory) {
    }
  
    // Initiates the PropertyCache look up.
    virtual ProxyFetchPropertyCallbackCollector* InitiatePropertyCacheLookup(
        bool is_resource_fetch,
        const GoogleUrl& request_url,
        RewriteOptions* options,
        AsyncFetch* async_fetch,
        const bool requires_blink_cohort) {
      GoogleString options_signature_hash;
      if (options != NULL) {
        manager_->ComputeSignature(options);
        options_signature_hash =
            manager_->GetRewriteOptionsSignatureHash(options);
      }
      PropertyCache* pcache = manager_->page_property_cache();
      const PropertyCache::Cohort* cohort =
          pcache->GetCohort(BlinkUtil::kBlinkCohort);
      key_ = factory_->cache_property_store()->CacheKey(
          request_url.Spec(),
          options_signature_hash,
          UserAgentMatcher::DeviceTypeSuffix(UserAgentMatcher::kDesktop),
          cohort);
      delay_cache_->DelayKey(key_);
      return ProxyFetchFactory::InitiatePropertyCacheLookup(
          is_resource_fetch, request_url, server_context_, options, async_fetch,
          requires_blink_cohort);
    }
  
    const GoogleString& key() const { return key_; }
  
   private:
    ServerContext* manager_;
    DelayCache* delay_cache_;
    GoogleString key_;
    TestRewriteDriverFactory* factory_;
  
    DISALLOW_COPY_AND_ASSIGN(ProxyInterfaceWithDelayCache);
  };
  
  }  // namespace
  
  // RequestContext that overrides NewSubordinateLogRecord to return a
  // CopyOnWriteLogRecord that copies to a logging_info given at construction
  // time.
  class TestRequestContext : public RequestContext {
   public:
    TestRequestContext(ThreadSystem* threads, LoggingInfo* logging_info)
        : RequestContext(kDefaultHttpOptionsForTests, threads->NewMutex(), NULL),
          logging_info_copy_(logging_info) {}
  
    virtual AbstractLogRecord* NewSubordinateLogRecord(
        AbstractMutex* logging_mutex) {
      return new CopyOnWriteLogRecord(logging_mutex, logging_info_copy_);
    }
  
   private:
    LoggingInfo* logging_info_copy_;  // Not owned by us.
  
    DISALLOW_COPY_AND_ASSIGN(TestRequestContext);
  };
  typedef RefCountedPtr<TestRequestContext> TestRequestContextPtr;
  
  // TODO(nikhilmadan): Test cookies, fetch failures, 304 responses etc.
  // TODO(nikhilmadan): Test 304 responses etc.
  class CacheHtmlFlowTest : public ProxyInterfaceTestBase {
   protected:
    static const int kHtmlCacheTimeSec = 5000;
  
    CacheHtmlFlowTest() : test_request_context_(TestRequestContextPtr(
        new TestRequestContext(server_context()->thread_system(),
                               &cache_html_logging_info_))) {
      ConvertTimeToString(MockTimer::kApr_5_2010_ms, &start_time_string_);
    }
  
    // These must be run prior to the calls to 'new CustomRewriteDriverFactory'
    // in the constructor initializer above.  Thus the calls to Initialize() in
    // the base class are too late.
    static void SetUpTestCase() {
      RewriteOptions::Initialize();
    }
    static void TearDownTestCase() {
      RewriteOptions::Terminate();
    }
  
    void InitializeOutputs(RewriteOptions* options) {
      blink_output_partial_ = StringPrintf(
          kBlinkOutputCommon, kTestUrl, kTestUrl,
          GetJsDisableScriptSnippet(options).c_str());
      blink_output_ = StrCat(blink_output_partial_, kCookieScript,
                             StringPrintf(kBlinkOutputSuffix, "image1"));
      noblink_output_ = StrCat("<html><head></head><body>",
                               StringPrintf(kNoScriptRedirectFormatter,
                                            kNoBlinkUrl, kNoBlinkUrl),
                               "</body></html>");
      blink_output_with_cacheable_panels_no_cookies_ =
          StrCat(StringPrintf(kBlinkOutputCommon, "http://test.com/flaky.html",
                              "http://test.com/flaky.html",
                              GetJsDisableScriptSnippet(options).c_str()),
                 kBlinkOutputWithCacheablePanelsNoCookiesSuffix);
      blink_output_with_cacheable_panels_cookies_ =
          StrCat(StringPrintf(kBlinkOutputCommon, "http://test.com/cache.html",
                              "http://test.com/cache.html",
                              GetJsDisableScriptSnippet(options).c_str()),
                 kBlinkOutputWithCacheablePanelsCookiesSuffix);
    }
  
    GoogleString GetJsDisableScriptSnippet(RewriteOptions* options) {
      if (options->enable_defer_js_experimental()) {
        return StrCat("<script type=\"text/javascript\" data-pagespeed-no-defer>",
                      JsDisableFilter::kEnableJsExperimental,
                      "</script>");
      } else {
        return "";
      }
    }
  
    virtual void SetUp() {
      const PropertyCache::Cohort* blink_cohort =
          SetupCohort(server_context_->page_property_cache(),
                      BlinkUtil::kBlinkCohort);
      server_context_->set_blink_cohort(blink_cohort);
      server_context_->set_enable_property_cache(true);
      InitHasher();
      ThreadSynchronizer* sync = server_context()->thread_synchronizer();
      sync->EnableForPrefix(CacheHtmlFlow::kBackgroundComputationDone);
      sync->EnableForPrefix(ProxyFetch::kCollectorFinish);
      sync->AllowSloppyTermination(CacheHtmlFlow::kBackgroundComputationDone);
      sync->AllowSloppyTermination(ProxyFetch::kCollectorFinish);
      flush_early_info_finder_ = new MeaningfulFlushEarlyInfoFinder;
      server_context()->set_flush_early_info_finder(flush_early_info_finder_);
      options_.reset(server_context()->NewOptions());
      options_->EnableFilter(RewriteOptions::kCachePartialHtml);
      options_->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
      options_->EnableFilter(RewriteOptions::kRewriteJavascriptInline);
      options_->set_non_cacheables_for_cache_partial_html(
          "class=item,id=beforeItems");
  
      options_->Disallow("*blacklist*");
  
      InitializeOutputs(options_.get());
      SetRewriteOptions(options_.get());
  
      server_context()->ComputeSignature(options_.get());
  
      ProxyInterfaceTestBase::SetUp();
      ProxyInterface::InitStats(statistics());
      proxy_interface_.reset(
          new ProxyInterface("localhost", 80, server_context(), statistics()));
  
      server_context()->url_namer()->set_proxy_domain("http://proxy-domain");
  
      server_context()->set_cache_html_info_finder(new CacheHtmlInfoFinder());
  
      SetTimeMs(MockTimer::kApr_5_2010_ms);
      SetFetchFailOnUnexpected(false);
  
      response_headers_.SetStatusAndReason(HttpStatus::kOK);
      response_headers_.Add(HttpAttributes::kContentType,
                            kContentTypePng.mime_type());
      SetFetchResponse("http://test.com/test.png", response_headers_,
                       kFakePngInput);
      response_headers_.Remove(HttpAttributes::kContentType,
                               kContentTypePng.mime_type());
  
      response_headers_.SetStatusAndReason(HttpStatus::kNotFound);
      response_headers_.Add(HttpAttributes::kContentType,
                            kContentTypeText.mime_type());
      SetFetchResponse("http://test.com/404.html", response_headers_,
                       kHtmlInput);
  
      response_headers_.SetStatusAndReason(HttpStatus::kOK);
      response_headers_.SetDateAndCaching(MockTimer::kApr_5_2010_ms,
                                         1 * Timer::kSecondMs);
      response_headers_.ComputeCaching();
      SetFetchResponse("http://test.com/plain.html", response_headers_,
                       kHtmlInput);
  
      SetFetchResponse("http://test.com/blacklist.html", response_headers_,
                       kHtmlInput);
  
      response_headers_.Replace(HttpAttributes::kContentType,
                                "text/html; charset=utf-8");
      response_headers_.Add(HttpAttributes::kSetCookie, "helo=world; path=/");
      SetFetchResponse("http://test.com/text.html", response_headers_,
                       kHtmlInput);
      SetFetchResponse("http://test.com/minifiable_text.html", response_headers_,
                       kHtmlInputWithMinifiableJs);
      SetFetchResponse("http://test.com/smalltest.html", response_headers_,
                       kSmallHtmlInput);
      SetFetchResponse("http://test.com/noblink_text.html", response_headers_,
                       kHtmlInputForNoBlink);
      SetFetchResponse("https://test.com/noblink_text.html", response_headers_,
                       kHtmlInputForNoBlink);
      SetFetchResponse("http://test.com/cache.html", response_headers_,
                       kHtmlInput);
      SetFetchResponse("http://test.com/non_html.html", response_headers_,
                       kFakePngInput);
      SetFetchResponse("http://test.com/ws_text.html", response_headers_,
                       StrCat(kWhitespace, kHtmlInput));
      SetFetchResponse("http://test.com/flush_subresources.html",
                       response_headers_, kFlushSubresourcesHtmlInput);
      SetResponseWithDefaultHeaders(StrCat(kTestDomain, "1.css"), kContentTypeCss,
                                    kCssContent, kHtmlCacheTimeSec * 2);
      AddFileToMockFetcher(StrCat(kTestDomain, "image1"), kSampleJpgFile,
                           kContentTypeJpeg, 100);
    }
  
    virtual void InitHasher() {
      UseMd5Hasher();
    }
  
    virtual RequestContextPtr CreateRequestContext() {
      return RequestContextPtr(test_request_context_);
    }
  
    void InitializeExperimentSpec() {
      options_->set_running_experiment(true);
      NullMessageHandler handler;
      ASSERT_TRUE(options_->AddExperimentSpec("id=3;percent=100;default",
                                              &handler));
    }
  
    void GetDefaultRequestHeaders(RequestHeaders* request_headers) {
      // Request from an internal ip.
      request_headers->Add(HttpAttributes::kUserAgent, kChromeLinuxUserAgent);
      request_headers->Add(HttpAttributes::kAccept, "image/webp");
      request_headers->Add(HttpAttributes::kXForwardedFor, "127.0.0.1");
      request_headers->Add(HttpAttributes::kXGoogleRequestEventId,
                           "1345815119391831");
    }
  
    void FetchFromProxyWaitForBackground(const StringPiece& url,
                                         bool expect_success,
                                         GoogleString* string_out,
                                         ResponseHeaders* headers_out) {
      FetchFromProxy(url, expect_success, string_out, headers_out, true);
    }
    void FetchFromProxyWaitForBackground(const StringPiece& url,
                                         bool expect_success,
                                         const RequestHeaders& request_headers,
                                         GoogleString* string_out,
                                         ResponseHeaders* headers_out,
                                         GoogleString* user_agent_out,
                                         bool wait_for_background_computation) {
    FetchFromProxy(url, expect_success, request_headers, string_out,
                   headers_out, user_agent_out,
                   wait_for_background_computation);
    }
  
    void VerifyNonCacheHtmlResponse(const ResponseHeaders& response_headers) {
      ConstStringStarVector values;
      EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kCacheControl,
                                           &values));
      EXPECT_EQ(2, values.size());
      EXPECT_STREQ("max-age=0", *(values[0]));
      EXPECT_STREQ("no-cache", *(values[1]));
    }
  
    void VerifyCacheHtmlResponse(const ResponseHeaders& response_headers) {
      EXPECT_STREQ("OK", response_headers.reason_phrase());
      EXPECT_STREQ(start_time_string_,
                   response_headers.Lookup1(HttpAttributes::kDate));
      ConstStringStarVector v;
      EXPECT_STREQ("text/html; charset=utf-8",
                   response_headers.Lookup1(HttpAttributes::kContentType));
      EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kCacheControl, &v));
      EXPECT_EQ("max-age=0", *v[0]);
      EXPECT_EQ("private", *v[1]);
      EXPECT_EQ("no-cache", *v[2]);
    }
  
    void FetchFromProxyNoWaitForBackground(const StringPiece& url,
                                           bool expect_success,
                                           GoogleString* string_out,
                                           ResponseHeaders* headers_out) {
      FetchFromProxy(url, expect_success, string_out, headers_out, false);
    }
  
    void FetchFromProxy(const StringPiece& url,
                        bool expect_success,
                        GoogleString* string_out,
                        ResponseHeaders* headers_out,
                        bool wait_for_background_computation) {
      RequestHeaders request_headers;
      GetDefaultRequestHeaders(&request_headers);
      FetchFromProxy(url, expect_success, request_headers,
                     string_out, headers_out, wait_for_background_computation);
    }
  
    void FetchFromProxy(const StringPiece& url,
                        bool expect_success,
                        const RequestHeaders& request_headers,
                        GoogleString* string_out,
                        ResponseHeaders* headers_out,
                        bool wait_for_background_computation) {
      FetchFromProxy(url, expect_success, request_headers,
                     string_out, headers_out, NULL,
                     wait_for_background_computation);
    }
  
    void FetchFromProxy(const StringPiece& url,
                          bool expect_success,
                          const RequestHeaders& request_headers,
                          GoogleString* string_out,
                          ResponseHeaders* headers_out,
                          GoogleString* user_agent_out,
                          bool wait_for_background_computation) {
      FetchFromProxy(url,
                     expect_success,
                     request_headers,
                     string_out,
                     headers_out,
                     NULL,
                     wait_for_background_computation,
                     true);
    }
  
    void FetchFromProxy(const StringPiece& url,
                        bool expect_success,
                        const RequestHeaders& request_headers,
                        GoogleString* string_out,
                        ResponseHeaders* headers_out,
                        GoogleString* user_agent_out,
                        bool wait_for_background_computation,
                        bool proxy_fetch_property_callback_collector_created) {
      FetchFromProxyNoQuiescence(url, expect_success, request_headers,
                                 string_out, headers_out, user_agent_out);
      if (proxy_fetch_property_callback_collector_created) {
        ThreadSynchronizer* thread_synchronizer =
            server_context()->thread_synchronizer();
        thread_synchronizer->Wait(ProxyFetch::kCollectorFinish);
      }
      if (wait_for_background_computation) {
        ThreadSynchronizer* sync = server_context()->thread_synchronizer();
        sync->Wait(CacheHtmlFlow::kBackgroundComputationDone);
      }
    }
  
    void FetchFromProxyNoQuiescence(const StringPiece& url,
                                    bool expect_success,
                                    const RequestHeaders& request_headers,
                                    GoogleString* string_out,
                                    ResponseHeaders* headers_out) {
      FetchFromProxyNoQuiescence(url, expect_success, request_headers,
                                 string_out, headers_out, NULL);
    }
  
    void FetchFromProxyNoQuiescence(const StringPiece& url,
                                    bool expect_success,
                                    const RequestHeaders& request_headers,
                                    GoogleString* string_out,
                                    ResponseHeaders* headers_out,
                                    GoogleString* user_agent_out) {
      WorkerTestBase::SyncPoint sync(server_context()->thread_system());
      AsyncExpectStringAsyncFetch callback(
          expect_success, &sync, rewrite_driver()->request_context());
      callback.set_response_headers(headers_out);
      callback.request_headers()->CopyFrom(request_headers);
      proxy_interface_->Fetch(AbsolutifyUrl(url), message_handler(), &callback);
      CHECK(server_context()->thread_synchronizer() != NULL);
      sync.Wait();
      EXPECT_TRUE(callback.done());
  
      *string_out = callback.buffer();
      if (user_agent_out != NULL &&
          callback.request_headers()->Lookup1(HttpAttributes::kUserAgent)
          != NULL) {
        user_agent_out->assign(
            callback.request_headers()->Lookup1(HttpAttributes::kUserAgent));
      }
    }
  
    void FetchFromProxyWithDelayCache(
        const StringPiece& url, bool expect_success,
        const RequestHeaders& request_headers,
        ProxyInterfaceWithDelayCache* proxy_interface,
        GoogleString* string_out,
        ResponseHeaders* headers_out) {
      WorkerTestBase::SyncPoint sync(server_context()->thread_system());
      AsyncExpectStringAsyncFetch callback(
          expect_success, &sync, rewrite_driver()->request_context());
      callback.set_response_headers(headers_out);
      callback.request_headers()->CopyFrom(request_headers);
      proxy_interface->Fetch(AbsolutifyUrl(url), message_handler(), &callback);
      CHECK(server_context()->thread_synchronizer() != NULL);
      delay_cache()->ReleaseKey(proxy_interface->key());
      sync.Wait();
      EXPECT_TRUE(callback.done());
      *string_out = callback.buffer();
      ThreadSynchronizer* ts = server_context()->thread_synchronizer();
      ts->Wait(CacheHtmlFlow::kBackgroundComputationDone);
      mock_scheduler()->AwaitQuiescence();
    }
  
    void CheckHeaders(const ResponseHeaders& headers,
                      const ContentType& expect_type) {
      ASSERT_TRUE(headers.has_status_code());
      EXPECT_EQ(HttpStatus::kOK, headers.status_code());
      EXPECT_STREQ(expect_type.mime_type(),
                   headers.Lookup1(HttpAttributes::kContentType));
    }
  
    // Verifies the fields of CacheHtmlFlow Info proto being logged.
    CacheHtmlLoggingInfo* VerifyCacheHtmlLoggingInfo(
        int cache_html_request_flow, const char* url) {
      CacheHtmlLoggingInfo* cache_html_logging_info =
          cache_html_logging_info_.mutable_cache_html_logging_info();
      EXPECT_EQ(cache_html_request_flow,
                cache_html_logging_info->cache_html_request_flow());
      EXPECT_EQ("1345815119391831",
                cache_html_logging_info->request_event_id_time_usec());
      EXPECT_STREQ(url, cache_html_logging_info->url());
      return cache_html_logging_info;
    }
  
    CacheHtmlLoggingInfo* VerifyCacheHtmlLoggingInfo(
        int cache_html_request_flow, bool html_match, const char* url) {
      CacheHtmlLoggingInfo* cache_html_logging_info =
          VerifyCacheHtmlLoggingInfo(cache_html_request_flow, url);
      EXPECT_EQ(html_match, cache_html_logging_info->html_match());
      return cache_html_logging_info;
    }
  
    void VerifyBlacklistUserAgent(const ResponseHeaders& response_headers) {
      ConstStringStarVector v;
      EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kCacheControl, &v));
      EXPECT_STREQ("text/plain",
                   response_headers.Lookup1(HttpAttributes::kContentType));
      EXPECT_STREQ("max-age=1", *v[0]);
    }
  
    void VerifyFlushSubresourcesResponse(GoogleString text,
                                         bool is_applied_expected) {
      // If FlushSubresources Filter is applied then the response has
      // rel="subresource".
      bool is_applied = false;
      const char pattern[] = "rel=\"stylesheet\"";
      int pattern_position = text.find(pattern);
      if (pattern_position != GoogleString::npos) {
        is_applied = true;
      }
      EXPECT_EQ(is_applied_expected, is_applied);
    }
  
    void UnEscapeString(GoogleString* str) {
      GlobalReplaceSubstring("__psa_lt;", "<", str);
      GlobalReplaceSubstring("__psa_gt;", ">", str);
    }
  
    // TODO(nikhilmadan): This is super fragile as RewriteTestBase also has
    // an options_ member.
    scoped_ptr<RewriteOptions> options_;
    GoogleString start_time_string_;
  
    void SetFetchHtmlResponseWithStatus(const char* url,
                                        HttpStatus::Code status) {
      ResponseHeaders response_headers;
      response_headers.SetStatusAndReason(status);
      response_headers.Add(HttpAttributes::kContentType, "text/html");
      SetFetchResponse(url, response_headers, kHtmlInput);
    }
  
    void CheckStats(int diff_matches, int diff_mismatches,
                    int smart_diff_matches, int smart_diff_mismatches,
                    int hits, int misses) {
      EXPECT_EQ(diff_matches, TimedValue(CacheHtmlFlow::kNumCacheHtmlMatches));
      EXPECT_EQ(diff_mismatches, TimedValue(
          CacheHtmlFlow::kNumCacheHtmlMismatches));
      EXPECT_EQ(smart_diff_matches, TimedValue(
          CacheHtmlFlow::kNumCacheHtmlSmartdiffMatches));
      EXPECT_EQ(smart_diff_mismatches, TimedValue(
          CacheHtmlFlow::kNumCacheHtmlSmartdiffMismatches));
      EXPECT_EQ(hits, TimedValue(CacheHtmlFlow::kNumCacheHtmlHits));
      EXPECT_EQ(misses, TimedValue(CacheHtmlFlow::kNumCacheHtmlMisses));
    }
  
    void TestCacheHtmlChangeDetection(bool use_smart_diff) {
      options_->ClearSignatureForTesting();
      options_->set_enable_blink_html_change_detection(true);
      server_context()->ComputeSignature(options_.get());
      GoogleString text;
      ResponseHeaders response_headers;
  
      // Hashes not set. Results in mismatches.
      FetchFromProxyWaitForBackground("text.html", true, &text,
                                      &response_headers);
      VerifyCacheHtmlLoggingInfo(
          CacheHtmlLoggingInfo::CACHE_HTML_MISS_TRIGGERED_REWRITE, false,
          "http://test.com/text.html");
      // Diff Match: 0, Diff Mismatch: 0,
      // Smart Diff Match: 0, Smart Diff Mismatch: 0
      // Hits: 0, Misses: 1
      CheckStats(0, 0, 0, 0, 0, 1);
      ClearStats();
      response_headers.Clear();
      // Hashes set. No mismatches.
      FetchFromProxyWaitForBackground("text.html", true, &text,
                                      &response_headers);
      // Diff Match: 1, Diff Mismatch: 0,
      // Smart Diff Match: 1, Smart Diff Mismatch: 0
      // Hits: 1, Misses: 0
      CheckStats(1, 0, 1, 0, 1, 0);
      VerifyCacheHtmlResponse(response_headers);
      UnEscapeString(&text);
      EXPECT_STREQ(blink_output_, text);
      VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, true,
                                 "http://test.com/text.html");
      ClearStats();
      response_headers.Clear();
  
      // Input with an extra comment. We strip out comments before taking hash,
      // so there should be no mismatches.
      SetFetchResponse(kTestUrl, response_headers_,
                       kHtmlInputWithExtraCommentAndNonCacheable);
      FetchFromProxyWaitForBackground("text.html", true, &text,
                                      &response_headers);
      // Diff Match: 1, Diff Mismatch: 0,
      // Smart Diff Match: 1, Smart Diff Mismatch: 0
      // Hits: 1, Misses: 0
      CheckStats(1, 0, 1, 0, 1, 0);
      VerifyCacheHtmlResponse(response_headers);
      UnEscapeString(&text);
      EXPECT_STREQ(blink_output_, text);
      VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, true,
                                 "http://test.com/text.html");
      ClearStats();
      response_headers.Clear();
  
      // Input with extra attributes. This should result in a mismatch with
      // full-diff but a match with smart-diff.
      SetFetchResponse(kTestUrl, response_headers_,
                       kHtmlInputWithExtraAttribute);
      FetchFromProxyWaitForBackground("text.html", true, &text,
                                      &response_headers);
      VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, false,
                                 "http://test.com/text.html");
      // Diff Match: 0, Diff Mismatch: 1,
      // Smart Diff Match: 1, Smart Diff Mismatch: 0
      // Hits: 1, Misses: 0
      CheckStats(0, 1, 1, 0, 1, 0);
      ClearStats();
  
      // Input with empty visible portions. Diff calculation should not trigger.
      SetFetchResponse(kTestUrl, response_headers_,
                       kHtmlInputWithEmptyVisiblePortions);
      FetchFromProxyWaitForBackground("text.html", true, &text,
                                      &response_headers);
      // Diff Match: 0, Diff Mismatch: 1,
      // Smart Diff Match: 0, Smart Diff Mismatch: 1
      // Hits: 1, Misses: 0
      CheckStats(0, 1, 0, 1, 1, 0);
    }
  
    GoogleString GetImageOnloadScriptBlock() const {
      return StrCat("<script data-pagespeed-no-defer type=\"text/javascript\">",
                    DelayImagesFilter::kImageOnloadJsSnippet,
                    "</script>");
    }
  
    LoggingInfo cache_html_logging_info_;
    ResponseHeaders response_headers_;
    GoogleString noblink_output_;
    GoogleString noblink_output_with_lazy_load_;
    GoogleString blink_output_with_lazy_load_;
    GoogleString blink_output_partial_;
    GoogleString blink_output_;
    GoogleString blink_output_with_cacheable_panels_cookies_;
    GoogleString blink_output_with_cacheable_panels_no_cookies_;
    MeaningfulFlushEarlyInfoFinder* flush_early_info_finder_;
    TestRequestContextPtr test_request_context_;
    MockCriticalCssFinder* critical_css_finder_;
  
   private:
    DISALLOW_COPY_AND_ASSIGN(CacheHtmlFlowTest);
  };
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlCacheMiss) {
    GoogleString text;
    ResponseHeaders response_headers;
    FetchFromProxyWaitForBackground("minifiable_text.html", true, &text,
                                    &response_headers);
    ConstStringStarVector values;
    EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kSetCookie, &values));
    EXPECT_EQ(1, values.size());
    VerifyNonCacheHtmlResponse(response_headers);
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::CACHE_HTML_MISS_TRIGGERED_REWRITE, false,
        "http://test.com/minifiable_text.html");
    EXPECT_STREQ(StringPrintf(
        kHtmlInputWithMinifiedJs, GetJsDisableScriptSnippet(
            options_.get()).c_str()), text);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlCacheMissAndHit) {
    GoogleString text;
    ResponseHeaders response_headers;
    // First request updates the property cache with cached html.
    FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
    VerifyNonCacheHtmlResponse(response_headers);
    EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::CACHE_HTML_MISS_TRIGGERED_REWRITE, false,
        "http://test.com/text.html");
    CheckStats(0, 0, 0, 0, 0, 1);
    ClearStats();
    // Cache Html hit case.
    response_headers.Clear();
    FetchFromProxyNoWaitForBackground("text.html", true, &text,
                                      &response_headers);
    VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, false,
                               "http://test.com/text.html");
    CheckStats(0, 0, 0, 0, 1, 0);
    ClearStats();
    VerifyCacheHtmlResponse(response_headers);
    UnEscapeString(&text);
    EXPECT_STREQ(blink_output_, text);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlChangeDetection) {
    TestCacheHtmlChangeDetection(false);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlChangeDetectionWithSmartDiffOn) {
    TestCacheHtmlChangeDetection(true);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlMissExperimentSetCookie) {
    options_->ClearSignatureForTesting();
    options_->set_experiment_cookie_duration_ms(1000);
    SetTimeMs(MockTimer::kApr_5_2010_ms);
    InitializeExperimentSpec();
    server_context()->ComputeSignature(options_.get());
    GoogleString text;
    ResponseHeaders response_headers;
  
    FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
  
    ConstStringStarVector values;
    EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kSetCookie, &values));
    ASSERT_EQ(2, values.size());
    EXPECT_STREQ("PageSpeedExperiment=3", (*(values[1])).substr(0, 21));
    GoogleString expires_str;
    ConvertTimeToString(MockTimer::kApr_5_2010_ms + 1000, &expires_str);
    EXPECT_NE(GoogleString::npos, ((*(values[1])).find(expires_str)));
    VerifyNonCacheHtmlResponse(response_headers);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlHitExperimentSetCookie) {
    options_->ClearSignatureForTesting();
    InitializeExperimentSpec();
    server_context()->ComputeSignature(options_.get());
    GoogleString text;
    ResponseHeaders response_headers;
  
    // Populate the property cache in first request.
    FetchFromProxyWaitForBackground("text.html", true, &text,
                                    &response_headers);
  
    response_headers.Clear();
    FetchFromProxyNoWaitForBackground("text.html", true, &text,
                                      &response_headers);
  
    ConstStringStarVector values;
    EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kSetCookie, &values));
    EXPECT_EQ(1, values.size());
    EXPECT_STREQ("PageSpeedExperiment=3", (*(values[0])).substr(0, 21));
    VerifyCacheHtmlResponse(response_headers);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlExperimentCookieHandling) {
    options_->ClearSignatureForTesting();
    InitializeExperimentSpec();
    server_context()->ComputeSignature(options_.get());
    GoogleString text;
    ResponseHeaders response_headers;
    RequestHeaders request_headers;
    GetDefaultRequestHeaders(&request_headers);
    request_headers.Add(HttpAttributes::kCookie, "PageSpeedExperiment=3");
  
    // Populate the property cache in first request.
    FetchFromProxyWaitForBackground("text.html", true, &text,
                                    &response_headers);
  
    response_headers.Clear();
    FetchFromProxy("text.html", true, request_headers,
                   &text, &response_headers, false);
  
    EXPECT_FALSE(response_headers.Has(HttpAttributes::kSetCookie));
    VerifyCacheHtmlResponse(response_headers);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlCacheHitWithInlinePreviewImages) {
    const char kInlinePreviewHtmlInput[] =
        "<html>"
        "<head>"
        "</head>"
        "<body>\n"
        "<div id=\"header\"> This is the header </div>"
        "<div id=\"container\" class>"
          "<h2 id=\"beforeItems\"> This is before Items </h2>"
          "<div class=\"item1\">"
             "<img src=\"image1\">"
             "<img src=\"image2\">"
          "</div>"
          "<div class=\"item\">"
             "<img src=\"image3\">"
              "<div class=\"item\">"
                 "<img src=\"image4\">"
              "</div>"
          "</div>"
        "</div>"
        "</body></html>";
    SetFetchResponse("http://test.com/text.html", response_headers_,
                     kInlinePreviewHtmlInput);
  
    StringSet* critical_images = new StringSet;
    critical_images->insert(StrCat(kTestDomain, "image1"));
    SetCriticalImagesInFinder(critical_images);
    options_->ClearSignatureForTesting();
    options_->EnableFilter(RewriteOptions::kDelayImages);
    server_context()->ComputeSignature(options_.get());
  
    GoogleString text;
    ResponseHeaders response_headers;
    // First request updates the property cache with cached html.
    FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
    VerifyNonCacheHtmlResponse(response_headers);
    EXPECT_EQ(-1, logging_info()->num_html_critical_images());
    EXPECT_EQ(-1, logging_info()->num_css_critical_images());
    // Cache Html hit case.
    response_headers.Clear();
    FetchFromProxyNoWaitForBackground("text.html", true, &text,
                                      &response_headers);
  
    VerifyCacheHtmlResponse(response_headers);
    UnEscapeString(&text);
  
    const char kBlinkOutputWithInlinePreviewImages[] =
        "<html><head></head><body>"
        "<noscript><meta HTTP-EQUIV=\"refresh\" content=\"0;"
        "url='%s?PageSpeed=noscript'\" />"
        "<style><!--table,div,span,font,p{display:none} --></style>"
        "<div style=\"display:block\">Please click "
        "<a href=\"%s?PageSpeed=noscript\">here</a> "
        "if you are not redirected within a few seconds.</div></noscript>"
        "\n<div id=\"header\"> This is the header </div>"
        "<div id=\"container\" class>"
        "<!--GooglePanel begin panel-id-1.0-->"
        "<!--GooglePanel end panel-id-1.0-->"
        "<div class=\"item1\">%s"  // Inlined Image tag with script.
        "%s"  // image-onload js snippet.
        "<img src=\"image2\">"
        "</div>"
        "<!--GooglePanel begin panel-id-0.0-->"
        "<!--GooglePanel end panel-id-0.0-->"
        "</div>"
        "</body></html>"
        "%s<script type=\"text/javascript\" src=\"/psajs/blink.0.js\"></script>"
        "<script type=\"text/javascript\">"
        "pagespeed.panelLoaderInit();</script>\n"
        "<script type=\"text/javascript\">"
        "pagespeed.panelLoader.setRequestFromInternalIp();</script>\n"
        "%s"  // kCookieScript
        "<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-1.0\":{\"instance_html\":\"<h2 id=\\\"beforeItems\\\"> This is before Items </h2>\",\"xpath\":\"//div[@id=\\\"container\\\"]/h2[1]\"}}\n);</script>"  // NOLINT
        "<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-0.0\":{\"instance_html\":\"<div class=\\\"item\\\"><img src=\\\"image3\\\"><div class=\\\"item\\\"><img src=\\\"image4\\\"></div></div>\",\"xpath\":\"//div[@id=\\\"container\\\"]/div[3]\"}}\n);</script>"  // NOLINT
        "<script>pagespeed.panelLoader.bufferNonCriticalData({});</script>";  // NOLINT
  
    GoogleString inlined_image_wildcard =
        StringPrintf(kBlinkOutputWithInlinePreviewImages, kTestUrl, kTestUrl,
                    GetImageOnloadScriptBlock().c_str(),
                     "<img data-pagespeed-high-res-src=\"image1\" "
                     "src=\"data:image/jpeg;base64*",
                     GetJsDisableScriptSnippet(options_.get()).c_str(),
                     kCookieScript);
    EXPECT_TRUE(Wildcard(inlined_image_wildcard).Match(text))
        << "Expected:\n" << inlined_image_wildcard << "\n\nGot:\n" << text;
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlOverThreshold) {
    options_->ClearSignatureForTesting();
    // Content type is more than the limit to buffer in secondary fetch.
    int64 size_of_small_html = arraysize(kSmallHtmlInput) - 1;
    int64 html_buffer_threshold = size_of_small_html - 1;
    options_->ClearSignatureForTesting();
    options_->set_blink_max_html_size_rewritable(html_buffer_threshold);
    server_context()->ComputeSignature(options_.get());
    GoogleString text;
    ResponseHeaders response_headers;
    FetchFromProxyWaitForBackground(
        "smalltest.html", true, &text, &response_headers);
  
    GoogleString SmallHtmlOutput =
        StrCat("<html><head></head><body>A small test html.",
               GetJsDisableScriptSnippet(options_.get()),
               "<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
               "</script></body></html>");
    EXPECT_STREQ(SmallHtmlOutput, text);
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::FOUND_CONTENT_LENGTH_OVER_THRESHOLD,
        "http://test.com/smalltest.html");
    // 1 Miss for original plain text,
    // 1 Miss for Blink Cohort.
    EXPECT_EQ(2, lru_cache()->num_misses());
  
    CheckStats(0, 0, 0, 0, 0, 1);
    ClearStats();
    text.clear();
    response_headers.Clear();
    options_->ClearSignatureForTesting();
    html_buffer_threshold = size_of_small_html + 1;
    options_->set_blink_max_html_size_rewritable(html_buffer_threshold);
    server_context()->ComputeSignature(options_.get());
  
    FetchFromProxyWaitForBackground(
        "smalltest.html", true, &text, &response_headers);
    EXPECT_EQ(2, lru_cache()->num_misses());
  
    CheckStats(0, 0, 0, 0, 0, 1);
    ClearStats();
    text.clear();
    response_headers.Clear();
    options_->ClearSignatureForTesting();
    FetchFromProxyNoWaitForBackground(
        "smalltest.html", true, &text, &response_headers);
    CheckStats(0, 0, 0, 0, 1, 0);
    EXPECT_EQ(1, lru_cache()->num_misses());
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlHeaderOverThreshold) {
    options_->ClearSignatureForTesting();
    InitializeExperimentSpec();
    int64 size_of_small_html = arraysize(kSmallHtmlInput) - 1;
    int64 html_buffer_threshold = size_of_small_html;
    options_->ClearSignatureForTesting();
    options_->set_blink_max_html_size_rewritable(html_buffer_threshold);
    server_context()->ComputeSignature(options_.get());
  
    GoogleString text;
    ResponseHeaders response_headers;
    // Setting a higher content length to verify if the header's content length
    // is checked before rewriting.
    response_headers.Add(HttpAttributes::kContentLength,
                         IntegerToString(size_of_small_html + 1));
    response_headers.SetStatusAndReason(HttpStatus::kOK);
    response_headers.Add(HttpAttributes::kContentType,
                         "text/html; charset=utf-8");
    SetFetchResponse("http://test.com/smalltest.html", response_headers,
                     kSmallHtmlInput);
    FetchFromProxyWaitForBackground(
        "smalltest.html", true, &text, &response_headers);
  
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::FOUND_CONTENT_LENGTH_OVER_THRESHOLD,
        "http://test.com/smalltest.html");
    // 1 Miss for original plain text,
    // 1 Miss for Blink Cohort.
    EXPECT_EQ(2, lru_cache()->num_misses());
    EXPECT_EQ(0, lru_cache()->num_hits());
    EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  }
  
  TEST_F(CacheHtmlFlowTest, Non200StatusCode) {
    GoogleString text;
    ResponseHeaders response_headers;
    FetchFromProxyWaitForBackground("404.html", true, &text, &response_headers);
    EXPECT_STREQ(kHtmlInput, text);
    EXPECT_STREQ("text/plain",
                 response_headers.Lookup1(HttpAttributes::kContentType));
    VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_MISS_FETCH_NON_OK,
                               "http://test.com/404.html");
    // 1 Miss for original plain text,
    // 1 Miss for Blink Cohort.
    EXPECT_EQ(2, lru_cache()->num_misses());
    CheckStats(0, 0, 0, 0, 0, 1);
  }
  
  TEST_F(CacheHtmlFlowTest, NonHtmlContent) {
    // Content type is non html.
    GoogleString text;
    ResponseHeaders response_headers;
    FetchFromProxyNoWaitForBackground(
        "plain.html", true, &text, &response_headers);
  
    EXPECT_STREQ(kHtmlInput, text);
    EXPECT_STREQ("text/plain",
                 response_headers.Lookup1(HttpAttributes::kContentType));
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::CACHE_HTML_MISS_FOUND_RESOURCE,
        "http://test.com/plain.html");
    // 1 Miss for Blink Cohort.
    EXPECT_EQ(2, lru_cache()->num_misses());
    EXPECT_EQ(0, lru_cache()->num_hits());
    EXPECT_EQ(1, lru_cache()->num_inserts());
  
    CheckStats(0, 0, 0, 0, 0, 1);
    ClearStats();
    text.clear();
    response_headers.Clear();
  
    FetchFromProxyNoWaitForBackground(
        "plain.html", true, &text, &response_headers);
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::CACHE_HTML_MISS_FOUND_RESOURCE,
        "http://test.com/plain.html");
    // 1 Miss for Blink Cohort.
    CheckStats(0, 0, 0, 0, 0, 1);
    EXPECT_EQ(1, lru_cache()->num_misses());
    EXPECT_EQ(1, lru_cache()->num_hits());
    EXPECT_EQ(0, lru_cache()->num_inserts());
  
    // Content type is html but the actual content is non html.
    FetchFromProxyNoWaitForBackground(
        "non_html.html", true, &text, &response_headers);
    FetchFromProxyWaitForBackground(
        "non_html.html", true, &text, &response_headers);
    VerifyCacheHtmlLoggingInfo(
        CacheHtmlLoggingInfo::CACHE_HTML_MISS_FOUND_RESOURCE,
        "http://test.com/non_html.html");
    CheckStats(0, 0, 0, 0, 0, 3);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlWithWebp) {
    if (RunningOnValgrind()) {
      return;
    }
    rewrite_driver_->server_context()->set_hasher(factory_->mock_hasher());
    AddFileToMockFetcher(StrCat(kTestDomain, "image1"), "Puzzle.jpg",
                         kContentTypeJpeg, 100);
    options_->ClearSignatureForTesting();
    options_->EnableFilter(RewriteOptions::kConvertJpegToWebp);
    server_context()->ComputeSignature(options_.get());
    GoogleString text;
    ResponseHeaders response_headers;
    // First request updates the property cache with cached html.
    FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
    VerifyNonCacheHtmlResponse(response_headers);
    ClearStats();
    // Cache Html hit case.
    response_headers.Clear();
    FetchFromProxyNoWaitForBackground("text.html", true, &text,
                                      &response_headers);
    ClearStats();
    VerifyCacheHtmlResponse(response_headers);
    UnEscapeString(&text);
    GoogleString correct_url =
        Encode("", RewriteOptions::kImageCompressionId, "0", "image1", "webp");
  
    GoogleString blink_output_with_webp =
        StrCat(blink_output_partial_, kCookieScript,
               StringPrintf(kBlinkOutputSuffix, correct_url.c_str()));
    EXPECT_STREQ(blink_output_with_webp, text);
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlWithHttpsUrl) {
    GoogleString text;
    ResponseHeaders response_headers;
    RequestHeaders request_headers;
    GetDefaultRequestHeaders(&request_headers);
  
    FetchFromProxy("https://test.com/noblink_text.html", true, request_headers,
                   &text, &response_headers, false);
    EXPECT_STREQ(
        StrCat("<html><head></head><body>",
               GetJsDisableScriptSnippet(options_.get()),
               "<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
               "</script></body></html>"), text);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlWithWhitespace) {
    GoogleString text;
    ResponseHeaders response_headers;
    FetchFromProxyWaitForBackground(
        "ws_text.html", true, &text, &response_headers);
    EXPECT_EQ(0, TimedValue(CacheHtmlFlow::kNumCacheHtmlHits));
    EXPECT_EQ(1, TimedValue(CacheHtmlFlow::kNumCacheHtmlMisses));
    EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlushSubresources) {
    // FlushSubresources is applied when blink is enabled and user agent
    // does not support blink.
    GoogleString text;
    RequestHeaders request_headers;
    request_headers.Replace(HttpAttributes::kUserAgent,
                            "prefetch_link_rel_subresource");
    ResponseHeaders response_headers;
    FetchFromProxy("http://test.com/flush_subresources.html"
                   "?PageSpeedFilters=+extend_cache_css,-inline_css", true,
                   request_headers, &text, &response_headers, NULL, false);
    VerifyNonCacheHtmlResponse(response_headers);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  
    // Requesting again.
    flush_early_info_finder_->Clear();
    response_headers.Clear();
    FetchFromProxy("http://test.com/flush_subresources.html"
                   "?PageSpeedFilters=+extend_cache_css,-inline_css", true,
                   request_headers, &text, &response_headers, NULL, false);
    VerifyFlushSubresourcesResponse(text, true);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowUrlCacheInvalidation) {
    GoogleString text;
    ResponseHeaders response_headers;
    GoogleString htmlOutput = StrCat(
      "<html><head></head>"
      "<body>\n"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>",
      GetJsDisableScriptSnippet(options_.get()),
      "<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\"></script>"
      "</body></html>");
  
    FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
    EXPECT_STREQ(htmlOutput, text);
  
    // Cache lookup for original plain text and Blink Cohort
    // all miss.
    // ie., 1 + 1 (Blink Cohort).
    EXPECT_EQ(2, lru_cache()->num_misses());
    EXPECT_EQ(0, lru_cache()->num_hits());
    EXPECT_EQ(1, lru_cache()->num_inserts());
    EXPECT_EQ(0, lru_cache()->num_deletes());
    EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
    response_headers.Clear();
    ClearStats();
  
    // Property cache hit.
    FetchFromProxyNoWaitForBackground(
        "text.html", true, &text, &response_headers);
    UnEscapeString(&text);
    EXPECT_STREQ(blink_output_, text);
    // 1 Miss for original plain text
    EXPECT_EQ(1, lru_cache()->num_misses());
    EXPECT_EQ(1, lru_cache()->num_hits());
    EXPECT_EQ(0, lru_cache()->num_inserts());
    EXPECT_EQ(0, lru_cache()->num_deletes());
    EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
    ClearStats();
  
    // Invalidate the cache for some URL other than 'text.html'.
    options_->ClearSignatureForTesting();
    options_->AddUrlCacheInvalidationEntry(
        AbsolutifyUrl("foo.bar"), timer()->NowMs(), true);
    server_context()->ComputeSignature(options_.get());
  
    // Property cache hit.
    FetchFromProxyNoWaitForBackground(
        "text.html", true, &text, &response_headers);
    UnEscapeString(&text);
    EXPECT_STREQ(blink_output_, text);
    // 1 Miss for original plain text
    EXPECT_EQ(1, lru_cache()->num_misses());
    EXPECT_EQ(1, lru_cache()->num_hits());
    EXPECT_EQ(0, lru_cache()->num_inserts());
    EXPECT_EQ(0, lru_cache()->num_deletes());
    EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
    ClearStats();
  
    // Invalidate the cache.
    options_->ClearSignatureForTesting();
    options_->AddUrlCacheInvalidationEntry(
        AbsolutifyUrl("text.html"), timer()->NowMs(), true);
    server_context()->ComputeSignature(options_.get());
  
    // Property cache hit, but invalidated.  Hence treated as a miss and
    // passthrough by blink.
    FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
  
    EXPECT_STREQ(htmlOutput, text);
    // 1 Miss for original plain text
    EXPECT_EQ(1, lru_cache()->num_misses());
    EXPECT_EQ(1, lru_cache()->num_hits());
    EXPECT_EQ(0, lru_cache()->num_inserts());
    EXPECT_EQ(0, lru_cache()->num_deletes());
    EXPECT_EQ(1, lru_cache()->num_identical_reinserts());  // identical insert
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowWithHeadRequest) {
    GoogleString text;
    ResponseHeaders response_headers;
    RequestHeaders request_headers;
    request_headers.Add(HttpAttributes::kUserAgent, kChromeLinuxUserAgent);
    request_headers.set_method(RequestHeaders::kHead);
    FetchFromProxy("text.html", true, request_headers,
                   &text, &response_headers, false);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowDataMissDelayCache) {
    GoogleString text;
    ResponseHeaders response_headers;
    ProxyInterfaceWithDelayCache* proxy_interface =
        new ProxyInterfaceWithDelayCache("localhost", 80,
                                         server_context(), statistics(),
                                         delay_cache(), factory());
    proxy_interface_.reset(proxy_interface);
    RequestHeaders request_headers;
    GetDefaultRequestHeaders(&request_headers);
    FetchFromProxyWithDelayCache(
        "text.html", true, request_headers, proxy_interface,
        &text, &response_headers);
    GoogleString htmlOutput = StrCat(
      "<html><head></head>"
      "<body>\n"
      "<div id=\"header\"> This is the header </div>"
      "<div id=\"container\" class>"
        "<h2 id=\"beforeItems\"> This is before Items </h2>"
        "<div class=\"item\">"
           "<img src=\"image1\">"
           "<img src=\"image2\">"
        "</div>"
        "<div class=\"item\">"
           "<img src=\"image3\">"
            "<div class=\"item\">"
               "<img src=\"image4\">"
            "</div>"
        "</div>"
      "</div>",
      GetJsDisableScriptSnippet(options_.get()),
      "<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\"></script>"
      "</body></html>");
  
    EXPECT_STREQ(htmlOutput, text);
  
    EXPECT_STREQ("text/html; charset=utf-8",
                 response_headers.Lookup1(HttpAttributes::kContentType));
  
    // 1 Miss for original plain text,
    // 1 miss for BlinkCohort
    VerifyNonCacheHtmlResponse(response_headers);
    EXPECT_EQ(2, lru_cache()->num_misses());
    EXPECT_EQ(0, lru_cache()->num_hits());
    EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  }
  
  TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowWithDifferentUserAgents) {
    GoogleString text;
    ResponseHeaders response_headers;
    RequestHeaders request_headers;
  
    // Blacklisted User Agent.
    request_headers.Add(HttpAttributes::kUserAgent, kBlackListUserAgent);
    FetchFromProxy("blacklist.html", true, request_headers, &text,
                   &response_headers, NULL, false, false);
    EXPECT_STREQ(kHtmlInput, text);
    VerifyBlacklistUserAgent(response_headers);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
    ClearStats();
  
    // NULL User Agent.
    request_headers.Add(HttpAttributes::kUserAgent, NULL);
    FetchFromProxy("noblink_text.html", true, request_headers, &text,
                   &response_headers, false);
    EXPECT_STREQ(
        StrCat("<html><head>"
               "</head><body>",
               GetJsDisableScriptSnippet(options_.get()),
               "<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
               "</script></body></html>"), text);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
    ClearStats();
  
    // Empty User Agent.
    request_headers.Replace(HttpAttributes::kUserAgent, "");
    FetchFromProxy("noblink_text.html", true, request_headers, &text,
                   &response_headers, false);
    EXPECT_STREQ(
        StrCat("<html><head>"
               "</head><body>",
               GetJsDisableScriptSnippet(options_.get()),
               "<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
               "</script></body></html>"), text);
    EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
    ClearStats();
  
    // Mobile User Agent.
    request_headers.Clear();
    request_headers.Add(
        HttpAttributes::kUserAgent,
        UserAgentMatcherTestBase::kIPhone4Safari);  // Mobile Request.
    request_headers.Add(HttpAttributes::kXForwardedFor, "127.0.0.1");
  
    FetchFromProxy("text.html", true, request_headers, &text, &response_headers,
                   true);
    VerifyNonCacheHtmlResponse(response_headers);
    EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
  
    ClearStats();
    // Hit case.
    response_headers.Clear();
    FetchFromProxy("text.html", true, request_headers, &text, &response_headers,
                   false);
    VerifyCacheHtmlResponse(response_headers);
    UnEscapeString(&text);
    EXPECT_STREQ(blink_output_, text);
  }
  
  class CacheHtmlPrioritizeCriticalCssTest : public CacheHtmlFlowTest {
   public:
    virtual void SetUp() {
      CacheHtmlFlowTest::SetUp();
      SetOptions();
      InitializeResponses();
    }
  
    virtual void InitHasher() {}  // avoid UseMd5Hasher() in base class
  
    void SetOptions() {
      // Enable FlushSubresourcesFilter filter.
      options_->ClearSignatureForTesting();
      options_->EnableFilter(RewriteOptions::kCachePartialHtml);
      options_->EnableFilter(RewriteOptions::kPrioritizeCriticalCss);
      options_->DisableFilter(RewriteOptions::kRewriteJavascriptExternal);
      options_->DisableFilter(RewriteOptions::kRewriteJavascriptInline);
      options_->set_non_cacheables_for_cache_partial_html(
          "class=item,id=beforeItems");
      options_->set_in_place_rewriting_enabled(true);
      options_->set_use_selectors_for_critical_css(false);
      options_->ComputeSignature();
    }
  
    void InitializeResponses() {
      // Some weird but valid CSS.
      SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
                                    "div,span,*::first-letter { display: block; }"
                                    "p { display: inline; }",
                                    kHtmlCacheTimeSec * 2);
      SetResponseWithDefaultHeaders("b.css?x=1&y=2", kContentTypeCss,
                                    "@media screen,print { * { margin: 0px; } }",
                                    kHtmlCacheTimeSec * 2);
    }
  
    GoogleString InputHtml() {
      return GoogleString(
          "<!doctype html PUBLIC \"HTML 4.0.1 Strict>"
          "<html><head>"
          "<title>Flush Subresources Early example</title>"
          "<link rel=\"stylesheet\" type=\"text/css\" href=\"a.css\">"
          "<link rel=\"stylesheet\" type=\"text/css\" href=\"b.css?x=1&y=2\">"
          "</head>"
          "<body>"
          "Hello, mod_pagespeed!"
          "</body></html>");
    }
  
    GoogleString ExpectedHtml(GoogleString full_styles_html) {
      GoogleString expected_html = StrCat(
          "<!doctype html PUBLIC \"HTML 4.0.1 Strict>"
          "<html><head>"
          "<title>Flush Subresources Early example</title>",
          "<style>div,*::first-letter{display:block}</style>"  // from a.css
          "<style>@media screen{*{margin:0}}</style>"  // from b.css
          "</head>");
      StrAppend(&expected_html,
          "<body>",
          StringPrintf(kNoScriptRedirectFormatter,
                       kNoScriptTextUrl, kNoScriptTextUrl),
          "Hello, mod_pagespeed!",
          full_styles_html,
          "</body></html>");
      StrAppend(&expected_html,
          GetJsDisableScriptSnippet(options_.get()),
          "<script type=\"text/javascript\" src=\"/psajs/blink.0.js\"></script>"
          "<script type=\"text/javascript\">"
          "pagespeed.panelLoaderInit();</script>\n"
          "<script type=\"text/javascript\">"
          "pagespeed.panelLoader.setRequestFromInternalIp();</script>\n",
          kCookieScript,
          "<script>pagespeed.panelLoader.bufferNonCriticalData({});</script>");
      return expected_html;
    }
  
    GoogleString CssLinkEncodedHref(GoogleString url) {
      return StrCat(
          "<link rel=\"stylesheet\" type=\"text/css\" href=\"",
          Encode("", "cf", kMockHashValue, url, "css"),
          "\">");
    }
  
    GoogleString url() { return kTestUrl; }
  
    void ValidateCacheHtml(const StringPiece& case_id,
                            const GoogleString& input_html,
                            const GoogleString& expected_html) {
      SetFetchResponse(url(), response_headers_, input_html);
      ResponseHeaders headers;
      GoogleString actual_html;
      // First request updates the property cache with cached html.
      FetchFromProxyWaitForBackground(
          url(), true /* expect_success */, &actual_html, &headers);
      VerifyNonCacheHtmlResponse(headers);
      headers.Clear();
  
      // Fetch the url again (with no wait) and expect a cache html hit.
      FetchFromProxyNoWaitForBackground(
          url(), true /* expect_success */, &actual_html, &headers);
      VerifyCacheHtmlResponse(headers);
      UnEscapeString(&actual_html);
  
      EXPECT_EQ(expected_html, actual_html) << "Test id:" << case_id;
    }
  };
  
  TEST_F(CacheHtmlPrioritizeCriticalCssTest, CacheHtmlWithCriticalCss) {
    // Add critical css rules.
    MockCriticalCssFinder* critical_css_finder =
        new MockCriticalCssFinder(rewrite_driver(), statistics());
    server_context()->set_critical_css_finder(critical_css_finder);
    critical_css_finder->AddCriticalCss(
        "http://test.com/a.css", "div,*::first-letter{display:block}", 100);
    critical_css_finder->AddCriticalCss(
        "http://test.com/b.css?x=1&y=2", "@media screen{*{margin:0}}", 100);
  
    GoogleString full_styles_html = StrCat(
        "<noscript class=\"psa_add_styles\">"
        "<link rel=\"stylesheet\" type=\"text/css\" href=\"a.css\">"
        "<link rel=\"stylesheet\" type=\"text/css\" href=\"b.css?x=1&y=2\">"
        "</noscript>"
        "<script data-pagespeed-no-defer type=\"text/javascript\">",
        CriticalCssFilter::kAddStylesScript,
        "window['pagespeed'] = window['pagespeed'] || {};"
        "window['pagespeed']['criticalCss'] = {"
        "  'total_critical_inlined_size': 60,"
        "  'total_original_external_size': 200,"
        "  'total_overhead_size': 60,"
        "  'num_replaced_links': 2,"
        "  'num_unreplaced_links': 0};"
        "</script>");
    ValidateCacheHtml(
        "critical_css", InputHtml(), ExpectedHtml(full_styles_html));
  }
  
  class TestCriticalSelectorFinder : public CriticalSelectorFinder {
   public:
    TestCriticalSelectorFinder(const PropertyCache::Cohort* cohort,
                               Statistics* stats)
        : CriticalSelectorFinder(cohort, NULL /* nonce_generator */, stats) {}
  
    virtual ~TestCriticalSelectorFinder() {}
  
    virtual int SupportInterval() const { return 1; }
  
   protected:
    virtual bool ShouldReplacePriorResult() const { return true; }
  
   private:
    DISALLOW_COPY_AND_ASSIGN(TestCriticalSelectorFinder);
  };
  
  TEST_F(CacheHtmlPrioritizeCriticalCssTest, CacheHtmlWithCriticalSelectors) {
      SetMockHashValue("00000");  // Base64 encodes to kMockHashValue.
    server_context()->set_enable_property_cache(true);
    PropertyCache* pcache = server_context()->page_property_cache();
    const PropertyCache::Cohort* dom_cohort =
        SetupCohort(pcache, RewriteDriver::kDomCohort);
    const PropertyCache::Cohort* beacon_cohort =
        SetupCohort(pcache, RewriteDriver::kBeaconCohort);
    server_context()->set_dom_cohort(dom_cohort);
    server_context()->set_beacon_cohort(beacon_cohort);
  
    rewrite_driver()->Clear();
    rewrite_driver()->set_request_context(
        RequestContext::NewTestRequestContext(factory()->thread_system()));
  
    MockPropertyPage* page = NewMockPage(
        url(), kMockHashValue /* hash */, UserAgentMatcher::kDesktop);
    rewrite_driver()->set_property_page(page);
    pcache->Read(page);
  
    options_->ClearSignatureForTesting();
    options_->set_use_selectors_for_critical_css(true);
    options_->ComputeSignature();
  
    server_context()->set_critical_selector_finder(new TestCriticalSelectorFinder(
        server_context()->beacon_cohort(), statistics()));
  
    // Write critical selectors to property cache
    StringSet selectors;
    selectors.insert("div");
    selectors.insert("*");
    CriticalSelectorFinder* finder = server_context()->critical_selector_finder();
    finder->WriteCriticalSelectorsToPropertyCache(
        selectors, "" /* last_nonce */, rewrite_driver());
    rewrite_driver()->property_page()->
        WriteCohort(server_context()->beacon_cohort());
  
    EXPECT_TRUE(finder->IsCriticalSelector(rewrite_driver(), "div"));
    EXPECT_TRUE(finder->IsCriticalSelector(rewrite_driver(), "*"));
  
    GoogleString full_styles_html = StrCat(
        "<noscript class=\"psa_add_styles\">",
        // URLs are encoded because CSS rewrite is enabled with selectors filter.
        CssLinkEncodedHref("a.css"), CssLinkEncodedHref("b.css?x=1&y=2"),
        "</noscript>"
        "<script data-pagespeed-no-defer type=\"text/javascript\">",
        rewrite_driver()->server_context()->static_asset_manager()->GetAsset(
            StaticAssetEnum::CRITICAL_CSS_LOADER_JS,
            rewrite_driver()->options()),
        "pagespeed.CriticalCssLoader.Run();</script>");
    ValidateCacheHtml(
        "critical_selector", InputHtml(), ExpectedHtml(full_styles_html));
  }
  
  }  // namespace net_instaweb
  